From f8905d39d0fa21f0dfb0a3dc5c8e520f9d7803f6 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:00:49 +0100 Subject: [PATCH 1/8] Add captured-data React hydration and a shipped browser helper --- .github/workflows/jsx.yml | 36 +++ .github/workflows/release.yml | 21 +- CHANGELOG.md | 15 ++ README.md | 1 + browser/hydrate.d.mts | 22 ++ browser/hydrate.mjs | 119 ++++++++++ docs/react.md | 130 ++++++++++- src/Engine.php | 57 ++++- src/Hydration/HydrationOptions.php | 31 +++ src/Hydration/PropsSnapshot.php | 109 +++++++++ src/Hydration/RenderedRoot.php | 30 +++ tests/Jsx/JsxHydrationTest.php | 281 ++++++++++++++++++++++++ tests/Jsx/hydration-run.php | 49 +++++ tests/Jsx/hydration/Root.jsx | 46 ++++ tests/Jsx/hydration/capture.jsx | 8 + tests/Jsx/hydration/format.js | 3 + tests/Jsx/oracle/hydration-contract.mjs | 192 ++++++++++++++++ 17 files changed, 1129 insertions(+), 21 deletions(-) create mode 100644 browser/hydrate.d.mts create mode 100644 browser/hydrate.mjs create mode 100644 src/Hydration/HydrationOptions.php create mode 100644 src/Hydration/PropsSnapshot.php create mode 100644 src/Hydration/RenderedRoot.php create mode 100644 tests/Jsx/JsxHydrationTest.php create mode 100644 tests/Jsx/hydration-run.php create mode 100644 tests/Jsx/hydration/Root.jsx create mode 100644 tests/Jsx/hydration/capture.jsx create mode 100644 tests/Jsx/hydration/format.js create mode 100644 tests/Jsx/oracle/hydration-contract.mjs diff --git a/.github/workflows/jsx.yml b/.github/workflows/jsx.yml index c13ee372..da8597cd 100644 --- a/.github/workflows/jsx.yml +++ b/.github/workflows/jsx.yml @@ -11,6 +11,7 @@ on: branches: [main] paths: - 'src/**' + - 'browser/**' - 'tests/Jsx/**' - 'phpunit.xml.dist' - 'composer.json' @@ -20,6 +21,7 @@ on: branches: [main] paths: - 'src/**' + - 'browser/**' - 'tests/Jsx/**' - 'phpunit.xml.dist' - 'composer.json' @@ -89,3 +91,37 @@ jobs: env: TZ: UTC run: node tests/Jsx/oracle/hydrate.mjs + + - name: Qualify the Composer hydration consumer + run: node tests/Jsx/oracle/hydration-contract.mjs + + hydration-runtime: + name: Hydration runtime PHP ${{ matrix.php }} + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + matrix: + php: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v5 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, bcmath, intl, curl + coverage: none + + - uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + cache-dependency-path: tests/Jsx/oracle/package-lock.json + + - name: Install the pinned browser toolchain + working-directory: tests/Jsx/oracle + run: npm ci --no-audit --no-fund + + # Only the downstream runtime is installed. PHPUnit requires newer PHP + # than our supported library minimum and is not a runtime dependency. + - name: Qualify the Composer hydration consumer + run: node tests/Jsx/oracle/hydration-contract.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6737704e..b157b6d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + with: + ref: ${{ github.event.release.tag_name }} - uses: shivammathur/setup-php@v2 with: @@ -20,27 +22,20 @@ jobs: extensions: mbstring, bcmath, curl, intl tools: composer - - run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + - name: Stage the canonical runtime archive + run: | + mkdir -p dist/phasis + git archive HEAD | tar -x -C dist/phasis + composer install --working-dir=dist/phasis --no-dev --no-interaction --prefer-dist --optimize-autoloader - name: Verify autoloader run: | + cd dist/phasis php -r "require 'vendor/autoload.php'; class_exists('Phasis\\Engine', true) || exit(1); echo 'Autoloader OK';" php bin/phasis -e 'console.log(1 + 1)' | grep -q '^2$' - name: Create release archive run: | - mkdir -p dist - rsync -a --exclude='.git' --exclude='.github' --exclude='.claude' \ - --exclude='.compat-state' --exclude='.phpunit*' --exclude='.phpstan*' \ - --exclude='tests' --exclude='test262' --exclude='scenarios' \ - --exclude='bench' --exclude='docs' --exclude='prds' \ - --exclude='AGENTS.md' --exclude='CLAUDE.md' --exclude='BENCH.md' --exclude='bench.json' \ - --exclude='compat.json' --exclude='phpunit.xml*' --exclude='phpcs.xml' \ - --exclude='phpstan.neon*' \ - --exclude='bin/compat-*' --exclude='bin/build-*' --exclude='bin/gen-*' \ - --exclude='bin/test-regression' --exclude='bin/test262' \ - --exclude='bin/verify-all' \ - . dist/phasis/ cd dist && zip -r ../phasis.zip phasis/ - name: Upload release asset diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b348dd..796ecb3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [0.10.0] - 2026-10-25 + +### Added +- `Engine::renderForHydration()` captures public JSON-data props before module execution and returns immutable matched React HTML, script-safe state and root metadata, without changing the existing rendering or callable interop APIs. +- Explicit hydration entry/build/root identities, named exports, React `createElement` semantics and matching `identifierPrefix` forwarding to native Fizz and the browser. +- Composer-shipped `browser/hydrate.mjs` and TypeScript declarations. The helper uses the consumer's peer React/React DOM pair, validates the captured transport and identities, rejects duplicate initialization, and preserves server HTML on transport failure. +- Downstream Composer archive tests covering development/production hydration, node reuse, connected imports/context/state, render-time mutation, safe data shapes and real React diagnostics on PHP 8.2 through 8.5. + +### Changed +- Release download archives use the same export-ignore runtime inventory as Composer before installing their bundled production autoloader. + +### Upgrade notes +- The new helper targets React/React DOM 19.3.1 and element roots. Full-document convenience transport and renderer nonce/CSP options are explicitly unsupported; existing document rendering remains available through `render()`. +- Only public data belongs in hydration props. Larger exact integers must be strings. WordPress loaders, application context, asset identities and cache policy remain the embedding application's responsibility. + ## [0.9.0] - 2026-10-24 ### Added diff --git a/README.md b/README.md index e64ae5cf..64beaa8a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Phasis lexes, parses, and executes ECMAScript in pure PHP. No `exec('node …')` - **Experimental Express and Hono support** through opt-in Node compatibility: CommonJS packages, Buffer, streams, filesystem access, HTTP/HTTPS/HTTP2 servers and clients, TLS, crypto, timers, and async context, executed inside PHP. See the [module coverage and host limits](./docs/modules.md#node-compatibility). - **JSX Pack**: render JSX components to HTML from PHP with `Engine::render()`, no build step - **Native React 19.3 server renderer** in PHP: `react`, `react-dom/server` and `react-dom/static` are built in, with HTML byte-identical to `react-dom@19.3.1` and hydratable by React on the client (hooks, context, Suspense, `useId`, async components, resource hints) + - **Captured-data hydration**: `Engine::renderForHydration()` pairs HTML with safe public props and a Composer-shipped browser helper; see the [data and browser contract](./docs/react.md#rendering-a-component-for-hydration) - **General HTML renderer** (`phasis`, `phasis/jsx-runtime`) for emails, feeds and static pages, byte-identical to preact-render-to-string 6.8.1, plus `raw()` - **Bundler-style imports**: extensionless paths and `index` files, `jsconfig` aliases, `node_modules` with `package.json` `exports`, CommonJS packages, style and asset imports, import maps, and barrel pruning for `sideEffects: false` packages - Optional precompile of static markup, an `opcache.preload` script, and an on-disk parse cache diff --git a/browser/hydrate.d.mts b/browser/hydrate.d.mts new file mode 100644 index 00000000..a00d4815 --- /dev/null +++ b/browser/hydrate.d.mts @@ -0,0 +1,22 @@ +import type { HydrationOptions, Root } from 'react-dom/client'; + +/** A transport or identity failure detected before React starts hydrating. */ +export declare class HydrationError extends Error { + readonly code: 'root' | 'duplicate' | 'state' | 'protocol' | 'identity' | 'react' | 'export' | 'diagnostics'; + constructor(code: HydrationError['code'], message: string); +} + +/** Identities compiled into the host's external browser entry and React diagnostics. */ +export interface ExpectedHydration extends Pick { + entryId: string; + buildId: string; + export?: string; + onError?: (error: HydrationError) => void; +} + +/** Hydrate using a statically imported module and the root's adjacent Phasis state. */ +export declare function hydrate( + root: Element, + componentModule: object, + expected: ExpectedHydration, +): Root; diff --git a/browser/hydrate.mjs b/browser/hydrate.mjs new file mode 100644 index 00000000..abd5f3de --- /dev/null +++ b/browser/hydrate.mjs @@ -0,0 +1,119 @@ +import { createElement, version as reactVersion } from 'react'; +import { version as reactDOMVersion } from 'react-dom'; +import { hydrateRoot } from 'react-dom/client'; + +const protocol = 1; +const initialized = new WeakSet(); +const fields = [ + 'protocol', 'entryId', 'export', 'buildId', 'rootId', + 'identifierPrefix', 'reactVersion', 'props', +]; + +function isFiniteData(value, depth = 0) { + if (depth > 128) return false; + if (typeof value === 'number') return Number.isFinite(value); + if (value && typeof value === 'object') { + return Object.values(value).every((item) => isFiniteData(item, depth + 1)); + } + return true; +} + +/** A transport or identity error detected before React changes the server HTML. */ +export class HydrationError extends Error { + constructor(code, message) { + super(message); + this.name = 'HydrationError'; + this.code = code; + } +} + +/** + * Hydrate one element using its adjacent captured state and a statically + * imported component module. Identity failures throw and preserve the HTML. + */ +export function hydrate(root, componentModule, expected) { + const fail = (code, message) => { + const error = new HydrationError(code, message); + try { + expected?.onError?.(error); + } finally { + throw error; + } + }; + if (!root || root.nodeType !== 1 || root.namespaceURI !== 'http://www.w3.org/1999/xhtml' + || ['html', 'head', 'body'].includes(root.localName) || !root.id) { + fail('root', 'Hydration requires an HTML element root with a nonempty ID.'); + } + if (initialized.has(root) || root.hasAttribute('data-phasis-hydrated')) { + fail('duplicate', 'This element has already been initialized by Phasis.'); + } + const matches = Array.from(root.ownerDocument.querySelectorAll('[id]')).filter((element) => element.id === root.id); + if (matches.length !== 1 || matches[0] !== root) { + fail('root', 'The hydration root ID must be unique within its document.'); + } + const tag = root.nextElementSibling; + if (!tag || tag.localName !== 'script' || tag.type !== 'application/json' + || tag.getAttribute('data-phasis-state') !== root.id) { + fail('state', 'The hydration root must be followed by its inert Phasis state tag.'); + } + let state; + try { + state = JSON.parse(tag.textContent); + } catch { + fail('state', 'The hydration state is not valid JSON.'); + } + if (!state || typeof state !== 'object' || Array.isArray(state) + || Object.keys(state).length !== fields.length + || fields.some((field) => !Object.hasOwn(state, field)) + || !state.props || typeof state.props !== 'object' || Array.isArray(state.props) + || fields.slice(1, 7).some((field) => typeof state[field] !== 'string') + || ['entryId', 'export', 'buildId', 'rootId', 'reactVersion'].some((field) => state[field] === '')) { + fail('state', 'The hydration state has an invalid shape.'); + } + if (!isFiniteData(state.props)) { + fail('state', 'The hydration props exceed the data depth or contain a non-finite number.'); + } + if (state.protocol !== protocol) { + fail('protocol', 'Unsupported Phasis hydration protocol.'); + } + if (!expected || typeof expected.entryId !== 'string' || !expected.entryId + || typeof expected.buildId !== 'string' || !expected.buildId + || (expected.export !== undefined && (typeof expected.export !== 'string' || !expected.export))) { + fail('identity', 'The browser entry must supply its expected entryId, buildId and export.'); + } + if (state.entryId !== expected.entryId || state.buildId !== expected.buildId + || state.export !== (expected.export ?? 'default') || state.rootId !== root.id) { + fail('identity', 'The server root and browser entry identities do not match.'); + } + if (state.reactVersion !== reactVersion || reactDOMVersion !== reactVersion) { + fail('react', 'The server, React and React DOM versions must match exactly.'); + } + if (!componentModule || !Object.hasOwn(componentModule, state.export)) { + fail('export', 'The statically imported module does not own the selected component export.'); + } + const component = componentModule[state.export]; + if (component === null || !['function', 'object', 'string', 'symbol'].includes(typeof component)) { + fail('export', 'The selected module export is not a React component.'); + } + const options = { identifierPrefix: state.identifierPrefix }; + for (const name of ['onRecoverableError', 'onCaughtError', 'onUncaughtError']) { + if (expected[name] !== undefined) { + if (typeof expected[name] !== 'function') { + fail('diagnostics', `${name} must be a function.`); + } + options[name] = expected[name]; + } + } + // React retains ownership of actual hydration diagnostics and recovery. + // A stale transport never reaches hydrateRoot or a createRoot fallback. + const element = createElement(component, state.props); + initialized.add(root); + root.setAttribute('data-phasis-hydrated', ''); + try { + return hydrateRoot(root, element, options); + } catch (error) { + initialized.delete(root); + root.removeAttribute('data-phasis-hydrated'); + throw error; + } +} diff --git a/docs/react.md b/docs/react.md index 826796fa..b53facda 100644 --- a/docs/react.md +++ b/docs/react.md @@ -32,21 +32,131 @@ export default function CommentsToggle({ count, excerpt }) { } ``` -PHP renders it and embeds the props, so the client renders the same tree: +For a matched server render and hydration payload, capture public data with +`renderForHydration()`. PHP needs no Node process: ```php -$props = ['count' => 3, 'excerpt' => $excerpt]; -echo '
' . $engine->render(__DIR__ . '/components/CommentsToggle.jsx', $props) . '
'; -echo ''; +use Phasis\Engine; +use Phasis\Hydration\HydrationOptions; + +$engine = new Engine(); +try { + $root = $engine->renderForHydration( + __DIR__ . '/components/CommentsToggle.jsx', + ['count' => 3, 'excerpt' => $excerpt], + options: new HydrationOptions( + rootId: 'comments', + entryId: 'comments', + buildId: $assetBuildId, + identifierPrefix: 'comments-', + ), + ); + echo $root->toHtml(); +} finally { + $engine->close(); +} ``` -On the client, standard React 19.3 tooling: +An external browser entry statically imports the same component module and the +shipped helper. Bundle this with the host's existing esbuild or other tooling: ```jsx -hydrateRoot(document.getElementById('comments'), ); +import * as Comments from './components/CommentsToggle.jsx'; +import { hydrate } from './vendor/phasis/phasis/browser/hydrate.mjs'; + +hydrate(document.getElementById('comments'), Comments, { + entryId: 'comments', + buildId: ASSET_BUILD_ID, // Supplied by this build, matching PHP's identity. + onError: reportTransportError, + onRecoverableError: reportReactError, + onCaughtError: reportReactError, + onUncaughtError: reportReactError, +}); ``` -The text markers React needs (`` between adjacent text) are in the output, and `useId()` produces the same `_R_..._` ids the client generates. The client must run React 19.3: other versions generate different ids and markup. +The helper is a runtime asset of the **Composer** package, at +`vendor/phasis/phasis/browser/hydrate.mjs`, with `hydrate.d.mts` declarations. +The example assumes an entry in the project root; adjust the relative path or +configure an explicit bundler alias for that installed file. There is no Phasis +npm package or implicit bare-specifier resolution. Its peer imports (`react`, +`react-dom`, `react-dom/client`) resolve through the consumer's bundler. Install +one compatible React/React DOM pair, currently **19.3.1**. The helper does not +bundle another copy. Node is development/build tooling, never the visitor's +execution backend. + +`RenderedRoot` exposes readonly `html`, `payload` and `options`. The payload is +captured **before** the component module executes. Phasis renders from those +captured values; later PHP mutation, JS mutation during render, or a component's +`toJSON` cannot alter the stored snapshot. The component sees ordinary props and +can provide them through its own React context. No automatic context or global +application singleton is created. PHP-to-JS callable/object interop remains +available through the existing `render()` and `renderSource()` APIs. + +### Data contract + +- Root props are a PHP map or a plain `stdClass`; empty PHP root props become + `{}`. A nonempty root list is rejected. Nested list arrays remain JSON arrays; + maps become JSON objects. Use `new stdClass()` for a nested empty object. +- Values are null, booleans, valid UTF-8 strings, finite doubles, safe PHP + integers, arrays and exact `stdClass` instances. Callable-looking strings and + arrays stay data. Arbitrary objects, serializers, closures, resources, cycles, + invalid UTF-8 and structures deeper than 128 levels are rejected before any + component module executes. Shared acyclic objects are allowed. +- PHP integers must fit `[-9007199254740991, 9007199254740991]`; use strings for + larger exact integers. PHP floats already have IEEE 754 double precision and + keep that precision, including signed zero. They may represent approximate + large numbers. Encoding and rendering temporarily use round-trip + `serialize_precision=-1` and restore the host setting, including on failure. +- Data-only JSON conversion preserves object/array distinctions, null and keys + without invoking PHP callbacks, JS `toJSON`, or JSON revivers. Normal React + `createElement` rules then apply to the root: reserved props and class + `defaultProps` behave as on the client. Use a named `data` prop for application + fields that collide with React's own props. + +### Transport, identity and diagnostics + +The versioned payload contains only protocol version, public entry/export/build +identity, root ID, React version, identifier prefix and captured props. The server +component path and executable source are never included. Public identities are +provided by the host; do not use a secret or server path as an identity. + +`toHtml()` emits a div with an escaped ID and an adjacent inert +`'; + } +} diff --git a/tests/Jsx/JsxHydrationTest.php b/tests/Jsx/JsxHydrationTest.php new file mode 100644 index 00000000..92be186c --- /dev/null +++ b/tests/Jsx/JsxHydrationTest.php @@ -0,0 +1,281 @@ +engine = new Engine(); + } + + protected function tearDown(): void + { + $this->engine->close(); + } + + private function options(string $rootId = 'root', string $prefix = 'test-'): HydrationOptions + { + return new HydrationOptions(rootId: $rootId, entryId: 'test-entry', buildId: 'build-123', identifierPrefix: $prefix); + } + + public function testCapturesBeforeModuleEvaluationAndNeverReserializesRenderedProps(): void + { + $data = (object) ['value' => 10]; + $this->engine->setGlobal('changeInput', static function () use ($data): void { + $data->value = 99; + }); + $result = $this->engine->renderForHydration(__DIR__ . '/hydration/capture.jsx', ['data' => $data], options: $this->options()); + $this->assertSame(99, $data->value); + $this->assertSame('

11

', $result->html); + $state = json_decode($result->payload, true, flags: JSON_THROW_ON_ERROR); + $this->assertSame(['value' => 10], $state['props']['data']); + $this->assertSame(1, $state['protocol']); + $this->assertSame('19.3.1', $state['reactVersion']); + $this->assertStringNotContainsString(__DIR__, $result->payload); + $this->assertStringNotContainsString('toJSON', $result->payload); + } + + public function testDataShapesAndCallableLookingArraysRemainData(): void + { + $data = (object) [ + 'emptyObject' => new \stdClass(), + 'emptyArray' => [], + 'null' => null, + 'callable' => ['DateTime', 'createFromFormat'], + '__proto__' => (object) ['polluted' => true], + 'constructor' => ['prototype' => ['safe' => true]], + 'toJSON' => 'ordinary data', + 'numericMap' => [2 => 'two', 0 => 'zero'], + ]; + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => $data], + export: 'Shapes', + options: $this->options(), + ); + $state = json_decode($result->payload, flags: JSON_THROW_ON_ERROR); + $this->assertInstanceOf(\stdClass::class, $state->props->data->emptyObject); + $this->assertSame([], $state->props->data->emptyArray); + $this->assertSame(['DateTime', 'createFromFormat'], $state->props->data->callable); + $this->assertTrue($state->props->data->__proto__->polluted); + $this->assertStringContainsString('"toJSON":"ordinary data"', $result->html); + $this->assertFalse($this->engine->eval('Object.hasOwn(Object.prototype, "polluted")')); + } + + public function testEmptyRootPropsAreObjectsAndClassDefaultsAndReservedPropsMatchCreateElement(): void + { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['key' => 'root-key', '__self' => 'hidden', '__source' => 'hidden', 'ref' => null], + export: 'Defaults', + options: $this->options(), + ); + $this->assertSame('

fallback:label

', $result->html); + $empty = $this->engine->renderForHydration(__DIR__ . '/hydration/Root.jsx', export: 'Defaults', options: $this->options()); + $this->assertInstanceOf(\stdClass::class, json_decode($empty->payload)->props); + $this->assertSame('

fallback:label

', $empty->html); + } + + public function testFiniteDoublesSignedZeroAndSafeIntegersRoundTripDespiteHostPrecision(): void + { + $previous = ini_set('serialize_precision', '3'); + try { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => ['zero' => -0.0, 'decimal' => 1.2345678901234567, 'max' => 9007199254740991, 'large' => 1.0e100]], + export: 'Shapes', + options: $this->options(), + ); + $this->assertSame('3', ini_get('serialize_precision')); + $state = json_decode($result->payload); + $this->assertSame(-INF, fdiv(1.0, $state->props->data->zero)); + $this->assertSame(1.2345678901234567, $state->props->data->decimal); + $this->assertSame(9007199254740991, $state->props->data->max); + $this->assertStringContainsString('negative-zero', $result->html); + $this->assertStringContainsString('1.2345678901234567', $result->html); + } finally { + ini_set('serialize_precision', $previous); + } + } + + public function testScriptSafetyAndAttributeEscapingAreSeparate(): void + { + $text = '' . "\u{2028}\u{2029}"; + $id = 'root"<&\''; + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => ['text' => $text]], + export: 'Shapes', + options: $this->options($id), + ); + $this->assertStringNotContainsString('<', $result->payload); + $this->assertStringNotContainsString('"', $result->payload); + $this->assertSame($text, json_decode($result->payload)->props->data->text); + $this->assertStringContainsString('id="root"<&'"', $result->toHtml()); + $this->assertSame(1, substr_count($result->toHtml(), '' . "\u{2028}\u{2029}", + ]]], +]; +$html = ''; +foreach ($cases as $case) { + $engine = new Engine(); + try { + $rendered = $engine->renderForHydration( + $consumer . '/components/Root.jsx', + $case['props'], + export: $case['export'], + options: new HydrationOptions( + rootId: $case['id'], + entryId: 'contract', + buildId: 'build-123', + identifierPrefix: $case['id'] . '-', + ), + ); + $html .= $rendered->toHtml(); + } finally { + $engine->close(); + } +} +echo $html; diff --git a/tests/Jsx/hydration/Root.jsx b/tests/Jsx/hydration/Root.jsx new file mode 100644 index 00000000..07f6c0af --- /dev/null +++ b/tests/Jsx/hydration/Root.jsx @@ -0,0 +1,46 @@ +import React, { Component, createContext, useContext, useId, useState } from 'react'; +import { format } from './format.js'; + +const Data = createContext(null); + +function Counter({ count, increment }) { + const data = useContext(Data); + const id = useId(); + return ; +} + +function Value({ count }) { + const data = useContext(Data); + return {data.value}:{count}; +} + +export default function Root({ data }) { + const [count, setCount] = useState(0); + // Deliberate impurity proves that the payload is the pre-render capture. + if (data.mutate) data.value += 1; + return +
+ setCount(count + 1)} /> + +
+
; +} + +export class Defaults extends Component { + static defaultProps = { label: 'fallback' }; + render() { + return

{this.props.label}:{Object.keys(this.props).sort().join(',')}

; + } +} + +export function Shapes({ data }) { + return
{JSON.stringify(data)}:{Object.is(data.zero, -0) ? 'negative-zero' : 'positive-zero'}:{data.decimal}
; +} + +export function Document() { + return DocumentContent; +} + +export function Broken() { + throw new Error('render failed deliberately'); +} diff --git a/tests/Jsx/hydration/capture.jsx b/tests/Jsx/hydration/capture.jsx new file mode 100644 index 00000000..9eb65d3b --- /dev/null +++ b/tests/Jsx/hydration/capture.jsx @@ -0,0 +1,8 @@ +globalThis.moduleLoaded = true; +globalThis.changeInput?.(); + +export default function Capture({ data }) { + data.value += 1; + data.toJSON = () => { throw new Error('must not reserialize rendered props'); }; + return

{data.value}

; +} diff --git a/tests/Jsx/hydration/format.js b/tests/Jsx/hydration/format.js new file mode 100644 index 00000000..9141334f --- /dev/null +++ b/tests/Jsx/hydration/format.js @@ -0,0 +1,3 @@ +export function format(value) { + return `Count: ${value}`; +} diff --git a/tests/Jsx/oracle/hydration-contract.mjs b/tests/Jsx/oracle/hydration-contract.mjs new file mode 100644 index 00000000..261c6b67 --- /dev/null +++ b/tests/Jsx/oracle/hydration-contract.mjs @@ -0,0 +1,192 @@ +// Qualify the actual Composer archive in an isolated downstream consumer. +// Node/esbuild are test/build tools; PHP alone executes the server render. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import esbuild from 'esbuild'; +import { JSDOM } from 'jsdom'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.resolve(here, '../../..'); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'phasis-hydration-')); +const sourceMode = process.argv.includes('--source'); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const until = async (test) => { + const deadline = Date.now() + 5000; + while (!test()) { + if (Date.now() >= deadline) throw new Error('Timed out waiting for React.'); + await sleep(10); + } +}; +let checks = 0; +try { + if (sourceMode) { + fs.mkdirSync(path.join(temp, 'vendor/phasis'), { recursive: true }); + fs.symlinkSync(repo, path.join(temp, 'vendor/phasis/phasis'), 'dir'); + fs.writeFileSync(path.join(temp, 'vendor/autoload.php'), ` /react\/cjs\/react\.(production|development)\.js$/.test(input)); + assert.equal(reactInputs.length, 1, 'The helper and components must share one React copy.'); + const code = bundle.outputFiles[0].text; + const dom = new JSDOM(`${html}`, { + runScripts: 'outside-only', pretendToBeVisual: true, url: 'https://consumer.test/', + }); + const { window } = dom; + const logs = []; + window.console.error = (...args) => logs.push(args.map(String).join(' ')); + window.console.warn = (...args) => logs.push(args.map(String).join(' ')); + const roots = []; + try { + window.eval(code); + const { hydrate, HydrationError, components } = window.contract; + const errors = []; + const options = (name = 'default') => ({ + entryId: 'contract', buildId: 'build-123', export: name, + onRecoverableError: (error) => errors.push(error), + onUncaughtError: (error) => errors.push(error), + onCaughtError: (error) => errors.push(error), + }); + const before = Array.from(window.document.querySelectorAll('div[id]')).flatMap((root) => { + const nodes = []; + const walker = window.document.createTreeWalker(root, 1 | 4 | 128); + for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node); + return nodes; + }); + for (const [id, name] of [['alpha', 'default'], ['beta', 'default'], ['named', 'Defaults'], ['empty', 'Defaults'], ['shapes', 'Shapes']]) { + roots.push(hydrate(window.document.getElementById(id), components, options(name))); + } + await sleep(100); + assert.deepEqual(errors, []); + assert.deepEqual(logs, [], 'Hydration must produce no React warnings.'); + assert.ok(before.every((node) => node.isConnected), 'All server elements, text and comments must be reused.'); + assert.equal(window.document.querySelector('#alpha output').textContent, '11:0'); + const alphaState = JSON.parse(window.document.getElementById('alpha').nextElementSibling.textContent); + assert.equal(alphaState.props.data.value, 10, 'Payload must precede render mutation.'); + assert.equal(window.document.querySelector('#named p').textContent, 'fallback:label'); + assert.equal(window.document.querySelector('#empty p').textContent, 'fallback:label'); + const a = window.document.querySelector('#alpha button'); + const b = window.document.querySelector('#beta button'); + assert.ok(a.id.startsWith('_alpha-')); + assert.ok(b.id.startsWith('_beta-')); + assert.notEqual(a.id, b.id); + assert.equal(window.exploit, undefined); + assert.equal(window.document.querySelectorAll('script').length, 5, 'Captured text must not escape an inert state tag.'); + assert.equal(window.Object.prototype.polluted, undefined); + a.click(); + await until(() => window.document.querySelector('#alpha output').textContent.endsWith(':1')); + assert.equal(window.document.querySelector('#beta output').textContent, '20:0'); + checks += 15; + + // Mismatched metadata fails synchronously, preserving the exact nodes. + const base = window.document.getElementById('beta'); + const baseState = JSON.parse(base.nextElementSibling.textContent); + const make = (change = () => {}) => { + const root = window.document.createElement('div'); + const id = `failure-${checks}`; + root.id = id; + root.innerHTML = base.innerHTML; + const tag = window.document.createElement('script'); + tag.type = 'application/json'; + tag.setAttribute('data-phasis-state', id); + const state = { ...baseState, rootId: id }; + change(state); + tag.textContent = JSON.stringify(state); + window.document.body.append(root, tag); + return { root, tag }; + }; + const cases = [ + ['protocol', (state) => { state.protocol = 2; }], + ['entry', (state) => { state.entryId = 'other'; }], + ['build', (state) => { state.buildId = 'old'; }], + ['export', (state) => { state.export = 'Defaults'; }], + ['React', (state) => { state.reactVersion = '0.0.0'; }], + ['root', (state) => { state.rootId = 'other'; }], + ['extra executable URL', (state) => { state.url = 'https://untrusted.test/run.js'; }], + ['array props', (state) => { state.props = []; }], + ['null props', (state) => { state.props = null; }], + ['missing key', (state) => { delete state.identifierPrefix; }], + ]; + for (const [name, change] of cases) { + const { root, tag } = make(change); + const node = root.firstChild; + const original = root.innerHTML; + const reports = []; + assert.throws(() => hydrate(root, components, { ...options(), onError: (error) => reports.push(error) }), HydrationError, name); + assert.equal(reports.length, 1); + assert.equal(root.innerHTML, original); + assert.equal(root.firstChild, node); + root.remove(); tag.remove(); + checks++; + } + for (const text of ['{', '{"protocol":1}', base.nextElementSibling.textContent.replace('20', '1e400')]) { + const { root, tag } = make(); + tag.textContent = text; + assert.throws(() => hydrate(root, components, options()), HydrationError); + root.remove(); tag.remove(); + checks++; + } + const inherited = make(); + assert.throws(() => hydrate(inherited.root, Object.create({ default: components.default }), options()), HydrationError); + inherited.root.remove(); inherited.tag.remove(); + checks++; + assert.throws(() => hydrate(base, components, options()), HydrationError, 'duplicate initialization'); + window.eval(code); + assert.throws(() => window.contract.hydrate(base, components, options()), /already been initialized/, 'duplicate helper bundle'); + checks += 2; + + // Real markup mismatches go to the caller's React diagnostics. + const mismatch = make(); + mismatch.root.querySelector('output').textContent = 'stale'; + roots.push(hydrate(mismatch.root, components, options())); + await until(() => errors.length > 0); + assert.ok(errors.some((error) => /hydrat|match|#418/i.test(String(error)))); + checks++; + } finally { + for (const root of roots) root.unmount(); + window.close(); + } + console.log(`hydration contract: ${mode} passed`); + } + console.log(`hydration contract: ${checks} checks passed (${sourceMode ? 'source checkout' : 'Composer archive consumer'})`); +} finally { + fs.rmSync(temp, { recursive: true, force: true }); +} From b61d305a0005526abcd01d0220705f6ccdee9066 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:04:43 +0100 Subject: [PATCH 2/8] Compile the shipped hydration declarations in the consumer --- tests/Jsx/oracle/hydration-contract.mjs | 20 ++ tests/Jsx/oracle/package-lock.json | 394 +++++++++++++++++++++++- tests/Jsx/oracle/package.json | 9 +- 3 files changed, 418 insertions(+), 5 deletions(-) diff --git a/tests/Jsx/oracle/hydration-contract.mjs b/tests/Jsx/oracle/hydration-contract.mjs index 261c6b67..dfc99133 100644 --- a/tests/Jsx/oracle/hydration-contract.mjs +++ b/tests/Jsx/oracle/hydration-contract.mjs @@ -48,6 +48,26 @@ try { assert.equal(cli.trim(), '2'); checks += 4; } + fs.symlinkSync(path.join(here, 'node_modules'), path.join(temp, 'node_modules'), 'dir'); + const types = path.join(temp, 'types.mts'); + fs.writeFileSync(types, ` + import { hydrate, HydrationError } from './vendor/phasis/phasis/browser/hydrate.mjs'; + import type { Root } from 'react-dom/client'; + const root: Root = hydrate(document.createElement('div'), {}, { + entryId: 'contract', buildId: 'build', export: 'Named', + onError(error) { const code: HydrationError['code'] = error.code; }, + onRecoverableError(error, info) { const stack: string | undefined = info.componentStack; }, + }); + root.unmount(); + // @ts-expect-error Build identity is mandatory. + hydrate(document.createElement('div'), {}, { entryId: 'contract' }); + // @ts-expect-error Document hydration is outside the element-root contract. + hydrate(document, {}, { entryId: 'contract', buildId: 'build' }); + `); + execFileSync(process.execPath, [path.join(here, 'node_modules/typescript/bin/tsc'), + '--strict', '--noEmit', '--module', 'NodeNext', '--target', 'ES2022', '--preserveSymlinks', types], + { cwd: temp, stdio: 'pipe' }); + checks++; fs.cpSync(path.join(repo, 'tests/Jsx/hydration'), path.join(temp, 'components'), { recursive: true }); const html = execFileSync('php', [path.join(repo, 'tests/Jsx/hydration-run.php'), temp], { encoding: 'utf8', env: { ...process.env, XDEBUG_MODE: 'off' }, diff --git a/tests/Jsx/oracle/package-lock.json b/tests/Jsx/oracle/package-lock.json index 890a6c3d..4d517a6a 100644 --- a/tests/Jsx/oracle/package-lock.json +++ b/tests/Jsx/oracle/package-lock.json @@ -28,6 +28,11 @@ "react-markdown": "10.1.0", "tailwind-merge": "3.7.0", "use-sync-external-store": "1.7.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" } }, "node_modules/@asamuzakjp/css-color": { @@ -631,17 +636,366 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", @@ -771,8 +1125,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-urls": { "version": "5.0.0", @@ -2198,6 +2551,41 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/tests/Jsx/oracle/package.json b/tests/Jsx/oracle/package.json index 190ed39b..558603c3 100644 --- a/tests/Jsx/oracle/package.json +++ b/tests/Jsx/oracle/package.json @@ -17,6 +17,7 @@ "lucide-react": "1.56.0", "preact": "11.0.1", "preact-render-to-string": "6.8.1", + "prettier": "3.9.10", "prop-types": "15.8.1", "react": "19.3.1", "react-dom": "19.3.1", @@ -24,7 +25,11 @@ "react-is": "19.3.1", "react-markdown": "10.1.0", "tailwind-merge": "3.7.0", - "use-sync-external-store": "1.7.0", - "prettier": "3.9.10" + "use-sync-external-store": "1.7.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "typescript": "7.0.2" } } From 22bd158fba5d21cfddb9253c7e5949abeb9e6320 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 25 Oct 2026 10:12:24 +0000 Subject: [PATCH 3/8] Update COMPAT.md from CI [skip ci] --- COMPAT.md | 44 ++++++++-------- compat.json | 146 +++++++++++++++++++++++++--------------------------- 2 files changed, 92 insertions(+), 98 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 1503a4f5..29d74e84 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -3,13 +3,13 @@ Generated from a completed `test262` pass. Do not edit by hand. - Refresh: `./bin/compat-report` -- Snapshot time: `2026-10-24T08:22:25+00:00` +- Snapshot time: `2026-10-25T10:12:22+00:00` - Chunk size: `25` - Timeout: `300s` - Jobs: `4` - Groups: `161` - Test files: `53616` -- Git: `feat/express-hono-compatibility` @ `b4624b71` (dirty) +- Git: `feat/react-hydration` @ `f8905d39` (dirty) ## Summary @@ -198,26 +198,26 @@ Generated from a completed `test262` pass. Do not edit by hand. | Group | Kind | Duration | Files | Sample | |---|---|---:|---:|---| -| staging/sm | NORMAL | 187.532s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js` | -| staging/sm | NORMAL | 185.335s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js` | -| staging/sm | NORMAL | 183.376s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js` | -| staging/sm | NORMAL | 181.430s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js` | -| staging/sm | NORMAL | 176.894s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js` | -| staging/sm | NORMAL | 167.859s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js` | -| built-ins/decodeURIComponent | NORMAL | 164.085s | 1 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js` | -| staging/sm | NORMAL | 156.742s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js` | -| staging/sm | NORMAL | 152.821s | 1 | `test262/test/staging/sm/TypedArray/sort_large_countingsort.js` | -| staging/sm | NORMAL | 149.594s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js` | -| built-ins/decodeURI | NORMAL | 144.082s | 1 | `test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js` | -| staging/sm | NORMAL | 125.528s | 1 | `test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js` | -| built-ins/decodeURIComponent | NORMAL | 74.418s | 25 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js`
...
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js` | -| built-ins/decodeURI | NORMAL | 73.594s | 25 | `test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js`
...
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js` | -| annexB/built-ins/RegExp | NORMAL | 64.136s | 25 | `test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js`
`test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js`
...
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js`
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js` | -| intl402/DateTimeFormat | NORMAL | 60.635s | 25 | `test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js`
`test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js`
...
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/name.js`
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js` | -| intl402 | NORMAL | 59.788s | 22 | `test262/test/intl402/constructors-string-and-single-element-array.js`
`test262/test/intl402/constructors-taint-Object-prototype-2.js`
...
`test262/test/intl402/supportedLocalesOf-throws-if-element-not-string-or-object.js`
`test262/test/intl402/supportedLocalesOf-unicode-extensions-ignored.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 59.566s | 5 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nandinagari.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_New_Tai_Lue.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Newa.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nko.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nushu.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 59.490s | 5 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Inscriptional_Parthian.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Javanese.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kaithi.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kannada.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Katakana.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 58.496s | 5 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Todhri.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Tolong_Siki.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Toto.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Tulu_Tigalari.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Ugaritic.js` | +| staging/sm | NORMAL | 178.996s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js` | +| staging/sm | NORMAL | 178.664s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js` | +| staging/sm | NORMAL | 178.023s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js` | +| staging/sm | NORMAL | 177.820s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js` | +| staging/sm | NORMAL | 177.799s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js` | +| staging/sm | NORMAL | 166.032s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js` | +| built-ins/decodeURIComponent | NORMAL | 160.372s | 1 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js` | +| staging/sm | NORMAL | 154.132s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js` | +| staging/sm | NORMAL | 148.807s | 1 | `test262/test/staging/sm/TypedArray/sort_large_countingsort.js` | +| built-ins/decodeURI | NORMAL | 148.468s | 1 | `test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js` | +| staging/sm | NORMAL | 143.142s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js` | +| staging/sm | NORMAL | 119.585s | 1 | `test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js` | +| built-ins/decodeURI | NORMAL | 74.512s | 25 | `test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js`
...
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js` | +| built-ins/decodeURIComponent | NORMAL | 74.306s | 25 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js`
...
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js` | +| built-ins/TypedArray | NORMAL | 72.127s | 25 | `test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached-prototype.js`
`test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached.js`
...
`test262/test/built-ins/TypedArray/prototype/copyWithin/not-a-constructor.js`
`test262/test/built-ins/TypedArray/prototype/copyWithin/prop-desc.js` | +| intl402/DateTimeFormat | NORMAL | 66.659s | 12 | `test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js`
`test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js`
...
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/dangi-calendar-dates.js`
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/date-constructor-not-called.js` | +| annexB/built-ins/RegExp | NORMAL | 63.918s | 25 | `test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js`
`test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js`
...
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js`
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.933s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Shavian.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Siddham.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Sidetic.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.472s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Old_Uyghur.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Oriya.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Osage.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.432s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Palmyrene.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Pau_Cin_Hau.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Phags_Pa.js` | ## Runner Caveats diff --git a/compat.json b/compat.json index b9fd4630..55cb46aa 100644 --- a/compat.json +++ b/compat.json @@ -1,10 +1,10 @@ { - "generatedAt": "2026-10-24T08:22:25+00:00", - "updatedAt": "2026-10-24T08:22:25+00:00", + "generatedAt": "2026-10-25T10:12:22+00:00", + "updatedAt": "2026-10-25T10:12:22+00:00", "complete": true, "git": { - "branch": "feat/express-hono-compatibility", - "commit": "b4624b71", + "branch": "feat/react-hydration", + "commit": "f8905d39", "isDirty": true }, "environment": { @@ -19,8 +19,8 @@ }, "state": { "directory": ".compat-state", - "fingerprint": "6dae375cd029fff559e6f2252d22bcbacb3f0284", - "runIdentity": "0730fc2d37920605e99fb17b1b6f28fff808f5d951dec70f65acd883210dd4b3" + "fingerprint": "4aa5c611c9bb9f4096c7be91527d157724bd4055", + "runIdentity": "a0d30642b348b33d3b54cb703f1f397a0e9fa52ef6f6102c8d227a2ae2e7fd72" }, "discovery": { "groupCount": 161, @@ -2719,16 +2719,16 @@ { "group": "staging/sm", "kind": "normal", - "durationSec": 187.532, + "durationSec": 178.996, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 185.335, + "durationSec": 178.664, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js" @@ -2737,34 +2737,34 @@ { "group": "staging/sm", "kind": "normal", - "durationSec": 183.376, + "durationSec": 178.023, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 181.43, + "durationSec": 177.82, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 176.894, + "durationSec": 177.799, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 167.859, + "durationSec": 166.032, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js" @@ -2773,7 +2773,7 @@ { "group": "built-ins/decodeURIComponent", "kind": "normal", - "durationSec": 164.085, + "durationSec": 160.372, "filesCount": 1, "sampleFiles": [ "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js" @@ -2782,7 +2782,7 @@ { "group": "staging/sm", "kind": "normal", - "durationSec": 156.742, + "durationSec": 154.132, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js" @@ -2791,141 +2791,135 @@ { "group": "staging/sm", "kind": "normal", - "durationSec": 152.821, + "durationSec": 148.807, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/TypedArray/sort_large_countingsort.js" ] }, { - "group": "staging/sm", + "group": "built-ins/decodeURI", "kind": "normal", - "durationSec": 149.594, + "durationSec": 148.468, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js" + "test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js" ] }, { - "group": "built-ins/decodeURI", + "group": "staging/sm", "kind": "normal", - "durationSec": 144.082, + "durationSec": 143.142, "filesCount": 1, "sampleFiles": [ - "test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js" + "test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 125.528, + "durationSec": 119.585, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js" ] }, { - "group": "built-ins/decodeURIComponent", + "group": "built-ins/decodeURI", "kind": "normal", - "durationSec": 74.418, + "durationSec": 74.512, "filesCount": 25, "sampleFiles": [ - "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js", - "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js", + "test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js", + "test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js", "...", - "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js", - "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js" + "test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js", + "test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js" ] }, { - "group": "built-ins/decodeURI", + "group": "built-ins/decodeURIComponent", "kind": "normal", - "durationSec": 73.594, + "durationSec": 74.306, "filesCount": 25, "sampleFiles": [ - "test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js", - "test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js", + "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js", + "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js", "...", - "test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js", - "test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js" + "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js", + "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js" ] }, { - "group": "annexB/built-ins/RegExp", + "group": "built-ins/TypedArray", "kind": "normal", - "durationSec": 64.136, + "durationSec": 72.127, "filesCount": 25, "sampleFiles": [ - "test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js", - "test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js", + "test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached-prototype.js", + "test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached.js", "...", - "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js", - "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js" + "test262/test/built-ins/TypedArray/prototype/copyWithin/not-a-constructor.js", + "test262/test/built-ins/TypedArray/prototype/copyWithin/prop-desc.js" ] }, { "group": "intl402/DateTimeFormat", "kind": "normal", - "durationSec": 60.635, - "filesCount": 25, + "durationSec": 66.659, + "filesCount": 12, "sampleFiles": [ "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js", "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js", "...", - "test262/test/intl402/DateTimeFormat/prototype/formatToParts/name.js", - "test262/test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js" + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/dangi-calendar-dates.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/date-constructor-not-called.js" ] }, { - "group": "intl402", + "group": "annexB/built-ins/RegExp", "kind": "normal", - "durationSec": 59.788, - "filesCount": 22, + "durationSec": 63.918, + "filesCount": 25, "sampleFiles": [ - "test262/test/intl402/constructors-string-and-single-element-array.js", - "test262/test/intl402/constructors-taint-Object-prototype-2.js", + "test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js", + "test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js", "...", - "test262/test/intl402/supportedLocalesOf-throws-if-element-not-string-or-object.js", - "test262/test/intl402/supportedLocalesOf-unicode-extensions-ignored.js" + "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js", + "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 59.566, - "filesCount": 5, + "durationSec": 59.933, + "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nandinagari.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_New_Tai_Lue.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Newa.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nko.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Nushu.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Shavian.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Siddham.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Sidetic.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 59.49, - "filesCount": 5, + "durationSec": 59.472, + "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Inscriptional_Parthian.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Javanese.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kaithi.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kannada.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Katakana.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Old_Uyghur.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Oriya.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Osage.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 58.496, - "filesCount": 5, + "durationSec": 59.432, + "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Todhri.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Tolong_Siki.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Toto.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Tulu_Tigalari.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Ugaritic.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Palmyrene.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Pau_Cin_Hau.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Phags_Pa.js" ] } ] From e6c2432095ac27c0342a6dcf0aaa3105bf764285 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:13:33 +0100 Subject: [PATCH 4/8] Preserve public data keys through React hydration --- CHANGELOG.md | 3 ++ src/BuiltIn/JsonObject.php | 48 +++++++++++++++++-------- src/BuiltIn/Jsx/React/ReactModule.php | 2 +- src/Engine.php | 2 +- src/Value/JsDataObject.php | 14 ++++++++ src/Value/JsObject.php | 8 ++--- tests/Jsx/JsxHydrationTest.php | 26 ++++++++++++++ tests/Jsx/hydration-run.php | 4 +++ tests/Jsx/hydration/Root.jsx | 4 +++ tests/Jsx/oracle/hydration-contract.mjs | 5 +-- 10 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 src/Value/JsDataObject.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 796ecb3c..ab886464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ ### Changed - Release download archives use the same export-ignore runtime inventory as Composer before installing their bundled production autoloader. +### Fixed +- Captured data preserves all public JSON keys, including names resembling native slots and leading null bytes, without interpreting `[[PrimitiveValue]]` as a boxed value. + ### Upgrade notes - The new helper targets React/React DOM 19.3.1 and element roots. Full-document convenience transport and renderer nonce/CSP options are explicitly unsupported; existing document rendering remains available through `render()`. - Only public data belongs in hydration props. Larger exact integers must be strings. WordPress loaders, application context, asset identities and cache policy remain the embedding application's responsibility. diff --git a/src/BuiltIn/JsonObject.php b/src/BuiltIn/JsonObject.php index 0e6823d6..d2a83ea6 100644 --- a/src/BuiltIn/JsonObject.php +++ b/src/BuiltIn/JsonObject.php @@ -250,7 +250,7 @@ private static function trivialJsonValue(JsValue $value, array &$stack, array &$ // Boolean / Number / String / BigInt) since spec ToJSON // unwraps these to their primitives, while our walker would // emit them as bare {} objects. - if ($value->getOwnPropertyDescriptor('[[PrimitiveValue]]') !== null) { + if (!$value instanceof \Phasis\Value\JsDataObject && $value->getOwnPropertyDescriptor('[[PrimitiveValue]]') !== null) { return self::trivialBailout(); } // RawJSON object: bail so the spec serialiser emits its raw @@ -318,7 +318,10 @@ private static function trivialJsonValue(JsValue $value, array &$stack, array &$ $integerKeys = []; $stringKeys = []; foreach (array_keys($value->properties->dataSlots) as $name) { - if (str_starts_with((string) $name, '[[') && str_ends_with((string) $name, ']]')) { + if ( + !$value instanceof \Phasis\Value\JsDataObject + && str_starts_with((string) $name, '[[') && str_ends_with((string) $name, ']]') + ) { continue; } // PHP stdClass treats NUL bytes / non-printable chars as @@ -392,7 +395,7 @@ private static function arrayLikeLength(JsObject $value): int private static function hasNumberData(JsObject $obj): bool { - if (!$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { return false; } @@ -401,7 +404,7 @@ private static function hasNumberData(JsObject $obj): bool private static function hasStringData(JsObject $obj): bool { - if (!$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { return false; } @@ -410,7 +413,7 @@ private static function hasStringData(JsObject $obj): bool private static function hasBooleanData(JsObject $obj): bool { - if (!$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { return false; } @@ -419,7 +422,7 @@ private static function hasBooleanData(JsObject $obj): bool private static function hasBigIntData(JsObject $obj): bool { - if (!$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { return false; } @@ -459,9 +462,21 @@ private static function enumerableOwnPropertyNames(JsObject $obj): array * * Mirrors the JSON.parse fast path (no reviver). Throws SyntaxError on * malformed input. + * + * @param bool $publicProperties Preserve every JSON key as public data, including legacy native-slot-looking names. */ - public static function parseSource(string $text): JsValue + public static function parseSource(string $text, bool $publicProperties = false): JsValue { + if ($publicProperties) { + // Associative validation accepts JSON keys (including leading NUL) + // that PHP cannot represent as decoded stdClass properties. + try { + json_decode($text, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + throw new \Phasis\Exceptions\SyntaxError('Unexpected token in JSON'); + } + return self::parseWithSources($text, publicProperties: true)['value']; + } $trimmed = trim($text); if ($trimmed === '-0') { return JsNumber::of(-0.0); @@ -514,13 +529,13 @@ private static function parse(): \Closure /** * @return array{value: JsValue, root: JsObject, sourceMap: array} */ - private static function parseWithSources(string $text): array + private static function parseWithSources(string $text, bool $publicProperties = false): array { /** @var array $sourceMap */ $sourceMap = []; $pos = 0; - $root = new JsObject(); - $value = self::parseJsonValue($text, $pos, $sourceMap, $root, ''); + $root = $publicProperties ? new \Phasis\Value\JsDataObject() : new JsObject(); + $value = self::parseJsonValue($text, $pos, $sourceMap, $root, '', $publicProperties); $root->defineOwnProperty('', PropertyDescriptor::data($value, true, true, true)); return ['value' => $value, 'root' => $root, 'sourceMap' => $sourceMap]; } @@ -534,6 +549,7 @@ private static function parseJsonValue( array &$sourceMap, JsObject $holder, string $key, + bool $publicProperties = false, ): JsValue { self::skipWhitespace($text, $pos); $len = strlen($text); @@ -545,10 +561,10 @@ private static function parseJsonValue( $ch = $text[$pos]; if ($ch === '{') { - return self::parseJsonObjectWithSources($text, $pos, $sourceMap); + return self::parseJsonObjectWithSources($text, $pos, $sourceMap, $publicProperties); } if ($ch === '[') { - return self::parseJsonArrayWithSources($text, $pos, $sourceMap); + return self::parseJsonArrayWithSources($text, $pos, $sourceMap, $publicProperties); } if ($ch === '"') { $start = $pos; @@ -593,9 +609,10 @@ private static function parseJsonObjectWithSources( string $text, int &$pos, array &$sourceMap, + bool $publicProperties = false, ): JsObject { $pos++; // skip { - $obj = new JsObject(); + $obj = $publicProperties ? new \Phasis\Value\JsDataObject() : new JsObject(); self::skipWhitespace($text, $pos); if ($pos < strlen($text) && $text[$pos] === '}') { @@ -614,7 +631,7 @@ private static function parseJsonObjectWithSources( throw new \Phasis\Exceptions\SyntaxError('Expected colon in JSON object'); } $pos++; // skip : - $propVal = self::parseJsonValue($text, $pos, $sourceMap, $obj, $propKey); + $propVal = self::parseJsonValue($text, $pos, $sourceMap, $obj, $propKey, $publicProperties); $obj->defineOwnProperty($propKey, PropertyDescriptor::data($propVal, true, true, true)); self::skipWhitespace($text, $pos); if ($pos >= strlen($text)) { @@ -640,6 +657,7 @@ private static function parseJsonArrayWithSources( string $text, int &$pos, array &$sourceMap, + bool $publicProperties = false, ): JsArray { $pos++; // skip [ $arr = JsArray::fromArray([]); @@ -653,7 +671,7 @@ private static function parseJsonArrayWithSources( while ($pos < strlen($text)) { $propKey = (string) $index; - $propVal = self::parseJsonValue($text, $pos, $sourceMap, $arr, $propKey); + $propVal = self::parseJsonValue($text, $pos, $sourceMap, $arr, $propKey, $publicProperties); $arr->defineOwnProperty($propKey, PropertyDescriptor::data($propVal, true, true, true)); $index++; $arr->set('length', JsNumber::of($index)); diff --git a/src/BuiltIn/Jsx/React/ReactModule.php b/src/BuiltIn/Jsx/React/ReactModule.php index 228a0bed..de1432ee 100644 --- a/src/BuiltIn/Jsx/React/ReactModule.php +++ b/src/BuiltIn/Jsx/React/ReactModule.php @@ -125,7 +125,7 @@ public static function createElement(array $args): JsElement { $type = $args[0] ?? JsUndefined::instance(); $config = $args[1] ?? JsUndefined::instance(); - $props = new JsObject(); + $props = new \Phasis\Value\JsDataObject(); $key = JsNull::instance(); if (!Js::isNullish($config)) { $configKey = Js::get($config, 'key'); diff --git a/src/Engine.php b/src/Engine.php index 52168547..81a8ae42 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -2191,7 +2191,7 @@ public function renderForHydration( } return Hydration\PropsSnapshot::withPrecision(fn (): Hydration\RenderedRoot => $this->withCurrentInterpreter( function () use ($resolved, $payload, $export, $options): Hydration\RenderedRoot { - $state = BuiltIn\JsonObject::parseSource($payload); + $state = BuiltIn\JsonObject::parseSource($payload, publicProperties: true); if (!$state instanceof JsObject || !($jsProps = $state->get('props')) instanceof JsObject) { throw new \LogicException('Captured hydration props must be an object.'); } diff --git a/src/Value/JsDataObject.php b/src/Value/JsDataObject.php new file mode 100644 index 00000000..24e7d2c7 --- /dev/null +++ b/src/Value/JsDataObject.php @@ -0,0 +1,14 @@ +properties->keys(), - fn(string $k) => !self::isInternalSlot($k), + fn(string $k) => !static::isInternalSlot($k), )); } @@ -993,7 +993,7 @@ public function getOwnPropertyNames(): array $integerIndices = []; $nonIndexStrings = []; foreach ($allKeys as $key) { - if (self::isInternalSlot($key)) { + if (static::isInternalSlot($key)) { continue; } if (self::isArrayIndex($key)) { @@ -1088,7 +1088,7 @@ public function ordinaryOwnPropertyKeys(): array $integerIndices = []; $nonIndexStrings = []; foreach ($stringKeys as $key) { - if (self::isInternalSlot($key)) { + if (static::isInternalSlot($key)) { continue; } if (self::isArrayIndex($key)) { @@ -1169,7 +1169,7 @@ public function getOwnEnumerableKeys(): array $integerIndices = []; $nonIndexStrings = []; foreach ($enumKeys as $key) { - if (self::isInternalSlot($key)) { + if (static::isInternalSlot($key)) { continue; } if (self::isArrayIndex($key)) { diff --git a/tests/Jsx/JsxHydrationTest.php b/tests/Jsx/JsxHydrationTest.php index 92be186c..1532df26 100644 --- a/tests/Jsx/JsxHydrationTest.php +++ b/tests/Jsx/JsxHydrationTest.php @@ -278,4 +278,30 @@ public function testInvalidMetadataFailsBeforeRendering(): void $this->expectException(\InvalidArgumentException::class); $this->engine->renderForHydration(__DIR__ . '/hydration/Root.jsx', export: '', options: $this->options()); } + + public function testDataKeysCannotBecomeHiddenNativeSlots(): void + { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['[[public]]' => true, 'data' => ['[[public]]' => 'visible', '[[PrimitiveValue]]' => 42]], + export: 'Keys', + options: $this->options(), + ); + $this->assertStringContainsString('[[public]],data', $result->html); + $this->assertStringContainsString('[[public]],[[PrimitiveValue]]', $result->html); + $this->assertStringContainsString('"[[public]]":"visible"', $result->html); + $this->assertStringContainsString('"[[PrimitiveValue]]":42', $result->html); + } + + public function testLeadingNullBytesInDataKeysRemainJsonProperties(): void + { + $result = $this->engine->renderForHydration( + __DIR__ . '/hydration/Root.jsx', + ['data' => ["\0field" => 'visible']], + export: 'Shapes', + options: $this->options(), + ); + $this->assertStringContainsString('\\u0000field', $result->html); + $this->assertStringContainsString('visible', $result->html); + } } diff --git a/tests/Jsx/hydration-run.php b/tests/Jsx/hydration-run.php index 9c2cdd37..7a72dbcb 100644 --- a/tests/Jsx/hydration-run.php +++ b/tests/Jsx/hydration-run.php @@ -12,6 +12,9 @@ ['id' => 'beta', 'export' => 'default', 'props' => ['data' => ['label' => 'Beta', 'value' => 20]]], ['id' => 'named', 'export' => 'Defaults', 'props' => ['key' => 'key', '__self' => 'hidden', '__source' => 'hidden', 'ref' => null]], ['id' => 'empty', 'export' => 'Defaults', 'props' => []], + ['id' => 'keys', 'export' => 'Keys', 'props' => ['[[public]]' => true, 'data' => [ + '[[public]]' => 'visible', '[[PrimitiveValue]]' => 42, + ]]], ['id' => 'shapes', 'export' => 'Shapes', 'props' => ['data' => (object) [ 'emptyObject' => new stdClass(), 'emptyArray' => [], @@ -23,6 +26,7 @@ 'large' => 1.0e100, '__proto__' => (object) ['polluted' => true], 'toJSON' => 'data only', + 'nulKeys' => ["\0field" => 'visible'], 'text' => '' . "\u{2028}\u{2029}", ]]], ]; diff --git a/tests/Jsx/hydration/Root.jsx b/tests/Jsx/hydration/Root.jsx index 07f6c0af..99a30930 100644 --- a/tests/Jsx/hydration/Root.jsx +++ b/tests/Jsx/hydration/Root.jsx @@ -37,6 +37,10 @@ export function Shapes({ data }) { return
{JSON.stringify(data)}:{Object.is(data.zero, -0) ? 'negative-zero' : 'positive-zero'}:{data.decimal}
; } +export function Keys(props) { + return
{Object.keys(props).sort().join(',')}:{Reflect.ownKeys(props.data).join(',')}:{JSON.stringify(props.data)}
; +} + export function Document() { return DocumentContent; } diff --git a/tests/Jsx/oracle/hydration-contract.mjs b/tests/Jsx/oracle/hydration-contract.mjs index dfc99133..24cf1744 100644 --- a/tests/Jsx/oracle/hydration-contract.mjs +++ b/tests/Jsx/oracle/hydration-contract.mjs @@ -111,7 +111,8 @@ try { for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node); return nodes; }); - for (const [id, name] of [['alpha', 'default'], ['beta', 'default'], ['named', 'Defaults'], ['empty', 'Defaults'], ['shapes', 'Shapes']]) { + for (const [id, name] of [['alpha', 'default'], ['beta', 'default'], ['named', 'Defaults'], ['empty', 'Defaults'], + ['shapes', 'Shapes'], ['keys', 'Keys']]) { roots.push(hydrate(window.document.getElementById(id), components, options(name))); } await sleep(100); @@ -129,7 +130,7 @@ try { assert.ok(b.id.startsWith('_beta-')); assert.notEqual(a.id, b.id); assert.equal(window.exploit, undefined); - assert.equal(window.document.querySelectorAll('script').length, 5, 'Captured text must not escape an inert state tag.'); + assert.equal(window.document.querySelectorAll('script').length, 6, 'Captured text must not escape an inert state tag.'); assert.equal(window.Object.prototype.polluted, undefined); a.click(); await until(() => window.document.querySelector('#alpha output').textContent.endsWith(':1')); From 94ebe6989fd063480e2648cdf94ec11dbb1176a6 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:17:16 +0100 Subject: [PATCH 5/8] Ship hydration usage documentation with the runtime helper --- browser/README.md | 34 +++++++++++++++++++++++++ tests/Jsx/oracle/hydration-contract.mjs | 3 ++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 browser/README.md diff --git a/browser/README.md b/browser/README.md new file mode 100644 index 00000000..13af95f4 --- /dev/null +++ b/browser/README.md @@ -0,0 +1,34 @@ +# Browser hydration + +This directory ships in `phasis/phasis`'s Composer archive. There is no Phasis +npm package. Resolve the helper's peer `react`, `react-dom` and `react-dom/client` +imports with the consumer's existing bundler, using one React/React DOM 19.3.1 +pair. `hydrate.d.mts` supplies TypeScript declarations. + +Render with `Engine::renderForHydration()` and explicit +`Phasis\Hydration\HydrationOptions`, then emit the returned `toHtml()`. From an +external browser entry in the project root: + +```js +import * as Components from './components/Root.jsx'; +import { hydrate } from './vendor/phasis/phasis/browser/hydrate.mjs'; + +hydrate(document.getElementById('site'), Components, { + entryId: 'site', + buildId: BUILD_ID, + onError: reportTransportError, + onRecoverableError: reportReactError, +}); +``` + +The entry/build/export identities must match PHP. The helper consumes the +adjacent inert state tag, validates its metadata and calls `hydrateRoot` with +the captured props and identifier prefix. Transport failures throw +`HydrationError` and preserve server HTML. React hydration failures use React's +diagnostic hooks. No payload-driven imports or `createRoot` fallback occur. + +Only element roots are qualified. Renderer nonce options and full-document +convenience transport are unsupported. Inert JSON alone does not qualify all +React-emitted instructions for a strict CSP. The host owns headers and caching. + +See the [complete PHP, data, transport and lifecycle contract](https://github.com/inline0/phasis/blob/main/docs/react.md). diff --git a/tests/Jsx/oracle/hydration-contract.mjs b/tests/Jsx/oracle/hydration-contract.mjs index 24cf1744..e5170269 100644 --- a/tests/Jsx/oracle/hydration-contract.mjs +++ b/tests/Jsx/oracle/hydration-contract.mjs @@ -43,10 +43,11 @@ try { execFileSync('composer', ['install', '--no-dev', '--no-plugins', '--no-scripts', '--no-interaction', '--quiet'], { cwd: temp, stdio: 'pipe' }); assert.ok(fs.existsSync(path.join(temp, 'vendor/phasis/phasis/browser/hydrate.mjs'))); assert.ok(fs.existsSync(path.join(temp, 'vendor/phasis/phasis/browser/hydrate.d.mts'))); + assert.ok(fs.existsSync(path.join(temp, 'vendor/phasis/phasis/browser/README.md'))); assert.equal(fs.existsSync(path.join(temp, 'vendor/phasis/phasis/tests')), false); const cli = execFileSync('php', [path.join(temp, 'vendor/bin/phasis'), '-e', 'console.log(1 + 1)'], { encoding: 'utf8', cwd: temp }); assert.equal(cli.trim(), '2'); - checks += 4; + checks += 5; } fs.symlinkSync(path.join(here, 'node_modules'), path.join(temp, 'node_modules'), 'dir'); const types = path.join(temp, 'types.mts'); From 194f06357c5fbd54f1b3f0904e1777a72d16f737 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:26:41 +0100 Subject: [PATCH 6/8] Keep public hydration data separate from native value brands --- CHANGELOG.md | 2 +- src/BuiltIn/AbortControllerConstructor.php | 4 +- src/BuiltIn/ArrayConstructor.php | 4 +- src/BuiltIn/AsyncContextConstructor.php | 4 +- src/BuiltIn/BlobConstructor.php | 8 +- src/BuiltIn/Date/DateConstruction.php | 4 +- src/BuiltIn/DateConstructor.php | 4 +- src/BuiltIn/DisposableStackConstructor.php | 28 ++--- src/BuiltIn/DomExceptionConstructor.php | 2 +- src/BuiltIn/ErrorConstructor.php | 12 +-- src/BuiltIn/EventTargetConstructor.php | 16 +-- .../FinalizationRegistryConstructor.php | 4 +- src/BuiltIn/FormDataConstructor.php | 6 +- src/BuiltIn/GlobalObject.php | 18 ++-- src/BuiltIn/Global_/GlobalWrappers.php | 8 +- src/BuiltIn/HeadersConstructor.php | 6 +- src/BuiltIn/Intl/CollatorSection.php | 12 +-- src/BuiltIn/Intl/DateTimeFormatSection.php | 100 +++++++++--------- src/BuiltIn/Intl/DisplayNamesSection.php | 6 +- src/BuiltIn/Intl/DurationFormatSection.php | 20 ++-- src/BuiltIn/Intl/ListFormatSection.php | 8 +- src/BuiltIn/Intl/LocaleSection.php | 16 +-- src/BuiltIn/Intl/NumberFormatSection.php | 18 ++-- src/BuiltIn/Intl/PluralRulesSection.php | 10 +- .../Intl/RelativeTimeFormatSection.php | 8 +- src/BuiltIn/Intl/SegmenterSection.php | 8 +- .../TemporalCalendarFormattingHelpers.php | 2 +- src/BuiltIn/IntlObject.php | 10 +- src/BuiltIn/IteratorConstructor.php | 16 +-- src/BuiltIn/JsonObject.php | 20 ++-- src/BuiltIn/MapConstructor.php | 6 +- src/BuiltIn/NumberConstructor.php | 4 +- src/BuiltIn/ObjectConstructor.php | 20 ++-- src/BuiltIn/PromiseConstructor.php | 4 +- src/BuiltIn/ProxyConstructor.php | 2 +- src/BuiltIn/RegExpPrototype.php | 14 +-- src/BuiltIn/RegExp_/RegExpExec.php | 30 +++--- src/BuiltIn/RegExp_/RegExpHelpers.php | 12 +-- src/BuiltIn/RegExp_/RegExpSymbolHooks.php | 2 +- src/BuiltIn/RequestConstructor.php | 4 +- src/BuiltIn/ResponseConstructor.php | 4 +- src/BuiltIn/SetConstructor.php | 6 +- src/BuiltIn/ShadowRealmConstructor.php | 4 +- src/BuiltIn/Streams/ByteStream.php | 4 +- src/BuiltIn/Streams/CompressionStream.php | 2 +- src/BuiltIn/Streams/QueuingStrategy.php | 8 +- src/BuiltIn/Streams/ReadableStream.php | 8 +- src/BuiltIn/Streams/TransformStream.php | 4 +- src/BuiltIn/Streams/WritableStream.php | 8 +- src/BuiltIn/String/StringIteration.php | 2 +- src/BuiltIn/String/StringSearch.php | 4 +- src/BuiltIn/StructuredCloneFunction.php | 8 +- src/BuiltIn/SymbolConstructor.php | 6 +- src/BuiltIn/Temporal/BrandCheckHelpers.php | 26 ++--- .../Temporal/CalendarRoundingHelpers.php | 6 +- src/BuiltIn/Temporal/DurationHelpers.php | 18 ++-- src/BuiltIn/Temporal/DurationSection.php | 20 ++-- .../Temporal/InstantParsingHelpers.php | 4 +- src/BuiltIn/Temporal/InstantSection.php | 4 +- .../Temporal/MethodRegistrationHelpers.php | 28 ++--- src/BuiltIn/Temporal/PlainDateSection.php | 13 +-- src/BuiltIn/Temporal/PlainDateTimeSection.php | 8 +- src/BuiltIn/Temporal/PlainMonthDaySection.php | 4 +- src/BuiltIn/Temporal/PlainTimeSection.php | 5 +- .../Temporal/PlainYearMonthSection.php | 4 +- src/BuiltIn/Temporal/TemporalUnitsHelpers.php | 2 +- src/BuiltIn/Temporal/TimezoneHelpers.php | 8 +- .../Temporal/TypeConversionHelpers.php | 31 +++--- src/BuiltIn/Temporal/ZonedDateTimeSection.php | 12 +-- src/BuiltIn/TemporalObject.php | 2 +- src/BuiltIn/TextDecoderConstructor.php | 4 +- src/BuiltIn/TextEncoderConstructor.php | 4 +- src/BuiltIn/TypedArray/BufferTypes.php | 8 +- src/BuiltIn/TypedArray/DataView.php | 6 +- src/BuiltIn/TypedArray/TypedArrayHelpers.php | 4 +- src/BuiltIn/Url/SearchParams.php | 6 +- src/BuiltIn/UrlConstructor.php | 4 +- src/BuiltIn/UrlPatternConstructor.php | 2 +- src/BuiltIn/WeakMapConstructor.php | 4 +- src/BuiltIn/WeakRefConstructor.php | 4 +- src/BuiltIn/WeakSetConstructor.php | 4 +- src/BuiltIn/WebAssemblyObject.php | 4 +- src/BuiltIn/WebSocketConstructor.php | 2 +- src/BuiltIn/XMLHttpRequestConstructor.php | 4 +- src/Bytecode/VM.php | 2 +- src/Engine.php | 18 ++-- src/Runtime/Parts/ExpressionEvaluation.php | 14 +-- src/Runtime/Parts/StatementExecution.php | 2 +- src/Value/JsFunction.php | 2 +- src/Value/JsObject.php | 36 +++++++ tests/Jsx/JsxHydrationTest.php | 2 + tests/Jsx/hydration/Root.jsx | 10 +- 92 files changed, 470 insertions(+), 421 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab886464..afa285a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - Release download archives use the same export-ignore runtime inventory as Composer before installing their bundled production autoloader. ### Fixed -- Captured data preserves all public JSON keys, including names resembling native slots and leading null bytes, without interpreting `[[PrimitiveValue]]` as a boxed value. +- Captured data preserves all public JSON keys, including names resembling native slots and leading null bytes. Native metadata readers keep those objects unbranded, so a public `[[PrimitiveValue]]` key cannot make them behave as boxed values. ### Upgrade notes - The new helper targets React/React DOM 19.3.1 and element roots. Full-document convenience transport and renderer nonce/CSP options are explicitly unsupported; existing document rendering remains available through `render()`. diff --git a/src/BuiltIn/AbortControllerConstructor.php b/src/BuiltIn/AbortControllerConstructor.php index 58635539..d5db0dc6 100644 --- a/src/BuiltIn/AbortControllerConstructor.php +++ b/src/BuiltIn/AbortControllerConstructor.php @@ -407,12 +407,12 @@ private static function buildControllerConstructor(JsObject $signalProto): JsFun $ctor = JsFunction::fromCallable( 'AbortController', function (JsValue $this_, array $args) use ($controllerProto, $signalProto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError( "Failed to construct 'AbortController': Please use the 'new' operator" ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype( diff --git a/src/BuiltIn/ArrayConstructor.php b/src/BuiltIn/ArrayConstructor.php index 8aec1f9b..85922318 100644 --- a/src/BuiltIn/ArrayConstructor.php +++ b/src/BuiltIn/ArrayConstructor.php @@ -53,8 +53,8 @@ public static function install(Environment $env): void // GetPrototypeFromConstructor: when newTarget.prototype is not an // Object, fall back to GetFunctionRealm(newTarget).[[%Array.prototype%]]. $protoToUse = null; - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { - $newTarget = $this_->get('[[NewTarget]]'); + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); $protoToUse = \Phasis\Spec\AbstractOperations::getPrototypeFromConstructor( $newTarget, static fn ($env) => \Phasis\Spec\AbstractOperations::realmIntrinsicPrototype($env, 'Array'), diff --git a/src/BuiltIn/AsyncContextConstructor.php b/src/BuiltIn/AsyncContextConstructor.php index 4ca10cae..a93d7a2b 100644 --- a/src/BuiltIn/AsyncContextConstructor.php +++ b/src/BuiltIn/AsyncContextConstructor.php @@ -67,7 +67,7 @@ private static function variableCtor(): JsFunction $ctor = JsFunction::fromCallable( 'Variable', static function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor AsyncContext.Variable requires 'new'"); } $this_->setPrototype($proto); @@ -195,7 +195,7 @@ private static function snapshotCtor(): JsFunction 'Snapshot', static function (JsValue $this_, array $args) use ($proto): JsValue { unset($args); - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor AsyncContext.Snapshot requires 'new'"); } $this_->setPrototype($proto); diff --git a/src/BuiltIn/BlobConstructor.php b/src/BuiltIn/BlobConstructor.php index 99e4c76a..0d3fcf8d 100644 --- a/src/BuiltIn/BlobConstructor.php +++ b/src/BuiltIn/BlobConstructor.php @@ -137,10 +137,10 @@ private static function buildBlobConstructor(JsObject $proto): JsFunction $ctor = JsFunction::fromCallable( 'Blob', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'Blob': Please use the 'new' operator"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); @@ -179,7 +179,7 @@ private static function buildFileConstructor(JsObject $proto, JsFunction $blobCt $ctor = JsFunction::fromCallable( 'File', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'File': Please use the 'new' operator"); } if (count($args) < 2) { @@ -188,7 +188,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { . count($args) . ' present.' ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); diff --git a/src/BuiltIn/Date/DateConstruction.php b/src/BuiltIn/Date/DateConstruction.php index 203d724e..bdfd0d67 100644 --- a/src/BuiltIn/Date/DateConstruction.php +++ b/src/BuiltIn/Date/DateConstruction.php @@ -295,7 +295,7 @@ public static function isDateObject(JsValue $value): bool if ($value->getInternalProperty('[[IsDate]]') === true) { return true; } - $marker = $value->get('[[IsDate]]'); + $marker = $value->getNativeSlot('[[IsDate]]'); return $marker instanceof JsBoolean && $marker->value; } @@ -329,7 +329,7 @@ private static function dateValueOf(JsObject $obj): float // [[DateValue]] as a real property descriptor still round-trip // through here. Keep both fallbacks so existing flows stay // correct. - $legacy = $obj->get('[[DateValue]]'); + $legacy = $obj->getNativeSlot('[[DateValue]]'); return $legacy instanceof JsNumber ? $legacy->value : NAN; } diff --git a/src/BuiltIn/DateConstructor.php b/src/BuiltIn/DateConstructor.php index 7391c984..f857df3e 100644 --- a/src/BuiltIn/DateConstructor.php +++ b/src/BuiltIn/DateConstructor.php @@ -42,7 +42,7 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable('Date', function (JsValue $this_, array $args) use ($proto): JsValue { // Called as a function (without new): always return the current date string. - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { return new JsString(self::toDateString(self::nowMs())); } @@ -50,7 +50,7 @@ public static function install(Environment $env): void // Per §9.1.13 OrdinaryCreateFromConstructor, the prototype must // come from newTarget.prototype (fall back to %Date.prototype% // if newTarget.prototype is not an object). - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; diff --git a/src/BuiltIn/DisposableStackConstructor.php b/src/BuiltIn/DisposableStackConstructor.php index b3e1e13f..c825122c 100644 --- a/src/BuiltIn/DisposableStackConstructor.php +++ b/src/BuiltIn/DisposableStackConstructor.php @@ -85,12 +85,12 @@ private static function installDisposableStack(Environment $env): void $proto = new JsObject(); $ctor = JsFunction::fromCallable('DisposableStack', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Constructor DisposableStack requires \'new\''); } // OrdinaryCreateFromConstructor: fetch newTarget.prototype and // apply it to this_ (so prototype getters throw through us). - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); $this_->setPrototype( @@ -183,7 +183,7 @@ private static function installDisposableStack(Environment $env): void if (!$this_ instanceof JsObject || !self::isDisposableStack($this_)) { throw new TypeError('Method DisposableStack.prototype.dispose requires a valid DisposableStack receiver'); } - $state = $this_->get('[[DisposableState]]'); + $state = $this_->getNativeSlot('[[DisposableState]]'); if ($state instanceof JsString && $state->value === 'disposed') { return JsUndefined::instance(); } @@ -207,7 +207,7 @@ private static function installDisposableStack(Environment $env): void false, false, )); - $internalStack = $this_->get('[[DisposableStack]]'); + $internalStack = $this_->getNativeSlot('[[DisposableStack]]'); $newStack->defineOwnProperty('[[DisposableStack]]', PropertyDescriptor::data( $internalStack, true, @@ -232,7 +232,7 @@ private static function installDisposableStack(Environment $env): void if (!$this_ instanceof JsObject || !self::isDisposableStack($this_)) { throw new TypeError('Method DisposableStack.prototype.disposed requires a valid DisposableStack receiver'); } - $state = $this_->get('[[DisposableState]]'); + $state = $this_->getNativeSlot('[[DisposableState]]'); return new JsBoolean($state instanceof JsString && $state->value === 'disposed'); }, 0), null, @@ -266,12 +266,12 @@ private static function installAsyncDisposableStack(Environment $env): void $proto = new JsObject(); $ctor = JsFunction::fromCallable('AsyncDisposableStack', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Constructor AsyncDisposableStack requires \'new\''); } // OrdinaryCreateFromConstructor: fetch newTarget.prototype and // apply it to this_ (so prototype getters throw through us). - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); $this_->setPrototype( @@ -367,7 +367,7 @@ private static function installAsyncDisposableStack(Environment $env): void 'disposeAsync requires a valid AsyncDisposableStack' ); } - $state = $this_->get('[[DisposableState]]'); + $state = $this_->getNativeSlot('[[DisposableState]]'); if ($state instanceof JsString && $state->value === 'disposed') { return \Phasis\Value\JsPromise::resolved(JsUndefined::instance()); } @@ -397,7 +397,7 @@ private static function installAsyncDisposableStack(Environment $env): void false, false, )); - $internalStack = $this_->get('[[DisposableStack]]'); + $internalStack = $this_->getNativeSlot('[[DisposableStack]]'); $newStack->defineOwnProperty('[[DisposableStack]]', PropertyDescriptor::data( $internalStack, true, @@ -421,7 +421,7 @@ private static function installAsyncDisposableStack(Environment $env): void if (!$this_ instanceof JsObject || !self::isAsyncDisposableStack($this_)) { throw new TypeError('Method AsyncDisposableStack.prototype.disposed requires a valid AsyncDisposableStack receiver'); } - $state = $this_->get('[[DisposableState]]'); + $state = $this_->getNativeSlot('[[DisposableState]]'); return new JsBoolean($state instanceof JsString && $state->value === 'disposed'); }, 0), null, @@ -471,7 +471,7 @@ private static function assertNotDisposed(JsValue $this_, bool $async = false): : 'Method DisposableStack.prototype requires a valid DisposableStack receiver', ); } - $state = $this_->get('[[DisposableState]]'); + $state = $this_->getNativeSlot('[[DisposableState]]'); if ($state instanceof JsString && $state->value === 'disposed') { throw new \Phasis\Exceptions\ReferenceError( 'Cannot add resource to a disposed stack.' @@ -490,7 +490,7 @@ private static function pushToStack(JsObject $this_, JsValue $resource, bool $is if ($record === null) { return; } - $stack = $this_->get('[[DisposableStack]]'); + $stack = $this_->getNativeSlot('[[DisposableStack]]'); if (!$stack instanceof JsObject) { $stack = new JsObject(); $this_->set('[[DisposableStack]]', $stack); @@ -512,7 +512,7 @@ private static function pushToStack(JsObject $this_, JsValue $resource, bool $is */ private static function disposeStack(JsObject $this_, bool $isAsync): void { - $stack = $this_->get('[[DisposableStack]]'); + $stack = $this_->getNativeSlot('[[DisposableStack]]'); if (!$stack instanceof JsObject) { return; } @@ -553,7 +553,7 @@ private static function disposeStack(JsObject $this_, bool $isAsync): void private static function disposeAsyncStack(JsObject $this_): JsPromise { $promise = new JsPromise(); - $stack = $this_->get('[[DisposableStack]]'); + $stack = $this_->getNativeSlot('[[DisposableStack]]'); $this_->set('[[DisposableStack]]', new JsObject()); if (!$stack instanceof JsObject) { $promise->resolve(JsUndefined::instance()); diff --git a/src/BuiltIn/DomExceptionConstructor.php b/src/BuiltIn/DomExceptionConstructor.php index 68438b14..3493127e 100644 --- a/src/BuiltIn/DomExceptionConstructor.php +++ b/src/BuiltIn/DomExceptionConstructor.php @@ -246,7 +246,7 @@ private static function makeConstructor(JsObject $proto): \Closure ? 'Error' : TypeConversion::toString($nameArg); - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { // Called via `new`: populate the pre-allocated object. // The prototype was already set by evalNewExpression based on // newTarget.prototype, so subclasses keep their chain. diff --git a/src/BuiltIn/ErrorConstructor.php b/src/BuiltIn/ErrorConstructor.php index 7d2fe08b..8cb7a72f 100644 --- a/src/BuiltIn/ErrorConstructor.php +++ b/src/BuiltIn/ErrorConstructor.php @@ -104,7 +104,7 @@ function (JsValue $this_): JsValue { // Per spec, check for [[ErrorData]] internal slot. // We mark real error objects with this slot during construction. return new \Phasis\Value\JsBoolean( - $arg->hasOwnProperty('[[ErrorData]]') + $arg->hasNativeSlot('[[ErrorData]]', own: true) ); }, 1); $constructor->defineOwnProperty('isError', PropertyDescriptor::data($isErrorFn, true, false, true)); @@ -259,11 +259,11 @@ private static function makeAggregateErrorConstructor(string $name, JsObject $pr $options = $args[2] ?? JsUndefined::instance(); // Determine target object. - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { $obj = $this_; // Per §9.1.13 OrdinaryCreateFromConstructor, set the prototype // from newTarget.prototype (fall back to intrinsicDefaultProto). - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; @@ -404,7 +404,7 @@ public static function makeConstructor(string $name, JsObject $proto): \Closure : null; $options = $args[1] ?? JsUndefined::instance(); - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { // Called via new: populate the already-created object. // Do NOT override the prototype: evalNewExpression already sets // the correct prototype based on new.target's .prototype property. @@ -484,11 +484,11 @@ private static function makeSuppressedErrorConstructor(JsObject $proto): \Closur $suppressedArg = $args[1] ?? JsUndefined::instance(); $messageArg = $args[2] ?? JsUndefined::instance(); - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { $obj = $this_; // Per §9.1.13 OrdinaryCreateFromConstructor, set prototype // from newTarget.prototype. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; diff --git a/src/BuiltIn/EventTargetConstructor.php b/src/BuiltIn/EventTargetConstructor.php index efb0a521..51ad6a4d 100644 --- a/src/BuiltIn/EventTargetConstructor.php +++ b/src/BuiltIn/EventTargetConstructor.php @@ -194,13 +194,13 @@ private static function createMessageEventConstructor(JsObject $eventProto, JsFu PropertyDescriptor::data(new JsString('MessageEvent'), false, false, true), ); $ctor = JsFunction::fromCallable('MessageEvent', static function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor MessageEvent requires 'new'"); } if (!isset($args[0])) { throw new TypeError('MessageEvent requires an event type'); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); $newProto = $newTarget instanceof JsObject ? $newTarget->get('prototype') : null; $this_->setPrototype($newProto instanceof JsObject ? $newProto : $proto); $type = TypeConversion::toString($args[0]); @@ -358,7 +358,7 @@ private static function createCustomEventConstructor( $ctor = JsFunction::fromCallable( 'CustomEvent', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor CustomEvent requires 'new'"); } if (!isset($args[0])) { @@ -366,7 +366,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { "Failed to construct 'CustomEvent': 1 argument required, but only 0 present." ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); @@ -423,10 +423,10 @@ private static function createEventTargetConstructor(JsObject $proto): JsFunctio $ctor = JsFunction::fromCallable( 'EventTarget', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor EventTarget requires 'new'"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); @@ -909,7 +909,7 @@ private static function createEventConstructor(JsObject $proto): JsFunction $ctor = JsFunction::fromCallable( 'Event', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor Event requires 'new'"); } // Per WebIDL the type argument is required. @@ -919,7 +919,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); diff --git a/src/BuiltIn/FinalizationRegistryConstructor.php b/src/BuiltIn/FinalizationRegistryConstructor.php index d5eca925..f5dcd1fd 100644 --- a/src/BuiltIn/FinalizationRegistryConstructor.php +++ b/src/BuiltIn/FinalizationRegistryConstructor.php @@ -31,7 +31,7 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'FinalizationRegistry', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor FinalizationRegistry requires \'new\''); } $callback = $args[0] ?? JsUndefined::instance(); @@ -40,7 +40,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } // Per spec OrdinaryCreateFromConstructor: use NewTarget's prototype. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ctorProto = $newTarget->get('prototype'); $useProto = $ctorProto instanceof JsObject ? $ctorProto : $proto; diff --git a/src/BuiltIn/FormDataConstructor.php b/src/BuiltIn/FormDataConstructor.php index c7b00fa5..48ca17df 100644 --- a/src/BuiltIn/FormDataConstructor.php +++ b/src/BuiltIn/FormDataConstructor.php @@ -91,12 +91,12 @@ private static function buildConstructor(JsObject $proto): JsFunction $ctor = JsFunction::fromCallable( 'FormData', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError( "Failed to construct 'FormData': Please use the 'new' operator" ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); @@ -481,7 +481,7 @@ function (JsValue $this_, array $args): JsValue { if (!$this_ instanceof JsObject) { throw new TypeError('not a FormData iterator'); } - $stateDesc = $this_->getOwnPropertyDescriptor('[[FDIterState]]'); + $stateDesc = $this_->getNativeSlotDescriptor('[[FDIterState]]'); if ($stateDesc === null) { throw new TypeError('not a FormData iterator'); } diff --git a/src/BuiltIn/GlobalObject.php b/src/BuiltIn/GlobalObject.php index 3d244ec2..5b0ed1c8 100644 --- a/src/BuiltIn/GlobalObject.php +++ b/src/BuiltIn/GlobalObject.php @@ -58,8 +58,8 @@ public static function install(Environment $env): void if ($this_ instanceof JsBoolean) { return $this_; } - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[PrimitiveValue]]')) { - $prim = $this_->get('[[PrimitiveValue]]'); + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[PrimitiveValue]]')) { + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsBoolean) { return $prim; } @@ -75,8 +75,8 @@ public static function install(Environment $env): void if ($this_ instanceof JsBoolean) { return new JsString($this_->toBoolean() ? 'true' : 'false'); } - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[PrimitiveValue]]')) { - $prim = $this_->get('[[PrimitiveValue]]'); + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[PrimitiveValue]]')) { + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsBoolean) { return new JsString($prim->toBoolean() ? 'true' : 'false'); } @@ -375,8 +375,8 @@ function (JsValue $this_, array $args) use ($env, $functionRealm): JsValue { } } // Honor new.target.prototype for subclassed Function constructor. - if ($result instanceof JsFunction && $this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]')) { - $newTarget = $this_->get('[[NewTarget]]'); + if ($result instanceof JsFunction && $this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof \Phasis\Value\JsObject) { @@ -647,14 +647,14 @@ function ( if ( $isConstructable && $th instanceof \Phasis\Value\JsObject - && !($th->get('[[NewTarget]]') instanceof JsUndefined) + && !($th->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) ) { if ($innerInterp !== null) { // Per §10.4.1.2 [[Construct]] for BoundFunction: // step 4. If SameValue(F, newTarget) is true, set // newTarget to target. This causes `new B()` where // `B = A.bind()` to pass newTarget=A to A's body. - $activeNewTarget = $th->get('[[NewTarget]]'); + $activeNewTarget = $th->getNativeSlot('[[NewTarget]]'); if ( $selfRef->fn !== null && $activeNewTarget === $selfRef->fn @@ -691,7 +691,7 @@ function ( // newTarget (extracted from $th's [[NewTarget]] slot // above) is honoured rather than collapsing to the // bound target's prototype. - $activeNewTarget = $th->get('[[NewTarget]]'); + $activeNewTarget = $th->getNativeSlot('[[NewTarget]]'); if ( $selfRef->fn !== null && $activeNewTarget === $selfRef->fn diff --git a/src/BuiltIn/Global_/GlobalWrappers.php b/src/BuiltIn/Global_/GlobalWrappers.php index 9b7c31ae..8aabb329 100644 --- a/src/BuiltIn/Global_/GlobalWrappers.php +++ b/src/BuiltIn/Global_/GlobalWrappers.php @@ -36,14 +36,14 @@ private static function stringConstructor(): \Closure // Per spec 22.1.1.1 step 2: when called as a function (no new.target), // a Symbol argument becomes its descriptive string. When called as a // constructor (new String(sym)), ToString(sym) still throws TypeError. - $isConstruct = $this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]'); + $isConstruct = $this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]'); if (!empty($args) && $args[0] instanceof \Phasis\Value\JsSymbol && !$isConstruct) { $str = $args[0]->display(); } else { $str = empty($args) ? '' : TypeConversion::toString($args[0]); } // When called as constructor (new String(x)), create wrapper object - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { $val = new JsString($str); $this_->defineOwnProperty( '[[PrimitiveValue]]', @@ -93,7 +93,7 @@ private static function numberConstructor(): \Closure } // When called as constructor (new Number(x)), set up wrapper. // valueOf/toString come from Number.prototype, not own properties. - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { $val = JsNumber::of($num); $this_->defineOwnProperty( '[[PrimitiveValue]]', @@ -109,7 +109,7 @@ private static function booleanConstructor(): \Closure { return function (JsValue $this_, array $args): JsValue { $bool = empty($args) ? false : TypeConversion::toBoolean($args[0]); - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { // Only set [[PrimitiveValue]], don't shadow prototype methods $this_->defineOwnProperty( '[[PrimitiveValue]]', diff --git a/src/BuiltIn/HeadersConstructor.php b/src/BuiltIn/HeadersConstructor.php index 2cdc2adf..6b4a7529 100644 --- a/src/BuiltIn/HeadersConstructor.php +++ b/src/BuiltIn/HeadersConstructor.php @@ -239,10 +239,10 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'Headers', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'Headers': Please use the 'new' operator"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); @@ -489,7 +489,7 @@ private static function buildIteratorPrototype(?JsObject $intrinsic): JsObject if (!$this_ instanceof JsObject) { throw new TypeError('Headers Iterator.next called on incompatible receiver'); } - $stateDesc = $this_->getOwnPropertyDescriptor('[[HeadersIterState]]'); + $stateDesc = $this_->getNativeSlotDescriptor('[[HeadersIterState]]'); if ($stateDesc === null) { throw new TypeError('Headers Iterator.next called on incompatible receiver'); } diff --git a/src/BuiltIn/Intl/CollatorSection.php b/src/BuiltIn/Intl/CollatorSection.php index eba131bf..35443089 100644 --- a/src/BuiltIn/Intl/CollatorSection.php +++ b/src/BuiltIn/Intl/CollatorSection.php @@ -267,7 +267,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { \Collator::ON, ); - $numericVal = $this_->get('[[Numeric]]'); + $numericVal = $this_->getNativeSlot('[[Numeric]]'); if ($numericVal instanceof JsBoolean && $numericVal->toBoolean()) { $collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON); } @@ -277,7 +277,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { // [[IgnorePunctuation]] choice — otherwise opting out // of ignorePunctuation on a Thai collator silently // keeps SHIFTED active. - $ipVal = $this_->get('[[IgnorePunctuation]]'); + $ipVal = $this_->getNativeSlot('[[IgnorePunctuation]]'); $ignorePunct = $ipVal instanceof JsBoolean && $ipVal->toBoolean(); $collator->setAttribute( \Collator::ALTERNATE_HANDLING, @@ -300,7 +300,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ) use ($compareFn): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedCollator]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedCollator]]') instanceof JsUndefined ) { throw new TypeError('Intl.Collator.prototype.compare called on non-Collator'); } @@ -329,7 +329,7 @@ enumerable: false, ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedCollator]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedCollator]]') instanceof JsUndefined ) { throw new TypeError('Intl.Collator.prototype.resolvedOptions called on non-Collator'); } @@ -342,14 +342,14 @@ enumerable: false, self::defineDataProp($result, 'sensitivity', new JsString( self::extractInternalString($this_, '[[Sensitivity]]', 'variant'), )); - $ipVal = $this_->get('[[IgnorePunctuation]]'); + $ipVal = $this_->getNativeSlot('[[IgnorePunctuation]]'); self::defineDataProp($result, 'ignorePunctuation', new JsBoolean( $ipVal instanceof JsBoolean ? $ipVal->toBoolean() : false, )); self::defineDataProp($result, 'collation', new JsString( self::extractInternalString($this_, '[[Collation]]', 'default'), )); - $numVal = $this_->get('[[Numeric]]'); + $numVal = $this_->getNativeSlot('[[Numeric]]'); self::defineDataProp($result, 'numeric', new JsBoolean( $numVal instanceof JsBoolean ? $numVal->toBoolean() : false, )); diff --git a/src/BuiltIn/Intl/DateTimeFormatSection.php b/src/BuiltIn/Intl/DateTimeFormatSection.php index 9db7cfd1..73383b39 100644 --- a/src/BuiltIn/Intl/DateTimeFormatSection.php +++ b/src/BuiltIn/Intl/DateTimeFormatSection.php @@ -547,7 +547,7 @@ enumerable: false, ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedDateTimeFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDateTimeFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.DateTimeFormat.prototype.formatToParts called on non-DateTimeFormat', @@ -1011,7 +1011,7 @@ enumerable: false, self::defineDataProp($result, 'timeZone', new JsString( self::extractInternalString($this_, '[[TimeZone]]', 'UTC'), )); - $hcVal = $this_->get('[[HourCycle]]'); + $hcVal = $this_->getNativeSlot('[[HourCycle]]'); if (!$hcVal instanceof JsUndefined) { $hcStr = TypeConversion::toString($hcVal); self::defineDataProp($result, 'hourCycle', new JsString($hcStr)); @@ -1029,7 +1029,7 @@ enumerable: false, ['weekday', 'era', 'year', 'month', 'day', 'dayPeriod', 'hour', 'minute', 'second', 'fractionalSecondDigits', 'timeZoneName'] as $comp ) { - $val = $this_->get("[[{$comp}]]"); + $val = $this_->getNativeSlot("[[{$comp}]]"); if ($val instanceof JsUndefined) { continue; } @@ -1043,11 +1043,11 @@ enumerable: false, } } - $dsVal = $this_->get('[[DateStyle]]'); + $dsVal = $this_->getNativeSlot('[[DateStyle]]'); if (!$dsVal instanceof JsUndefined) { self::defineDataProp($result, 'dateStyle', new JsString(TypeConversion::toString($dsVal))); } - $tsVal = $this_->get('[[TimeStyle]]'); + $tsVal = $this_->getNativeSlot('[[TimeStyle]]'); if (!$tsVal instanceof JsUndefined) { self::defineDataProp($result, 'timeStyle', new JsString(TypeConversion::toString($tsVal))); } @@ -1082,7 +1082,7 @@ private static function dateTimeFormatRangeReceiverCheck(JsValue $this_, string { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedDateTimeFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDateTimeFormat]]') instanceof JsUndefined ) { throw new TypeError( "Intl.DateTimeFormat.prototype.{$name} called on non-DateTimeFormat", @@ -2034,8 +2034,8 @@ private static function checkSameTemporalType(JsValue $a, JsValue $b): void } } // Instant: detected by [[EpochNanoseconds]] without ZDT. - $aIsInstant = $a->has('[[EpochNanoseconds]]') && !$a->has('[[IsZonedDateTime]]'); - $bIsInstant = $b->has('[[EpochNanoseconds]]') && !$b->has('[[IsZonedDateTime]]'); + $aIsInstant = $a->hasNativeSlot('[[EpochNanoseconds]]') && !$a->hasNativeSlot('[[IsZonedDateTime]]'); + $bIsInstant = $b->hasNativeSlot('[[EpochNanoseconds]]') && !$b->hasNativeSlot('[[IsZonedDateTime]]'); if ($aIsInstant !== $bIsInstant) { throw new TypeError( 'formatRange Temporal arguments must be the same type', @@ -2061,8 +2061,8 @@ private static function checkSameTemporalType(JsValue $a, JsValue $b): void } } if ($aHasCal && $bHasCal) { - $calA = $a->get('[[Calendar]]'); - $calB = $b->get('[[Calendar]]'); + $calA = $a->getNativeSlot('[[Calendar]]'); + $calB = $b->getNativeSlot('[[Calendar]]'); $calAStr = $calA instanceof JsString ? $calA->value : ''; $calBStr = $calB instanceof JsString ? $calB->value : ''; if ($calAStr !== '' && $calBStr !== '' && $calAStr !== $calBStr) { @@ -2075,15 +2075,15 @@ private static function checkSameTemporalType(JsValue $a, JsValue $b): void private static function isTemporalDateLike(JsObject $obj): bool { - return $obj->has('[[IsPlainDate]]') - || $obj->has('[[IsPlainDateTime]]') - || $obj->has('[[IsPlainTime]]') - || $obj->has('[[IsPlainYearMonth]]') - || $obj->has('[[IsPlainMonthDay]]') - || $obj->has('[[IsZonedDateTime]]') + return $obj->hasNativeSlot('[[IsPlainDate]]') + || $obj->hasNativeSlot('[[IsPlainDateTime]]') + || $obj->hasNativeSlot('[[IsPlainTime]]') + || $obj->hasNativeSlot('[[IsPlainYearMonth]]') + || $obj->hasNativeSlot('[[IsPlainMonthDay]]') + || $obj->hasNativeSlot('[[IsZonedDateTime]]') || ( - $obj->has('[[EpochNanoseconds]]') - && !$obj->has('[[IsZonedDateTime]]') + $obj->hasNativeSlot('[[EpochNanoseconds]]') + && !$obj->hasNativeSlot('[[IsZonedDateTime]]') ); } @@ -2095,7 +2095,7 @@ private static function isTemporalDateLike(JsObject $obj): bool */ private static function formatTemporal(JsObject $dtf, JsObject $obj): string { - if ($obj->has('[[IsZonedDateTime]]')) { + if ($obj->hasNativeSlot('[[IsZonedDateTime]]')) { // Spec: Temporal.ZonedDateTime is rejected by format() // and formatToParts(); the user must call its // toLocaleString instead. @@ -2105,7 +2105,7 @@ private static function formatTemporal(JsObject $dtf, JsObject $obj): string // PlainDate can't be used with time-only options; etc. self::checkTemporalOptionsCompat($dtf, $obj); - if ($obj->has('[[EpochNanoseconds]]')) { + if ($obj->hasNativeSlot('[[EpochNanoseconds]]')) { // Instant: uses epoch nanoseconds with the formatter's // own time zone. Use temporalFormatterFor so default // formatters get augmented to include both date AND @@ -2158,7 +2158,7 @@ private static function temporalFormatterFor(JsObject $dtf, JsObject $obj): \Int // values aren't shifted by the formatter's TimeZone option. // Instant uses the formatter's own zone (it carries an // absolute moment, so the zone is meaningful). - $isInstantBrand = $obj->has('[[EpochNanoseconds]]') && !$obj->has('[[IsZonedDateTime]]'); + $isInstantBrand = $obj->hasNativeSlot('[[EpochNanoseconds]]') && !$obj->hasNativeSlot('[[IsZonedDateTime]]'); if (!$isInstantBrand) { $base->setTimeZone(new \DateTimeZone('UTC')); } @@ -2166,12 +2166,12 @@ private static function temporalFormatterFor(JsObject $dtf, JsObject $obj): \Int if (!is_string($pattern) || $pattern === '') { return $base; } - $isPlainDate = $obj->has('[[IsPlainDate]]'); - $isPlainDateTime = $obj->has('[[IsPlainDateTime]]'); - $isPlainTime = $obj->has('[[IsPlainTime]]'); - $isPlainYearMonth = $obj->has('[[IsPlainYearMonth]]'); - $isPlainMonthDay = $obj->has('[[IsPlainMonthDay]]'); - $isInstant = $obj->has('[[EpochNanoseconds]]') && !$obj->has('[[IsZonedDateTime]]'); + $isPlainDate = $obj->hasNativeSlot('[[IsPlainDate]]'); + $isPlainDateTime = $obj->hasNativeSlot('[[IsPlainDateTime]]'); + $isPlainTime = $obj->hasNativeSlot('[[IsPlainTime]]'); + $isPlainYearMonth = $obj->hasNativeSlot('[[IsPlainYearMonth]]'); + $isPlainMonthDay = $obj->hasNativeSlot('[[IsPlainMonthDay]]'); + $isInstant = $obj->hasNativeSlot('[[EpochNanoseconds]]') && !$obj->hasNativeSlot('[[IsZonedDateTime]]'); $timeLetters = ['h', 'H', 'K', 'k', 'm', 's', 'S', 'a', 'B', 'z', 'Z', 'O', 'v', 'V', 'X', 'x']; $dateLetters = ['G', 'y', 'Y', 'u', 'r', 'M', 'L', 'd', 'D', 'F', 'g', 'E', 'e', 'c', 'w', 'W', 'Q', 'q', 'U']; $tzLetters = ['z', 'Z', 'O', 'v', 'V', 'X', 'x']; @@ -2305,7 +2305,7 @@ private static function temporalFormatterFor(JsObject $dtf, JsObject $obj): \Int */ private static function dtfHasExplicitOptions(JsObject $dtf): bool { - $defaulted = $dtf->get('[[ComponentsDefaulted]]'); + $defaulted = $dtf->getNativeSlot('[[ComponentsDefaulted]]'); if ($defaulted instanceof JsBoolean && $defaulted->value === true) { return false; } @@ -2534,22 +2534,22 @@ private static function checkTemporalOptionsCompat(JsObject $dtf, JsObject $obj) // When the formatter's components were defaulted (no user // options), it adapts to the Temporal type so any plain // type renders without throwing. - $defaulted = $dtf->get('[[ComponentsDefaulted]]'); + $defaulted = $dtf->getNativeSlot('[[ComponentsDefaulted]]'); if ($defaulted instanceof JsBoolean && $defaulted->value === true) { return; } - $hasTimeStyle = !$dtf->get('[[TimeStyle]]') instanceof JsUndefined; - $hasDateStyle = !$dtf->get('[[DateStyle]]') instanceof JsUndefined; - $hasYear = !$dtf->get('[[year]]') instanceof JsUndefined; - $hasMonth = !$dtf->get('[[month]]') instanceof JsUndefined; - $hasDay = !$dtf->get('[[day]]') instanceof JsUndefined; - $hasWeekday = !$dtf->get('[[weekday]]') instanceof JsUndefined; - $hasEra = !$dtf->get('[[era]]') instanceof JsUndefined; - $hasHour = !$dtf->get('[[hour]]') instanceof JsUndefined; - $hasMinute = !$dtf->get('[[minute]]') instanceof JsUndefined; - $hasSecond = !$dtf->get('[[second]]') instanceof JsUndefined; - $hasFractionalSec = !$dtf->get('[[fractionalSecondDigits]]') instanceof JsUndefined; - $hasDayPeriod = !$dtf->get('[[dayPeriod]]') instanceof JsUndefined; + $hasTimeStyle = !$dtf->getNativeSlot('[[TimeStyle]]') instanceof JsUndefined; + $hasDateStyle = !$dtf->getNativeSlot('[[DateStyle]]') instanceof JsUndefined; + $hasYear = !$dtf->getNativeSlot('[[year]]') instanceof JsUndefined; + $hasMonth = !$dtf->getNativeSlot('[[month]]') instanceof JsUndefined; + $hasDay = !$dtf->getNativeSlot('[[day]]') instanceof JsUndefined; + $hasWeekday = !$dtf->getNativeSlot('[[weekday]]') instanceof JsUndefined; + $hasEra = !$dtf->getNativeSlot('[[era]]') instanceof JsUndefined; + $hasHour = !$dtf->getNativeSlot('[[hour]]') instanceof JsUndefined; + $hasMinute = !$dtf->getNativeSlot('[[minute]]') instanceof JsUndefined; + $hasSecond = !$dtf->getNativeSlot('[[second]]') instanceof JsUndefined; + $hasFractionalSec = !$dtf->getNativeSlot('[[fractionalSecondDigits]]') instanceof JsUndefined; + $hasDayPeriod = !$dtf->getNativeSlot('[[dayPeriod]]') instanceof JsUndefined; $hasAnyTime = $hasHour || $hasMinute || $hasSecond || $hasFractionalSec || $hasDayPeriod || $hasTimeStyle; $hasAnyDate = $hasYear || $hasMonth || $hasDay || $hasWeekday @@ -2558,19 +2558,19 @@ private static function checkTemporalOptionsCompat(JsObject $dtf, JsObject $obj) // requested fields and the Temporal type's data model is // empty. The type can still render with extra-but-irrelevant // formatter fields as long as at least one field overlaps. - if ($obj->has('[[IsPlainDate]]')) { + if ($obj->hasNativeSlot('[[IsPlainDate]]')) { // PlainDate carries year/month/day/weekday/era. if (!$hasAnyDate) { throw new TypeError('Temporal.PlainDate has no overlap with formatter options'); } } - if ($obj->has('[[IsPlainTime]]')) { + if ($obj->hasNativeSlot('[[IsPlainTime]]')) { // PlainTime carries hour/minute/second/fractionalSec/dayPeriod. if (!$hasAnyTime) { throw new TypeError('Temporal.PlainTime has no overlap with formatter options'); } } - if ($obj->has('[[IsPlainYearMonth]]')) { + if ($obj->hasNativeSlot('[[IsPlainYearMonth]]')) { // PlainYearMonth carries year/month/era. // Reject formatters that ONLY ask for day/weekday or any // time field with no year/month/era overlap. dateStyle is @@ -2580,7 +2580,7 @@ private static function checkTemporalOptionsCompat(JsObject $dtf, JsObject $obj) throw new TypeError('Temporal.PlainYearMonth has no overlap with formatter options'); } } - if ($obj->has('[[IsPlainMonthDay]]')) { + if ($obj->hasNativeSlot('[[IsPlainMonthDay]]')) { // PlainMonthDay carries month/day. $hasMonthDay = $hasMonth || $hasDay || $hasDateStyle; if (!$hasMonthDay) { @@ -2782,8 +2782,8 @@ private static function dateTimeFormatterFor(JsObject $dtf): \IntlDateFormatter if (preg_match('/^[+-]\d{2}:\d{2}$/', $tz) === 1) { $tz = 'GMT' . $tz; } - $dateStyle = $dtf->get('[[DateStyle]]'); - $timeStyle = $dtf->get('[[TimeStyle]]'); + $dateStyle = $dtf->getNativeSlot('[[DateStyle]]'); + $timeStyle = $dtf->getNativeSlot('[[TimeStyle]]'); $dateStyle = $dateStyle instanceof JsString ? $dateStyle->value : null; $timeStyle = $timeStyle instanceof JsString ? $timeStyle->value : null; $mapStyle = static function (?string $s): int { @@ -2816,9 +2816,9 @@ private static function dateTimeFormatterFor(JsObject $dtf): \IntlDateFormatter // the locale's CLDR-derived time pattern. Without this the // user's hour-cycle choice silently drops when timeStyle // is set. - $hourCycleVal = $dtf->get('[[HourCycle]]'); + $hourCycleVal = $dtf->getNativeSlot('[[HourCycle]]'); $hourCycle = $hourCycleVal instanceof JsString ? $hourCycleVal->value : null; - $hourCycleSource = $dtf->get('[[HourCycleSource]]'); + $hourCycleSource = $dtf->getNativeSlot('[[HourCycleSource]]'); $isExplicit = $hourCycleSource instanceof JsString && $hourCycleSource->value === 'option'; if ($isExplicit && $hourCycle !== null && $timeStyle !== null) { @@ -3049,7 +3049,7 @@ private static function dateTimeSkeletonFromOptions(JsObject $dtf): string if ($v = $get('[[second]]')) { $sk .= $v === '2-digit' ? 'ss' : 's'; } - $fsdVal = $dtf->get('[[fractionalSecondDigits]]'); + $fsdVal = $dtf->getNativeSlot('[[fractionalSecondDigits]]'); if ($fsdVal instanceof JsNumber) { $n = (int) $fsdVal->value; if ($n >= 1 && $n <= 3) { diff --git a/src/BuiltIn/Intl/DisplayNamesSection.php b/src/BuiltIn/Intl/DisplayNamesSection.php index 8567fdff..dda8bbfb 100644 --- a/src/BuiltIn/Intl/DisplayNamesSection.php +++ b/src/BuiltIn/Intl/DisplayNamesSection.php @@ -39,7 +39,7 @@ private static function installDisplayNames(JsObject $intl): void $constructor = JsFunction::fromCallable( 'DisplayNames', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.DisplayNames requires \'new\''); } @@ -158,7 +158,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $of = JsFunction::fromCallable('of', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedDisplayNames]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDisplayNames]]') instanceof JsUndefined ) { throw new TypeError('Intl.DisplayNames.prototype.of called on non-DisplayNames'); } @@ -294,7 +294,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $resolvedOptions = JsFunction::fromCallable('resolvedOptions', function (JsValue $this_): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedDisplayNames]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDisplayNames]]') instanceof JsUndefined ) { throw new TypeError('Intl.DisplayNames.prototype.resolvedOptions called on non-DisplayNames'); } diff --git a/src/BuiltIn/Intl/DurationFormatSection.php b/src/BuiltIn/Intl/DurationFormatSection.php index 7cb2e587..e264c3f6 100644 --- a/src/BuiltIn/Intl/DurationFormatSection.php +++ b/src/BuiltIn/Intl/DurationFormatSection.php @@ -42,7 +42,7 @@ private static function installDurationFormat(JsObject $intl): void function (JsValue $this_, array $args) use ($proto): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[NewTarget]]') instanceof JsUndefined + || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined ) { throw new TypeError('Constructor Intl.DurationFormat requires \'new\''); } @@ -239,7 +239,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if ( !$this_ instanceof JsObject || $this_ instanceof \Phasis\Value\JsProxy - || $this_->get('[[InitializedDurationFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDurationFormat]]') instanceof JsUndefined ) { throw new TypeError('Intl.DurationFormat.prototype.format called on non-DurationFormat'); } @@ -260,7 +260,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if ( !$this_ instanceof JsObject || $this_ instanceof \Phasis\Value\JsProxy - || $this_->get('[[InitializedDurationFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDurationFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.DurationFormat.prototype.formatToParts called on non-DurationFormat', @@ -284,7 +284,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if ( !$this_ instanceof JsObject || $this_ instanceof \Phasis\Value\JsProxy - || $this_->get('[[InitializedDurationFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedDurationFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.DurationFormat.prototype.resolvedOptions called on non-DurationFormat', @@ -315,7 +315,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { self::extractInternalString($this_, $displaySlot, 'auto'), )); } - $fdVal = $this_->get('[[FractionalDigits]]'); + $fdVal = $this_->getNativeSlot('[[FractionalDigits]]'); if ($fdVal instanceof JsNumber) { self::defineDataProp($result, 'fractionalDigits', $fdVal); } @@ -543,7 +543,7 @@ private static function emitClockParts( $seconds += intdiv($fracTotalNs, 1000000000); $fracTotalNs %= 1000000000; } - $fdVal = $df->get('[[FractionalDigits]]'); + $fdVal = $df->getNativeSlot('[[FractionalDigits]]'); $fdLimit = $fdVal instanceof JsNumber ? (int) $fdVal->value : null; $fracDigits = ''; if ($fdLimit === null) { @@ -719,7 +719,7 @@ private static function durationToValues(JsObject $df, JsValue $duration): array // Per spec ToDurationRecord: when input is a Temporal.Duration, // bypass the prototype getters and read the internal slots // directly. Detected via the [[IsDuration]] brand. - $isTemporalDuration = $duration->has('[[IsDuration]]'); + $isTemporalDuration = $duration->hasNativeSlot('[[IsDuration]]'); $readUnit = static function (string $u) use ($duration, $isTemporalDuration): JsValue { if ($isTemporalDuration) { return $duration->get('[[' . $u . ']]'); @@ -843,7 +843,7 @@ private static function durationFormatRender(JsObject $df, JsValue $duration): s // ToDurationRecord: when input is a Temporal.Duration, bypass // the prototype's getters and read internal slots directly so // userland prototype tampering can't observe the read. - $isTemporalDuration = $duration->has('[[IsDuration]]'); + $isTemporalDuration = $duration->hasNativeSlot('[[IsDuration]]'); $readUnit = static function (string $u) use ($duration, $isTemporalDuration): JsValue { if ($isTemporalDuration) { return $duration->get('[[' . $u . ']]'); @@ -1184,7 +1184,7 @@ private static function durationAbsorberDecimal(JsObject $df, array $values, str $rPadded = str_pad((string) $sub, $exponent, '0', STR_PAD_LEFT); } // Apply [[FractionalDigits]] override or default {0..9, trunc}. - $fdSlot = $df->get('[[FractionalDigits]]'); + $fdSlot = $df->getNativeSlot('[[FractionalDigits]]'); $fdLimit = $fdSlot instanceof JsNumber ? (int) $fdSlot->value : null; if ($fdLimit !== null) { // Pad to fdLimit with zeros, then truncate to that count. @@ -1370,7 +1370,7 @@ private static function renderClockSegment( } $fracTotalNsStr = (string) $tot; } - $fdVal = $df->get('[[FractionalDigits]]'); + $fdVal = $df->getNativeSlot('[[FractionalDigits]]'); $fdLimit = $fdVal instanceof JsNumber ? (int) $fdVal->value : null; $fracStr = ''; $padded = str_pad($fracTotalNsStr, 9, '0', STR_PAD_LEFT); diff --git a/src/BuiltIn/Intl/ListFormatSection.php b/src/BuiltIn/Intl/ListFormatSection.php index 45bfbea7..297539c6 100644 --- a/src/BuiltIn/Intl/ListFormatSection.php +++ b/src/BuiltIn/Intl/ListFormatSection.php @@ -206,7 +206,7 @@ private static function installListFormat(JsObject $intl): void $constructor = JsFunction::fromCallable( 'ListFormat', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.ListFormat requires \'new\''); } @@ -284,7 +284,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $format = JsFunction::fromCallable('format', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedListFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedListFormat]]') instanceof JsUndefined ) { throw new TypeError('Intl.ListFormat.prototype.format called on non-ListFormat'); } @@ -300,7 +300,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $formatToParts = JsFunction::fromCallable('formatToParts', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedListFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedListFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.ListFormat.prototype.formatToParts called on non-ListFormat' @@ -358,7 +358,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $resolvedOptions = JsFunction::fromCallable('resolvedOptions', function (JsValue $this_): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedListFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedListFormat]]') instanceof JsUndefined ) { throw new TypeError('Intl.ListFormat.prototype.resolvedOptions called on non-ListFormat'); } diff --git a/src/BuiltIn/Intl/LocaleSection.php b/src/BuiltIn/Intl/LocaleSection.php index 8f704633..8e03bc82 100644 --- a/src/BuiltIn/Intl/LocaleSection.php +++ b/src/BuiltIn/Intl/LocaleSection.php @@ -39,7 +39,7 @@ private static function installLocale(JsObject $intl): void $constructor = JsFunction::fromCallable( 'Locale', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.Locale requires \'new\''); } @@ -321,7 +321,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { // Locale.prototype.toString() $toString = JsFunction::fromCallable('toString', function (JsValue $this_): JsValue { if ($this_ instanceof JsObject && self::isInitializedLocale($this_)) { - $tag = $this_->get('[[LocaleTag]]'); + $tag = $this_->getNativeSlot('[[LocaleTag]]'); if (!$tag instanceof JsUndefined) { return $tag; } @@ -347,7 +347,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if (!$this_ instanceof JsObject || !self::isInitializedLocale($this_)) { throw new TypeError("Intl.Locale.prototype.{$internalKey} called on non-Locale"); } - $val = $this_->get("[[{$internalKey}]]"); + $val = $this_->getNativeSlot("[[{$internalKey}]]"); return $val instanceof JsUndefined ? JsUndefined::instance() : $val; }, 0); $proto->defineOwnProperty($prop, PropertyDescriptor::accessor( @@ -362,7 +362,7 @@ enumerable: false, if (!$this_ instanceof JsObject || !self::isInitializedLocale($this_)) { throw new TypeError('Intl.Locale.prototype.variants called on non-Locale'); } - $variants = $this_->get('[[variants]]'); + $variants = $this_->getNativeSlot('[[variants]]'); if (!$variants instanceof JsArray || $variants->getLength() === 0) { return JsUndefined::instance(); } @@ -384,7 +384,7 @@ enumerable: false, if (!$this_ instanceof JsObject || !self::isInitializedLocale($this_)) { throw new TypeError('Intl.Locale.prototype.numeric called on non-Locale'); } - $val = $this_->get('[[numeric]]'); + $val = $this_->getNativeSlot('[[numeric]]'); if ($val instanceof JsBoolean) { return $val; } @@ -405,7 +405,7 @@ enumerable: false, $lang = self::extractInternalString($this_, '[[language]]', ''); $script = self::extractInternalStringOrNull($this_, '[[script]]'); $region = self::extractInternalStringOrNull($this_, '[[region]]'); - $variantsVal = $this_->get('[[variants]]'); + $variantsVal = $this_->getNativeSlot('[[variants]]'); $parts = [$lang]; if ($script !== null) { @@ -477,7 +477,7 @@ enumerable: false, if (!$this_ instanceof JsObject || !self::isInitializedLocale($this_)) { throw new TypeError("Intl.Locale.prototype.{$name} called on non-Locale"); } - $preferred = $this_->get("[[{$slot}]]"); + $preferred = $this_->getNativeSlot("[[{$slot}]]"); $locale = self::extractInternalString($this_, '[[LocaleTag]]', 'und'); $language = self::extractInternalString($this_, '[[language]]', 'und'); $values = $preferred instanceof JsString ? [$preferred->value] : match ($name) { @@ -529,7 +529,7 @@ enumerable: false, $locale = self::extractInternalString($this_, '[[LocaleTag]]', 'und'); $week = LocaleInfoData::week(self::localePreferredRegions($locale)); $firstDay = $week['firstDay']; - $fwSlot = $this_->get('[[firstDayOfWeek]]'); + $fwSlot = $this_->getNativeSlot('[[firstDayOfWeek]]'); if ($fwSlot instanceof JsString) { static $weekdayIndex = [ 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, diff --git a/src/BuiltIn/Intl/NumberFormatSection.php b/src/BuiltIn/Intl/NumberFormatSection.php index 6e56da50..af278c73 100644 --- a/src/BuiltIn/Intl/NumberFormatSection.php +++ b/src/BuiltIn/Intl/NumberFormatSection.php @@ -722,7 +722,7 @@ enumerable: false, ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedNumberFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedNumberFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.NumberFormat.prototype.formatToParts called on non-NumberFormat', @@ -754,7 +754,7 @@ enumerable: false, ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedNumberFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedNumberFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.NumberFormat.prototype.formatRange called on non-NumberFormat', @@ -839,7 +839,7 @@ enumerable: false, ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedNumberFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedNumberFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.NumberFormat.prototype.formatRangeToParts called on non-NumberFormat', @@ -2383,10 +2383,10 @@ private static function formatNumberWithPriority( ?string $bigIntStr, string $priority, ): string { - $hasMaxSig = $nf->get('[[HasMaxSig]]') instanceof JsBoolean - && $nf->get('[[HasMaxSig]]')->toBoolean(); - $hasMaxFrac = $nf->get('[[HasMaxFrac]]') instanceof JsBoolean - && $nf->get('[[HasMaxFrac]]')->toBoolean(); + $hasMaxSig = $nf->getNativeSlot('[[HasMaxSig]]') instanceof JsBoolean + && $nf->getNativeSlot('[[HasMaxSig]]')->toBoolean(); + $hasMaxFrac = $nf->getNativeSlot('[[HasMaxFrac]]') instanceof JsBoolean + && $nf->getNativeSlot('[[HasMaxFrac]]')->toBoolean(); $sigClone = self::cloneNumberFormatWithRoundingType($nf, 'significantDigits'); $fracClone = self::cloneNumberFormatWithRoundingType($nf, 'fractionDigits'); $sigStr = self::formatNumber($sigClone, $number, $bigIntStr); @@ -2400,9 +2400,9 @@ private static function formatNumberWithPriority( // sigMag = e - (maxSd - 1), where e = floor(log10(|x|)) // fracMag = -maxFd // morePrecision keeps the smaller (more negative) magnitude. - $maxSdSlot = $nf->get('[[MaximumSignificantDigits]]'); + $maxSdSlot = $nf->getNativeSlot('[[MaximumSignificantDigits]]'); $maxSd = $maxSdSlot instanceof JsNumber ? (int) $maxSdSlot->value : 21; - $maxFdSlot = $nf->get('[[MaximumFractionDigits]]'); + $maxFdSlot = $nf->getNativeSlot('[[MaximumFractionDigits]]'); $maxFd = $maxFdSlot instanceof JsNumber ? (int) $maxFdSlot->value : 0; $absVal = abs($number); if ($absVal === 0.0 || !is_finite($absVal)) { diff --git a/src/BuiltIn/Intl/PluralRulesSection.php b/src/BuiltIn/Intl/PluralRulesSection.php index 37842b6f..2cfc3a45 100644 --- a/src/BuiltIn/Intl/PluralRulesSection.php +++ b/src/BuiltIn/Intl/PluralRulesSection.php @@ -106,7 +106,7 @@ private static function installPluralRules(JsObject $intl): void $constructor = JsFunction::fromCallable( 'PluralRules', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.PluralRules requires \'new\''); } @@ -329,7 +329,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedPluralRules]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedPluralRules]]') instanceof JsUndefined ) { throw new TypeError('Intl.PluralRules.prototype.select called on non-PluralRules'); } @@ -372,7 +372,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedPluralRules]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedPluralRules]]') instanceof JsUndefined ) { throw new TypeError('Intl.PluralRules.prototype.selectRange called on non-PluralRules'); } @@ -401,7 +401,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedPluralRules]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedPluralRules]]') instanceof JsUndefined ) { throw new TypeError('Intl.PluralRules.prototype.resolvedOptions called on non-PluralRules'); } @@ -415,7 +415,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { self::defineDataProp($result, 'notation', new JsString( self::extractInternalString($this_, '[[Notation]]', 'standard'), )); - $compactDisplay = $this_->get('[[CompactDisplay]]'); + $compactDisplay = $this_->getNativeSlot('[[CompactDisplay]]'); if ($compactDisplay instanceof JsString) { self::defineDataProp($result, 'compactDisplay', $compactDisplay); } diff --git a/src/BuiltIn/Intl/RelativeTimeFormatSection.php b/src/BuiltIn/Intl/RelativeTimeFormatSection.php index b73e9f34..4ae8fbeb 100644 --- a/src/BuiltIn/Intl/RelativeTimeFormatSection.php +++ b/src/BuiltIn/Intl/RelativeTimeFormatSection.php @@ -262,7 +262,7 @@ private static function installRelativeTimeFormat(JsObject $intl): void $constructor = JsFunction::fromCallable( 'RelativeTimeFormat', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.RelativeTimeFormat requires \'new\''); } @@ -368,7 +368,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $format = JsFunction::fromCallable('format', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.RelativeTimeFormat.prototype.format called on non-RelativeTimeFormat' @@ -394,7 +394,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $formatToParts = JsFunction::fromCallable('formatToParts', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.RelativeTimeFormat.prototype.formatToParts called on non-RelativeTimeFormat' @@ -523,7 +523,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $resolvedOptions = JsFunction::fromCallable('resolvedOptions', function (JsValue $this_): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedRelativeTimeFormat]]') instanceof JsUndefined ) { throw new TypeError('Intl.RelativeTimeFormat.prototype.resolvedOptions called on non-RelativeTimeFormat'); } diff --git a/src/BuiltIn/Intl/SegmenterSection.php b/src/BuiltIn/Intl/SegmenterSection.php index 3edc4436..b6557602 100644 --- a/src/BuiltIn/Intl/SegmenterSection.php +++ b/src/BuiltIn/Intl/SegmenterSection.php @@ -39,7 +39,7 @@ private static function installSegmenter(JsObject $intl): void $constructor = JsFunction::fromCallable( 'Segmenter', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Intl.Segmenter requires \'new\''); } @@ -100,7 +100,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $segment = JsFunction::fromCallable('segment', function (JsValue $this_, array $args): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedSegmenter]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedSegmenter]]') instanceof JsUndefined ) { throw new TypeError('Intl.Segmenter.prototype.segment called on non-Segmenter'); } @@ -137,7 +137,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { ): JsValue { if ( !$this2_ instanceof JsObject - || $this2_->get('[[String]]') instanceof JsUndefined + || $this2_->getNativeSlot('[[String]]') instanceof JsUndefined ) { throw new TypeError( 'Intl.Segmenter%segments%.prototype.containing called on incompatible receiver', @@ -305,7 +305,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { $resolvedOptions = JsFunction::fromCallable('resolvedOptions', function (JsValue $this_): JsValue { if ( !$this_ instanceof JsObject - || $this_->get('[[InitializedSegmenter]]') instanceof JsUndefined + || $this_->getNativeSlot('[[InitializedSegmenter]]') instanceof JsUndefined ) { throw new TypeError('Intl.Segmenter.prototype.resolvedOptions called on non-Segmenter'); } diff --git a/src/BuiltIn/Intl/TemporalCalendarFormattingHelpers.php b/src/BuiltIn/Intl/TemporalCalendarFormattingHelpers.php index 5e82f918..93c5202c 100644 --- a/src/BuiltIn/Intl/TemporalCalendarFormattingHelpers.php +++ b/src/BuiltIn/Intl/TemporalCalendarFormattingHelpers.php @@ -254,7 +254,7 @@ private static function calendarFormattingDateTime(?JsObject $temporal, float $t if ($temporal === null) { return self::dateTimeFromTimestampMs($timestampMs); } - if ($temporal->has('[[EpochNanoseconds]]')) { + if ($temporal->hasNativeSlot('[[EpochNanoseconds]]')) { return self::dateTimeFromTimestampMs(self::epochNsToMs( self::extractInternalString($temporal, '[[EpochNanoseconds]]', '0'), )); diff --git a/src/BuiltIn/IntlObject.php b/src/BuiltIn/IntlObject.php index cf0a0038..fded1f13 100644 --- a/src/BuiltIn/IntlObject.php +++ b/src/BuiltIn/IntlObject.php @@ -182,8 +182,8 @@ public static function canonicalizeLocaleList(JsValue $locales): array */ private static function isInitializedLocale(JsObject $obj): bool { - return !$obj->get('[[LocaleTag]]') instanceof JsUndefined - || !$obj->get('[[language]]') instanceof JsUndefined; + return !$obj->getNativeSlot('[[LocaleTag]]') instanceof JsUndefined + || !$obj->getNativeSlot('[[language]]') instanceof JsUndefined; } /** @@ -401,7 +401,7 @@ private static function chainLegacyIntlConstructor( ): JsObject { if ( $receiver instanceof JsObject - && $receiver->get('[[NewTarget]]') instanceof JsUndefined + && $receiver->getNativeSlot('[[NewTarget]]') instanceof JsUndefined && self::hasIntlPrototype($receiver, $prototype) ) { if ( @@ -468,13 +468,13 @@ private static function instanceFromConstructor( ): JsObject { if ( $this_ instanceof JsObject - && !$this_->get('[[NewTarget]]') instanceof JsUndefined + && !$this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined ) { // Per spec OrdinaryCreateFromConstructor: GetPrototypeFromConstructor // is called on NewTarget, which Gets the "prototype" property. A // poisoned getter on NewTarget.prototype must surface here, before // any option validation in the constructor body. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $resolved = self::resolveProtoFromCtor($newTarget, $intrinsicName); if ($resolved !== null) { diff --git a/src/BuiltIn/IteratorConstructor.php b/src/BuiltIn/IteratorConstructor.php index caeb14d1..6085cde4 100644 --- a/src/BuiltIn/IteratorConstructor.php +++ b/src/BuiltIn/IteratorConstructor.php @@ -39,10 +39,10 @@ public static function install(Environment $env): void $iteratorCtor = JsFunction::fromCallable( 'Iterator', function (JsValue $this_, array $args) use (&$iteratorCtor): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Iterator is not callable'); } - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value === $iteratorCtor) { throw new TypeError('Abstract class Iterator not directly constructable'); } @@ -106,7 +106,7 @@ function (JsValue $this_, array $args): JsValue { if (!$this_ instanceof JsObject) { throw new TypeError('Iterator helper next called on non-object'); } - $handler = $this_->getOwnPropertyDescriptor('[[NextHandler]]'); + $handler = $this_->getNativeSlotDescriptor('[[NextHandler]]'); if ($handler === null || !$handler->value instanceof JsFunction) { throw new TypeError('Method called on incompatible Iterator helper'); } @@ -126,7 +126,7 @@ function (JsValue $this_, array $args): JsValue { if (!$this_ instanceof JsObject) { throw new TypeError('Iterator helper return called on non-object'); } - $handler = $this_->getOwnPropertyDescriptor('[[ReturnHandler]]'); + $handler = $this_->getNativeSlotDescriptor('[[ReturnHandler]]'); if ($handler === null || !$handler->value instanceof JsFunction) { throw new TypeError('Method called on incompatible Iterator helper'); } @@ -997,13 +997,13 @@ private static function createWrapForValidIterator(JsObject $iterator): JsObject $nextFn = JsFunction::fromCallable('next', function (JsValue $this_, array $args) use ($nextMethod): JsValue { // Validate `this` has [[WrapForValidIterator]] (set on creation). - if (!$this_ instanceof JsObject || $this_->getOwnPropertyDescriptor('[[WrapForValidIterator]]') === null) { + if (!$this_ instanceof JsObject || $this_->getNativeSlotDescriptor('[[WrapForValidIterator]]') === null) { throw new TypeError('Iterator wrap method called on incompatible receiver'); } if (!$nextMethod instanceof JsFunction) { throw new TypeError('Iterator next is not a function'); } - $iter = $this_->getOwnPropertyDescriptor('[[WrapForValidIterator]]')->value; + $iter = $this_->getNativeSlotDescriptor('[[WrapForValidIterator]]')->value; // Spec WrapForValidIteratorPrototype.next: invoke the inner next // method with no arguments; the wrapper's caller-supplied args // are discarded ("next argument is ignored" semantic). The spec @@ -1013,10 +1013,10 @@ private static function createWrapForValidIterator(JsObject $iterator): JsObject }, 0); $returnFn = JsFunction::fromCallable('return', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || $this_->getOwnPropertyDescriptor('[[WrapForValidIterator]]') === null) { + if (!$this_ instanceof JsObject || $this_->getNativeSlotDescriptor('[[WrapForValidIterator]]') === null) { throw new TypeError('Iterator wrap method called on incompatible receiver'); } - $iter = $this_->getOwnPropertyDescriptor('[[WrapForValidIterator]]')->value; + $iter = $this_->getNativeSlotDescriptor('[[WrapForValidIterator]]')->value; if (!$iter instanceof JsObject) { return self::iterResult(JsUndefined::instance(), true); } diff --git a/src/BuiltIn/JsonObject.php b/src/BuiltIn/JsonObject.php index d2a83ea6..96ea6cf2 100644 --- a/src/BuiltIn/JsonObject.php +++ b/src/BuiltIn/JsonObject.php @@ -250,7 +250,7 @@ private static function trivialJsonValue(JsValue $value, array &$stack, array &$ // Boolean / Number / String / BigInt) since spec ToJSON // unwraps these to their primitives, while our walker would // emit them as bare {} objects. - if (!$value instanceof \Phasis\Value\JsDataObject && $value->getOwnPropertyDescriptor('[[PrimitiveValue]]') !== null) { + if (!$value instanceof \Phasis\Value\JsDataObject && $value->getNativeSlotDescriptor('[[PrimitiveValue]]') !== null) { return self::trivialBailout(); } // RawJSON object: bail so the spec serialiser emits its raw @@ -395,38 +395,38 @@ private static function arrayLikeLength(JsObject $value): int private static function hasNumberData(JsObject $obj): bool { - if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->hasNativeSlot('[[PrimitiveValue]]')) { return false; } - return $obj->get('[[PrimitiveValue]]') instanceof JsNumber; + return $obj->getNativeSlot('[[PrimitiveValue]]') instanceof JsNumber; } private static function hasStringData(JsObject $obj): bool { - if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->hasNativeSlot('[[PrimitiveValue]]')) { return false; } - return $obj->get('[[PrimitiveValue]]') instanceof JsString; + return $obj->getNativeSlot('[[PrimitiveValue]]') instanceof JsString; } private static function hasBooleanData(JsObject $obj): bool { - if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->hasNativeSlot('[[PrimitiveValue]]')) { return false; } - return $obj->get('[[PrimitiveValue]]') instanceof JsBoolean; + return $obj->getNativeSlot('[[PrimitiveValue]]') instanceof JsBoolean; } private static function hasBigIntData(JsObject $obj): bool { - if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->has('[[PrimitiveValue]]')) { + if ($obj instanceof \Phasis\Value\JsDataObject || !$obj->hasNativeSlot('[[PrimitiveValue]]')) { return false; } - return $obj->get('[[PrimitiveValue]]') instanceof \Phasis\Value\JsBigInt; + return $obj->getNativeSlot('[[PrimitiveValue]]') instanceof \Phasis\Value\JsBigInt; } /** @@ -1107,7 +1107,7 @@ private static function serializeProperty( } elseif (self::hasStringData($value)) { $value = new JsString(TypeConversion::toString($value)); } elseif (self::hasBooleanData($value)) { - $prim = $value->get('[[PrimitiveValue]]'); + $prim = $value->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsBoolean) { $value = $prim; } diff --git a/src/BuiltIn/MapConstructor.php b/src/BuiltIn/MapConstructor.php index 6c56a81f..0ef3a4a6 100644 --- a/src/BuiltIn/MapConstructor.php +++ b/src/BuiltIn/MapConstructor.php @@ -46,12 +46,12 @@ public static function install(Environment $env): void 'Map', function (JsValue $this_, array $args) use ($proto): JsValue { // Map must be called with new - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Map requires \'new\''); } // Use the subclass's prototype when new.target is not Map itself. $effectiveProto = $proto; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof JsObject) { @@ -116,7 +116,7 @@ public static function getMapIteratorPrototype(?JsObject $iteratorPrototype = nu 'Method Map Iterator.prototype.next called on incompatible receiver', ); } - $slotDesc = $this_->getOwnPropertyDescriptor('[[MapIteratorData]]'); + $slotDesc = $this_->getNativeSlotDescriptor('[[MapIteratorData]]'); if ($slotDesc === null) { throw new TypeError( 'Method Map Iterator.prototype.next called on incompatible receiver', diff --git a/src/BuiltIn/NumberConstructor.php b/src/BuiltIn/NumberConstructor.php index c742d7a3..a01a222d 100644 --- a/src/BuiltIn/NumberConstructor.php +++ b/src/BuiltIn/NumberConstructor.php @@ -439,7 +439,7 @@ private static function valueOf(): \Closure return $this_; } if ($this_ instanceof JsObject) { - $prim = $this_->get('[[PrimitiveValue]]'); + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsNumber) { return $prim; } @@ -742,7 +742,7 @@ private static function extractNumberValue(JsValue $this_): float return $this_->value; } if ($this_ instanceof JsObject) { - $prim = $this_->get('[[PrimitiveValue]]'); + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsNumber) { return $prim->value; } diff --git a/src/BuiltIn/ObjectConstructor.php b/src/BuiltIn/ObjectConstructor.php index cf604c74..6c0175f3 100644 --- a/src/BuiltIn/ObjectConstructor.php +++ b/src/BuiltIn/ObjectConstructor.php @@ -40,21 +40,21 @@ function (JsValue $this_, array $args) use ($proto, $env, &$constructor): JsValu // When our interpreter sets up the new-call, $this_ is already an empty // object; set its prototype from newTarget so subclass construction // (e.g. `class O extends Object {}`) observes O.prototype. - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); $this_->setPrototype( $ntProto instanceof JsObject ? $ntProto : $proto, ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget !== $constructor) { return $this_; } } if (empty($args) || $args[0] instanceof JsUndefined || $args[0] instanceof JsNull) { - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { return $this_; } $obj = new JsObject($proto); @@ -350,18 +350,18 @@ function (JsValue $this_): JsValue { $builtinTag = $primitiveTag; } elseif (self::isArrayForToString($o)) { $builtinTag = 'Array'; - } elseif (!$isProxy && $o->hasOwnProperty('[[IsArguments]]')) { + } elseif (!$isProxy && $o->hasNativeSlot('[[IsArguments]]', own: true)) { $builtinTag = 'Arguments'; } elseif (self::isCallableForToString($o)) { $builtinTag = 'Function'; - } elseif (!$isProxy && $o->hasOwnProperty('[[ErrorData]]')) { + } elseif (!$isProxy && $o->hasNativeSlot('[[ErrorData]]', own: true)) { $builtinTag = 'Error'; - } elseif (!$isProxy && $o->hasOwnProperty('[[PrimitiveValue]]')) { + } elseif (!$isProxy && $o->hasNativeSlot('[[PrimitiveValue]]', own: true)) { // Spec internal-slot checks are own-only. Object.create on // a Boolean/Number/String wrapper does not inherit // [[BooleanData]]/[[NumberData]]/[[StringData]] from its // prototype, so the resulting object's tag is "Object". - $primDesc = $o->getOwnPropertyDescriptor('[[PrimitiveValue]]'); + $primDesc = $o->getNativeSlotDescriptor('[[PrimitiveValue]]'); $prim = $primDesc?->value; if ($prim instanceof JsBoolean) { $builtinTag = 'Boolean'; @@ -375,11 +375,11 @@ function (JsValue $this_): JsValue { } elseif ( !$isProxy && ( $o->getInternalProperty('[[IsDate]]') === true - || $o->hasOwnProperty('[[IsDate]]') + || $o->hasNativeSlot('[[IsDate]]', own: true) ) ) { $builtinTag = 'Date'; - } elseif (!$isProxy && $o->hasOwnProperty('[[PCREPattern]]')) { + } elseif (!$isProxy && $o->hasNativeSlot('[[PCREPattern]]', own: true)) { $builtinTag = 'RegExp'; } else { $builtinTag = 'Object'; diff --git a/src/BuiltIn/PromiseConstructor.php b/src/BuiltIn/PromiseConstructor.php index 5fdebc3a..ca4758ab 100644 --- a/src/BuiltIn/PromiseConstructor.php +++ b/src/BuiltIn/PromiseConstructor.php @@ -50,7 +50,7 @@ public static function install(Environment $env): void 'Promise', function (JsValue $this_, array $args) use ($proto): JsValue { // Promise must be called with new (§25.6.3.1 step 1) - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Promise constructor cannot be invoked without \'new\''); } $executor = $args[0] ?? JsUndefined::instance(); @@ -59,7 +59,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } // Per spec OrdinaryCreateFromConstructor: use NewTarget's prototype. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ctorProto = $newTarget->get('prototype'); $useProto = $ctorProto instanceof JsObject ? $ctorProto : $proto; diff --git a/src/BuiltIn/ProxyConstructor.php b/src/BuiltIn/ProxyConstructor.php index 740cc683..973a0419 100644 --- a/src/BuiltIn/ProxyConstructor.php +++ b/src/BuiltIn/ProxyConstructor.php @@ -36,7 +36,7 @@ function (JsValue $this_, array $args): JsValue { // called via `new`. If that marker is absent, it was a direct call. if ( !$this_ instanceof JsObject - || !$this_->hasOwnProperty('[[NewTarget]]') + || !$this_->hasNativeSlot('[[NewTarget]]', own: true) ) { throw new TypeError('Constructor Proxy requires \'new\''); } diff --git a/src/BuiltIn/RegExpPrototype.php b/src/BuiltIn/RegExpPrototype.php index a0e6cd3f..8a820e6e 100644 --- a/src/BuiltIn/RegExpPrototype.php +++ b/src/BuiltIn/RegExpPrototype.php @@ -157,7 +157,7 @@ static function (JsValue $this_, array $args) use ($proto, $propName, $flagChar) // - Otherwise, if R is %RegExp.prototype% and propName // is a flag getter, return undefined. // - Otherwise throw TypeError. - $origFlagsDesc = $this_->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $origFlagsDesc = $this_->getNativeSlotDescriptor('[[OriginalFlags]]'); if ($origFlagsDesc === null) { if ($this_ === $proto) { if ($propName === 'source') { @@ -178,7 +178,7 @@ static function (JsValue $this_, array $args) use ($proto, $propName, $flagChar) // LineTerminator code points in the pattern must be escaped // so the resulting string can round-trip through // `/` + source + `/` + flags. - $srcDesc = $this_->getOwnPropertyDescriptor('[[OriginalSource]]'); + $srcDesc = $this_->getNativeSlotDescriptor('[[OriginalSource]]'); if ($srcDesc && $srcDesc->value instanceof JsString) { $src = $srcDesc->value->value; if ($src === '') { @@ -263,7 +263,7 @@ enumerable: false, ); } // Check for [[RegExpMatcher]] internal slot (we use [[PCREPattern]]). - $pcreDesc = $this_->getOwnPropertyDescriptor('[[PCREPattern]]'); + $pcreDesc = $this_->getNativeSlotDescriptor('[[PCREPattern]]'); if ($pcreDesc === null) { throw new \Phasis\Exceptions\TypeError( 'Method RegExp.prototype.compile called on incompatible receiver', @@ -297,7 +297,7 @@ enumerable: false, } // Per Annex B step 3: if [[LegacyFeaturesEnabled]] is false, throw TypeError. - $legacyDesc = $this_->getOwnPropertyDescriptor("[[LegacyFeaturesEnabled]]"); + $legacyDesc = $this_->getNativeSlotDescriptor("[[LegacyFeaturesEnabled]]"); if ( $legacyDesc !== null && $legacyDesc->value instanceof \Phasis\Value\JsBoolean && !$legacyDesc->value->toBoolean() @@ -313,7 +313,7 @@ enumerable: false, // If pattern is a RegExp object, flags must be undefined. $isRegExp = false; if ($patternArg instanceof JsObject) { - $patternPcre = $patternArg->getOwnPropertyDescriptor('[[PCREPattern]]'); + $patternPcre = $patternArg->getNativeSlotDescriptor('[[PCREPattern]]'); if ($patternPcre !== null) { $isRegExp = true; } @@ -328,8 +328,8 @@ enumerable: false, // Read from [[OriginalSource]]/[[OriginalFlags]] internal slots // to avoid triggering user-defined accessors on the instance. /** @var JsObject $patternArg */ - $srcDesc = $patternArg->getOwnPropertyDescriptor('[[OriginalSource]]'); - $flgDesc = $patternArg->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $srcDesc = $patternArg->getNativeSlotDescriptor('[[OriginalSource]]'); + $flgDesc = $patternArg->getNativeSlotDescriptor('[[OriginalFlags]]'); if ($srcDesc !== null && $srcDesc->value !== null) { $p = TypeConversion::toString($srcDesc->value); } else { diff --git a/src/BuiltIn/RegExp_/RegExpExec.php b/src/BuiltIn/RegExp_/RegExpExec.php index b57cacce..f476a7fa 100644 --- a/src/BuiltIn/RegExp_/RegExpExec.php +++ b/src/BuiltIn/RegExp_/RegExpExec.php @@ -141,7 +141,7 @@ private static function execMethod(): \Closure } // Step 1: confirm R has [[RegExpMatcher]] (we use [[PCREPattern]]). - if ($this_->getOwnPropertyDescriptor('[[PCREPattern]]') === null) { + if ($this_->getNativeSlotDescriptor('[[PCREPattern]]') === null) { throw new \Phasis\Exceptions\TypeError('RegExp.prototype.exec called on incompatible receiver'); } @@ -166,7 +166,7 @@ private static function execMethod(): \Closure // [[OriginalFlags]] internal slot — NOT the public getter. // User overrides like Object.defineProperty(r, 'global', ...) // must not influence whether lastIndex is updated. - $origFlagsDesc = $this_->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $origFlagsDesc = $this_->getNativeSlotDescriptor('[[OriginalFlags]]'); $origFlags = ($origFlagsDesc !== null && $origFlagsDesc->value instanceof JsString) ? $origFlagsDesc->value->value : ''; @@ -179,7 +179,7 @@ private static function execMethod(): \Closure // overwritten [[PCREPattern]] by this point. The slot is // guaranteed present by the early check above; only its // (possibly mutated) value type still needs validation. - $pcrePatternDesc = $this_->getOwnPropertyDescriptor('[[PCREPattern]]'); + $pcrePatternDesc = $this_->getNativeSlotDescriptor('[[PCREPattern]]'); $pcrePatternVal = $pcrePatternDesc->value; if (!$pcrePatternVal instanceof JsString) { throw new \Phasis\Exceptions\TypeError('RegExp.prototype.exec called on incompatible receiver'); @@ -208,8 +208,8 @@ private static function execMethod(): \Closure // RegExp compiler stashed a parsed AST and the matcher // decides the result. Falls back to PCRE2 if our matcher // returns null for a pattern PCRE2 would have matched. - $customAstDesc = $this_->getOwnPropertyDescriptor('[[CustomRegexAst]]'); - $customFlagsDesc = $this_->getOwnPropertyDescriptor('[[CustomRegexFlags]]'); + $customAstDesc = $this_->getNativeSlotDescriptor('[[CustomRegexAst]]'); + $customFlagsDesc = $this_->getNativeSlotDescriptor('[[CustomRegexFlags]]'); if ( $customAstDesc !== null && $customAstDesc->value instanceof \Phasis\Value\JsHostValue @@ -351,7 +351,7 @@ private static function execMethod(): \Closure $groups = JsObject::createNullPrototype(); $hasGroups = false; - $groupNameMapDesc = $this_->getOwnPropertyDescriptor('[[GroupNameMap]]'); + $groupNameMapDesc = $this_->getNativeSlotDescriptor('[[GroupNameMap]]'); $groupNameMap = ($groupNameMapDesc !== null && $groupNameMapDesc->value instanceof JsObject) ? $groupNameMapDesc->value : null; @@ -359,7 +359,7 @@ private static function execMethod(): \Closure // that names that did not participate in the match still // appear in source order (per duplicate-named-groups // proposal). - $namedListDesc = $this_->getOwnPropertyDescriptor('[[NamedGroupNames]]'); + $namedListDesc = $this_->getNativeSlotDescriptor('[[NamedGroupNames]]'); if ($namedListDesc !== null && $namedListDesc->value instanceof JsArray) { $hasGroups = true; $len = $namedListDesc->value->getLength(); @@ -575,7 +575,7 @@ private static function execCustomMatcher( // undefined, then overwrite with matched values. $groups = JsObject::createNullPrototype(); $hasGroups = false; - $namedListDesc = $this_->getOwnPropertyDescriptor('[[NamedGroupNames]]'); + $namedListDesc = $this_->getNativeSlotDescriptor('[[NamedGroupNames]]'); if ($namedListDesc !== null && $namedListDesc->value instanceof JsArray) { $hasGroups = true; $nlen = $namedListDesc->value->getLength(); @@ -697,7 +697,7 @@ private static function applyRepeatedGroupFixes( string $pcrePattern, ): array { // Get the original ES pattern from internal slot. - $srcDesc = $regexp->getOwnPropertyDescriptor('[[OriginalSource]]'); + $srcDesc = $regexp->getNativeSlotDescriptor('[[OriginalSource]]'); if ($srcDesc === null || !$srcDesc->value instanceof JsString) { return $matches; } @@ -718,7 +718,7 @@ private static function applyRepeatedGroupFixes( if ($interp === null) { return $matches; } - $flagsDesc = $regexp->getOwnPropertyDescriptor("[[OriginalFlags]]"); + $flagsDesc = $regexp->getNativeSlotDescriptor("[[OriginalFlags]]"); $esFlags = ($flagsDesc !== null && $flagsDesc->value instanceof JsString) ? $flagsDesc->value->value : ""; $transformFn = static function (string $esSubPattern) use ($interp, $esFlags): string { @@ -772,11 +772,11 @@ private static function regExpTestFast(JsObject $rx, string $S): ?bool ) { return null; } - $pcreDesc = $rx->getOwnPropertyDescriptor('[[PCREPattern]]'); + $pcreDesc = $rx->getNativeSlotDescriptor('[[PCREPattern]]'); if ($pcreDesc === null || !$pcreDesc->value instanceof JsString) { return null; } - $origFlagsDesc = $rx->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $origFlagsDesc = $rx->getNativeSlotDescriptor('[[OriginalFlags]]'); $origFlags = ($origFlagsDesc !== null && $origFlagsDesc->value instanceof JsString) ? $origFlagsDesc->value->value : ''; @@ -790,8 +790,8 @@ private static function regExpTestFast(JsObject $rx, string $S): ?bool } // Predicate matching retains the same legacy captures and contexts as // exec without allocating a JavaScript match-result array. - $customAstDesc = $rx->getOwnPropertyDescriptor('[[CustomRegexAst]]'); - $customFlagsDesc = $rx->getOwnPropertyDescriptor('[[CustomRegexFlags]]'); + $customAstDesc = $rx->getNativeSlotDescriptor('[[CustomRegexAst]]'); + $customFlagsDesc = $rx->getNativeSlotDescriptor('[[CustomRegexFlags]]'); if ( $customAstDesc !== null && $customAstDesc->value instanceof \Phasis\Value\JsHostValue @@ -891,7 +891,7 @@ private static function regExpExec(JsObject $rx, string $S): JsObject|JsNull ); } // Per spec 22.2.5.2.1 step 6: throw TypeError if R lacks [[RegExpMatcher]]. - $pcreDesc = $rx->getOwnPropertyDescriptor('[[PCREPattern]]'); + $pcreDesc = $rx->getNativeSlotDescriptor('[[PCREPattern]]'); if ($pcreDesc === null) { throw new \Phasis\Exceptions\TypeError( 'Method RegExp.prototype.exec called on incompatible receiver' diff --git a/src/BuiltIn/RegExp_/RegExpHelpers.php b/src/BuiltIn/RegExp_/RegExpHelpers.php index 878382e9..8e55bd69 100644 --- a/src/BuiltIn/RegExp_/RegExpHelpers.php +++ b/src/BuiltIn/RegExp_/RegExpHelpers.php @@ -61,24 +61,24 @@ private static function getRegExpStringIteratorProto(): JsObject function (JsValue $this_): JsValue { if ( !$this_ instanceof JsObject - || !$this_->hasOwnProperty('[[IteratingRegExp]]') + || !$this_->hasNativeSlot('[[IteratingRegExp]]', own: true) ) { throw new \Phasis\Exceptions\TypeError( 'next called on non-RegExp string iterator' ); } - $doneVal = $this_->get('[[Done]]'); + $doneVal = $this_->getNativeSlot('[[Done]]'); if ($doneVal instanceof JsBoolean && $doneVal->value) { return self::iterResult(JsUndefined::instance(), true); } - $R = $this_->get('[[IteratingRegExp]]'); + $R = $this_->getNativeSlot('[[IteratingRegExp]]'); if (!$R instanceof JsObject) { return self::iterResult(JsUndefined::instance(), true); } - $S = TypeConversion::toString($this_->get('[[IteratedString]]')); - $global = $this_->get('[[Global]]'); + $S = TypeConversion::toString($this_->getNativeSlot('[[IteratedString]]')); + $global = $this_->getNativeSlot('[[Global]]'); $isGlobal = $global instanceof JsBoolean && $global->value; - $fullUnicode = $this_->get('[[Unicode]]'); + $fullUnicode = $this_->getNativeSlot('[[Unicode]]'); $isUnicode = $fullUnicode instanceof JsBoolean && $fullUnicode->value; $match = self::regExpExec($R, $S); diff --git a/src/BuiltIn/RegExp_/RegExpSymbolHooks.php b/src/BuiltIn/RegExp_/RegExpSymbolHooks.php index 4cabd8e4..b2bd521f 100644 --- a/src/BuiltIn/RegExp_/RegExpSymbolHooks.php +++ b/src/BuiltIn/RegExp_/RegExpSymbolHooks.php @@ -498,7 +498,7 @@ private static function symbolSplit(): \Closure // Get the PCRE pattern from the splitter (which may differ from the // original if side effects recompiled the regex). /** @var JsObject $splitter */ - $pcrePatternDesc = $splitter->getOwnPropertyDescriptor('[[PCREPattern]]'); + $pcrePatternDesc = $splitter->getNativeSlotDescriptor('[[PCREPattern]]'); $pcrePattern = ($pcrePatternDesc !== null && $pcrePatternDesc->value instanceof JsString) ? $pcrePatternDesc->value->value : null; diff --git a/src/BuiltIn/RequestConstructor.php b/src/BuiltIn/RequestConstructor.php index c65cb639..36686646 100644 --- a/src/BuiltIn/RequestConstructor.php +++ b/src/BuiltIn/RequestConstructor.php @@ -100,10 +100,10 @@ public static function install(Environment $env): void $ctor = JsFunction::fromCallable( 'Request', static function (JsValue $this_, array $args) use ($proto, $env): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'Request': Please use the 'new' operator"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); diff --git a/src/BuiltIn/ResponseConstructor.php b/src/BuiltIn/ResponseConstructor.php index 8fca49c5..5cf8041d 100644 --- a/src/BuiltIn/ResponseConstructor.php +++ b/src/BuiltIn/ResponseConstructor.php @@ -68,10 +68,10 @@ public static function install(Environment $env): void $ctor = JsFunction::fromCallable( 'Response', static function (JsValue $this_, array $args) use ($proto, $env): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'Response': Please use the 'new' operator"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); diff --git a/src/BuiltIn/SetConstructor.php b/src/BuiltIn/SetConstructor.php index 030699aa..14a698a2 100644 --- a/src/BuiltIn/SetConstructor.php +++ b/src/BuiltIn/SetConstructor.php @@ -46,11 +46,11 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'Set', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor Set requires \'new\''); } $effectiveProto = $proto; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof JsObject) { @@ -110,7 +110,7 @@ public static function getSetIteratorPrototype(?JsObject $iteratorPrototype = nu 'Method Set Iterator.prototype.next called on incompatible receiver', ); } - $slotDesc = $this_->getOwnPropertyDescriptor('[[SetIteratorData]]'); + $slotDesc = $this_->getNativeSlotDescriptor('[[SetIteratorData]]'); if ($slotDesc === null) { throw new TypeError( 'Method Set Iterator.prototype.next called on incompatible receiver', diff --git a/src/BuiltIn/ShadowRealmConstructor.php b/src/BuiltIn/ShadowRealmConstructor.php index aac4d98a..ce991fcd 100644 --- a/src/BuiltIn/ShadowRealmConstructor.php +++ b/src/BuiltIn/ShadowRealmConstructor.php @@ -34,7 +34,7 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'ShadowRealm', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor ShadowRealm requires \'new\''); } @@ -53,7 +53,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { // instead. Falls back to the active realm on the (rare) // case where NewTarget isn't a JsFunction. $outerRealm = null; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction && $newTarget->realm !== null) { $outerRealm = $newTarget->realm; } diff --git a/src/BuiltIn/Streams/ByteStream.php b/src/BuiltIn/Streams/ByteStream.php index 704f909e..6e282387 100644 --- a/src/BuiltIn/Streams/ByteStream.php +++ b/src/BuiltIn/Streams/ByteStream.php @@ -100,7 +100,7 @@ function (JsValue $this_, array $args): JsValue { $ctorByobReader = JsFunction::fromCallable( 'ReadableStreamBYOBReader', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor ReadableStreamBYOBReader requires 'new'"); } $stream = $args[0] ?? JsUndefined::instance(); @@ -111,7 +111,7 @@ function (JsValue $this_, array $args): JsValue { if ($stream->getInternalProperty('[[ControllerType]]') !== 'byte') { throw new TypeError('Cannot construct BYOB reader for non-byte stream'); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getByobReaderPrototype(); diff --git a/src/BuiltIn/Streams/CompressionStream.php b/src/BuiltIn/Streams/CompressionStream.php index 9716c12a..ac451b50 100644 --- a/src/BuiltIn/Streams/CompressionStream.php +++ b/src/BuiltIn/Streams/CompressionStream.php @@ -51,7 +51,7 @@ private static function makeConstructor(bool $compress): JsFunction $ctor = JsFunction::fromCallable( $className, static function (JsValue $this_, array $args) use ($proto, $compress, $className): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor {$className} requires 'new'"); } $formatArg = $args[0] ?? JsUndefined::instance(); diff --git a/src/BuiltIn/Streams/QueuingStrategy.php b/src/BuiltIn/Streams/QueuingStrategy.php index aae088c4..ec3a0cab 100644 --- a/src/BuiltIn/Streams/QueuingStrategy.php +++ b/src/BuiltIn/Streams/QueuingStrategy.php @@ -37,7 +37,7 @@ private static function installByteLength(Environment $env): void $ctor = JsFunction::fromCallable( 'ByteLengthQueuingStrategy', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor ByteLengthQueuingStrategy requires 'new'"); } $init = $args[0] ?? JsUndefined::instance(); @@ -48,7 +48,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if ($hwm instanceof JsUndefined) { throw new TypeError('highWaterMark required'); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; @@ -127,7 +127,7 @@ private static function installCount(Environment $env): void $ctor = JsFunction::fromCallable( 'CountQueuingStrategy', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor CountQueuingStrategy requires 'new'"); } $init = $args[0] ?? JsUndefined::instance(); @@ -138,7 +138,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { if ($hwm instanceof JsUndefined) { throw new TypeError('highWaterMark required'); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; diff --git a/src/BuiltIn/Streams/ReadableStream.php b/src/BuiltIn/Streams/ReadableStream.php index 6875044b..67152e23 100644 --- a/src/BuiltIn/Streams/ReadableStream.php +++ b/src/BuiltIn/Streams/ReadableStream.php @@ -93,10 +93,10 @@ public static function install(Environment $env): JsFunction $constructor = JsFunction::fromCallable( 'ReadableStream', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor ReadableStream requires 'new'"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getPrototype(); @@ -201,14 +201,14 @@ private static function installReaderConstructor(Environment $env): void $ctor = JsFunction::fromCallable( 'ReadableStreamDefaultReader', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor ReadableStreamDefaultReader requires 'new'"); } $stream = $args[0] ?? JsUndefined::instance(); if (!self::isReadableStream($stream)) { throw new TypeError('Argument must be a ReadableStream'); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getReaderPrototype(); diff --git a/src/BuiltIn/Streams/TransformStream.php b/src/BuiltIn/Streams/TransformStream.php index 85710866..ec0e22a5 100644 --- a/src/BuiltIn/Streams/TransformStream.php +++ b/src/BuiltIn/Streams/TransformStream.php @@ -65,10 +65,10 @@ public static function install(Environment $env): JsFunction $ctor = JsFunction::fromCallable( 'TransformStream', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor TransformStream requires 'new'"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getPrototype(); diff --git a/src/BuiltIn/Streams/WritableStream.php b/src/BuiltIn/Streams/WritableStream.php index adefad65..e9dc7802 100644 --- a/src/BuiltIn/Streams/WritableStream.php +++ b/src/BuiltIn/Streams/WritableStream.php @@ -92,10 +92,10 @@ public static function install(Environment $env): JsFunction $ctor = JsFunction::fromCallable( 'WritableStream', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor WritableStream requires 'new'"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getPrototype(); @@ -157,7 +157,7 @@ function (JsValue $this_, array $args): JsValue { $ctorWriter = JsFunction::fromCallable( 'WritableStreamDefaultWriter', function (JsValue $this_, array $args): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor WritableStreamDefaultWriter requires 'new'"); } $stream = $args[0] ?? JsUndefined::instance(); @@ -165,7 +165,7 @@ function (JsValue $this_, array $args): JsValue { throw new TypeError('Argument must be a WritableStream'); } /** @var JsObject $stream */ - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : self::getWriterPrototype(); diff --git a/src/BuiltIn/String/StringIteration.php b/src/BuiltIn/String/StringIteration.php index 037651a2..0ed40bdb 100644 --- a/src/BuiltIn/String/StringIteration.php +++ b/src/BuiltIn/String/StringIteration.php @@ -233,7 +233,7 @@ private static function toStringFn(): \Closure return $this_; } if ($this_ instanceof JsObject) { - $prim = $this_->get('[[PrimitiveValue]]'); + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsString) { return $prim; } diff --git a/src/BuiltIn/String/StringSearch.php b/src/BuiltIn/String/StringSearch.php index b001c223..60b591e5 100644 --- a/src/BuiltIn/String/StringSearch.php +++ b/src/BuiltIn/String/StringSearch.php @@ -1239,8 +1239,8 @@ private static function matchAll(): \Closure // If searchArg is a RegExp object, extract its source pattern. if ($searchArg instanceof JsUndefined) { $R = '(?:)'; - } elseif ($searchArg instanceof JsObject && $searchArg->has('[[OriginalSource]]')) { - $sourceVal = $searchArg->get('[[OriginalSource]]'); + } elseif ($searchArg instanceof JsObject && $searchArg->hasNativeSlot('[[OriginalSource]]')) { + $sourceVal = $searchArg->getNativeSlot('[[OriginalSource]]'); $R = $sourceVal instanceof JsString ? $sourceVal->value : TypeConversion::toString($searchArg); } else { $R = TypeConversion::toString($searchArg); diff --git a/src/BuiltIn/StructuredCloneFunction.php b/src/BuiltIn/StructuredCloneFunction.php index 8487f988..7e9b84d9 100644 --- a/src/BuiltIn/StructuredCloneFunction.php +++ b/src/BuiltIn/StructuredCloneFunction.php @@ -277,12 +277,12 @@ private static function cloneValue( } // RegExp — detected via the [[OriginalSource]] internal slot. - if ($value->hasOwnProperty('[[OriginalSource]]')) { + if ($value->hasNativeSlot('[[OriginalSource]]', own: true)) { return self::cloneRegExp($value, $seen); } // Error and subclasses — detected via [[ErrorData]]. - if ($value->hasOwnProperty('[[ErrorData]]')) { + if ($value->hasNativeSlot('[[ErrorData]]', own: true)) { return self::cloneError($value, $seen, $transferSet); } @@ -495,8 +495,8 @@ private static function cloneDate(JsObject $value, \SplObjectStorage $seen): JsO */ private static function cloneRegExp(JsObject $value, \SplObjectStorage $seen): JsObject { - $srcDesc = $value->getOwnPropertyDescriptor('[[OriginalSource]]'); - $flagsDesc = $value->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $srcDesc = $value->getNativeSlotDescriptor('[[OriginalSource]]'); + $flagsDesc = $value->getNativeSlotDescriptor('[[OriginalFlags]]'); $source = ($srcDesc !== null && $srcDesc->value instanceof JsString) ? $srcDesc->value->value : ''; diff --git a/src/BuiltIn/SymbolConstructor.php b/src/BuiltIn/SymbolConstructor.php index f6406d00..cb389547 100644 --- a/src/BuiltIn/SymbolConstructor.php +++ b/src/BuiltIn/SymbolConstructor.php @@ -147,8 +147,8 @@ public static function extractSymbol(JsValue $this_, string $methodName): JsSymb } // Handle Object(Symbol(...)) wrappers: [[PrimitiveValue]] holds the JsSymbol. - if ($this_ instanceof JsObject && $this_->has('[[PrimitiveValue]]')) { - $prim = $this_->get('[[PrimitiveValue]]'); + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[PrimitiveValue]]')) { + $prim = $this_->getNativeSlot('[[PrimitiveValue]]'); if ($prim instanceof JsSymbol) { return $prim; } @@ -163,7 +163,7 @@ public static function install(Environment $env): void // but invoking it via new throws TypeError per spec 20.4.1.1. $symbolFn = JsFunction::fromCallable('Symbol', function (JsValue $this_, array $args): JsValue { // When called via new, $this_ is the newly created object with [[NewTarget]]. - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Symbol is not a constructor'); } $description = null; diff --git a/src/BuiltIn/Temporal/BrandCheckHelpers.php b/src/BuiltIn/Temporal/BrandCheckHelpers.php index 8ee8e841..bfa1d1ff 100644 --- a/src/BuiltIn/Temporal/BrandCheckHelpers.php +++ b/src/BuiltIn/Temporal/BrandCheckHelpers.php @@ -34,10 +34,10 @@ trait BrandCheckHelpers private static function requireInstant(JsValue $this_): string { - if (!$this_ instanceof JsObject || !$this_->has('[[EpochNanoseconds]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[EpochNanoseconds]]')) { throw new TypeError('this is not a Temporal.Instant'); } - $v = $this_->get('[[EpochNanoseconds]]'); + $v = $this_->getNativeSlot('[[EpochNanoseconds]]'); return $v instanceof JsString ? $v->value : '0'; } @@ -46,7 +46,7 @@ private static function requireInstant(JsValue $this_): string */ private static function requireDuration(JsValue $this_): void { - if (!$this_ instanceof JsObject || !$this_->has('[[IsDuration]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[IsDuration]]')) { throw new TypeError('this is not a Temporal.Duration'); } } @@ -54,14 +54,14 @@ private static function requireDuration(JsValue $this_): void private static function requirePlainDate(JsValue $this_): void { $isPlainDate = $this_ instanceof JsObject - && $this_->has('[[ISOYear]]') - && !$this_->has('[[IsPlainTime]]') - && !$this_->has('[[IsPlainDateTime]]') - && !$this_->has('[[IsPlainYearMonth]]') - && !$this_->has('[[IsPlainMonthDay]]') - && !$this_->has('[[IsZonedDateTime]]') - && !$this_->has('[[IsDuration]]') - && !$this_->has('[[EpochNanoseconds]]'); + && $this_->hasNativeSlot('[[ISOYear]]') + && !$this_->hasNativeSlot('[[IsPlainTime]]') + && !$this_->hasNativeSlot('[[IsPlainDateTime]]') + && !$this_->hasNativeSlot('[[IsPlainYearMonth]]') + && !$this_->hasNativeSlot('[[IsPlainMonthDay]]') + && !$this_->hasNativeSlot('[[IsZonedDateTime]]') + && !$this_->hasNativeSlot('[[IsDuration]]') + && !$this_->hasNativeSlot('[[EpochNanoseconds]]'); if (!$isPlainDate) { throw new TypeError('this is not a Temporal.PlainDate'); } @@ -69,7 +69,7 @@ private static function requirePlainDate(JsValue $this_): void private static function requirePlainTime(JsValue $this_): bool { - if (!$this_ instanceof JsObject || !$this_->has('[[IsPlainTime]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[IsPlainTime]]')) { throw new TypeError('this is not a Temporal.PlainTime'); } return true; @@ -77,7 +77,7 @@ private static function requirePlainTime(JsValue $this_): bool private static function requirePlainDateTime(JsValue $this_): bool { - if (!$this_ instanceof JsObject || !$this_->has('[[IsPlainDateTime]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[IsPlainDateTime]]')) { throw new TypeError('this is not a Temporal.PlainDateTime'); } return true; diff --git a/src/BuiltIn/Temporal/CalendarRoundingHelpers.php b/src/BuiltIn/Temporal/CalendarRoundingHelpers.php index bbe8d2b7..fe48a572 100644 --- a/src/BuiltIn/Temporal/CalendarRoundingHelpers.php +++ b/src/BuiltIn/Temporal/CalendarRoundingHelpers.php @@ -26,7 +26,7 @@ private static function calendarUnitEpochNs(JsValue $reference, string $unit, in 0, 0, ); - if ($reference instanceof JsObject && $reference->has('[[IsZonedDateTime]]')) { + if ($reference instanceof JsObject && $reference->hasNativeSlot('[[IsZonedDateTime]]')) { $result = self::addDurationToZdt($reference, $duration, 1); self::validateInstantRange($result); return $result; @@ -41,7 +41,7 @@ private static function calendarUnitEpochNs(JsValue $reference, string $unit, in private static function durationDestinationEpochNs(JsValue $reference, JsValue $duration): string { - if ($reference instanceof JsObject && $reference->has('[[IsZonedDateTime]]')) { + if ($reference instanceof JsObject && $reference->hasNativeSlot('[[IsZonedDateTime]]')) { $result = self::addDurationToZdt($reference, $duration, 1); self::validateInstantRange($result); return $result; @@ -85,7 +85,7 @@ private static function calendarUnitBounds(JsValue $reference, string $destinati $start = self::calendarUnitEpochNs($reference, $unit, 0); $direction = bccomp($destination, $start, 0) < 0 ? -1 : 1; $calendar = self::getSlotString($reference, '[[Calendar]]'); - $zone = $reference instanceof JsObject && $reference->has('[[IsZonedDateTime]]') + $zone = $reference instanceof JsObject && $reference->hasNativeSlot('[[IsZonedDateTime]]') ? self::getSlotString($reference, '[[TimeZone]]') : 'UTC'; $baseParts = self::epochNsToISOParts($start, $zone); $endParts = self::epochNsToISOParts($destination, $zone); diff --git a/src/BuiltIn/Temporal/DurationHelpers.php b/src/BuiltIn/Temporal/DurationHelpers.php index 348bf905..863ddb48 100644 --- a/src/BuiltIn/Temporal/DurationHelpers.php +++ b/src/BuiltIn/Temporal/DurationHelpers.php @@ -37,7 +37,7 @@ private static function getDurationField(JsValue $obj, string $field): int|float if (!$obj instanceof JsObject) { return 0; } - $v = $obj->get("[[{$field}]]"); + $v = $obj->getNativeSlot("[[{$field}]]"); if ($v instanceof JsNumber) { if ($v->value >= PHP_INT_MAX || $v->value < PHP_INT_MIN) { return $v->value; @@ -152,7 +152,7 @@ private static function validateDurationRange(array $fields): void private static function toDuration(JsValue $item, bool $copy = false): JsObject { - if ($item instanceof JsObject && $item->has('[[IsDuration]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsDuration]]')) { if ($copy) { // Duration.from must return a new copy. $fields = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds']; @@ -550,7 +550,7 @@ private static function getDurationFieldStr(JsValue $obj, string $field): string if (!$obj instanceof JsObject) { return '0'; } - $v = $obj->get("[[{$field}]]"); + $v = $obj->getNativeSlot("[[{$field}]]"); if ($v instanceof JsNumber) { if (abs($v->value) < 1e15) { return (string) (int) $v->value; @@ -592,7 +592,7 @@ private static function durationTotalWithRelativeTo(JsValue $dur, string $unit, { // ZDT-aware totals: compute via actual epoch ns so that DST-shifted // wall days and months contribute their real (23/24/25h) length. - if ($relativeTo instanceof JsObject && $relativeTo->has('[[IsZonedDateTime]]')) { + if ($relativeTo instanceof JsObject && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]')) { $tzZdt = self::getSlotString($relativeTo, '[[TimeZone]]'); $startNsZdt = self::getSlotString($relativeTo, '[[EpochNanoseconds]]'); $endNsZdt = self::addDurationToZdt($relativeTo, $dur, 1, 'constrain'); @@ -742,16 +742,16 @@ private static function durationTotalWithRelativeTo(JsValue $dur, string $unit, } // Parse relativeTo as a PlainDate or PlainDateTime. $refDate = null; - if ($relativeTo instanceof JsObject && $relativeTo->has('[[IsPlainDate]]')) { + if ($relativeTo instanceof JsObject && $relativeTo->hasNativeSlot('[[IsPlainDate]]')) { $refDate = $relativeTo; - } elseif ($relativeTo instanceof JsObject && $relativeTo->has('[[IsPlainDateTime]]')) { + } elseif ($relativeTo instanceof JsObject && $relativeTo->hasNativeSlot('[[IsPlainDateTime]]')) { $refDate = self::createPlainDateObject( self::getSlotInt($relativeTo, '[[ISOYear]]'), self::getSlotInt($relativeTo, '[[ISOMonth]]'), self::getSlotInt($relativeTo, '[[ISODay]]'), self::getSlotString($relativeTo, '[[Calendar]]'), ); - } elseif ($relativeTo instanceof JsObject && $relativeTo->has('[[IsZonedDateTime]]')) { + } elseif ($relativeTo instanceof JsObject && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]')) { $parts = self::zonedDateTimeParts($relativeTo); $refDate = self::createPlainDateObject( $parts['year'], @@ -1019,7 +1019,7 @@ private static function roundDuration( $calUnits = ['year', 'month', 'week']; $hasCalUnit = $years !== 0 || $months !== 0 || $weeks !== 0; $relIsZdt = $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]'); + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]'); $needsCalendar = in_array($unit, $calUnits, true) || in_array($largestUnit, $calUnits, true) || $hasCalUnit @@ -1027,7 +1027,7 @@ private static function roundDuration( if ($needsCalendar && $relativeTo !== null) { $refDate = $relativeTo; $isZdtRelativeTo = $refDate instanceof JsObject - && $refDate->has('[[IsZonedDateTime]]'); + && $refDate->hasNativeSlot('[[IsZonedDateTime]]'); $zdtTzIsDst = false; if ($isZdtRelativeTo) { $tz = self::getSlotString($refDate, '[[TimeZone]]'); diff --git a/src/BuiltIn/Temporal/DurationSection.php b/src/BuiltIn/Temporal/DurationSection.php index e5ea2729..bfa5b6aa 100644 --- a/src/BuiltIn/Temporal/DurationSection.php +++ b/src/BuiltIn/Temporal/DurationSection.php @@ -43,7 +43,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J self::defineGetter($proto, $field, function (JsValue $this_) use ($field): JsValue { self::requireDuration($this_); // Read the float value directly to avoid int overflow for large values. - $v = $this_->get("[[{$field}]]"); + $v = $this_->getNativeSlot("[[{$field}]]"); return ($v instanceof JsNumber) ? $v : JsNumber::of(0.0); }); } @@ -184,7 +184,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J // fall through to PlainDate parsing below } } - } elseif ($rtv instanceof JsObject && $rtv->has('[[IsZonedDateTime]]')) { + } elseif ($rtv instanceof JsObject && $rtv->hasNativeSlot('[[IsZonedDateTime]]')) { $zonedRelativeTo = $rtv; } $relativeTo = self::toRelativeToPlainDate($rtv); @@ -329,7 +329,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $relativeTo !== null && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') ) { $epochNs = self::getSlotString($relativeTo, '[[EpochNanoseconds]]'); $durNs = self::durationToTotalNs($this_); @@ -345,7 +345,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $relativeTo !== null && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') && $unit === 'day' && $hasCalUnit ) { @@ -366,7 +366,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $relativeTo !== null && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') && in_array($unit, $subDayUnits, true) && ($hasDayUnit || $unit === 'day') ) { @@ -437,7 +437,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J // Constructor $ctor = JsFunction::fromCallable('Duration', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.Duration must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -557,7 +557,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $refDate === null && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') ) { $parts = self::zonedDateTimeParts($relativeTo); $refDate = self::createPlainDateObject( @@ -575,7 +575,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $hasDateUnit && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') ) { $epochNs = self::getSlotString($relativeTo, '[[EpochNanoseconds]]'); $durNs1 = self::durationToTotalNs($one); @@ -588,7 +588,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J $relativeTo !== null && $hasCalUnit && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') ) { // ZDT-aware compare: build end ZDTs respecting DST. $end1 = self::addDurationToZdt($relativeTo, $one, 1, 'constrain'); @@ -649,7 +649,7 @@ private static function installDuration(JsObject $temporal, Environment $env): J if ( $relativeTo !== null && $relativeTo instanceof JsObject - && $relativeTo->has('[[IsZonedDateTime]]') + && $relativeTo->hasNativeSlot('[[IsZonedDateTime]]') && ($oneHasDay || $twoHasDay) ) { $end1 = self::addDurationToZdt($relativeTo, $one, 1, 'constrain'); diff --git a/src/BuiltIn/Temporal/InstantParsingHelpers.php b/src/BuiltIn/Temporal/InstantParsingHelpers.php index 59d7d5d4..4fa69d75 100644 --- a/src/BuiltIn/Temporal/InstantParsingHelpers.php +++ b/src/BuiltIn/Temporal/InstantParsingHelpers.php @@ -73,10 +73,10 @@ private static function validateTemporalStringSyntax(string $value): void private static function toInstantNs(JsValue $item): string { - if ($item instanceof JsObject && $item->has('[[EpochNanoseconds]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[EpochNanoseconds]]')) { return self::requireInstant($item); } - if ($item instanceof JsObject && $item->has('[[IsZonedDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsZonedDateTime]]')) { return self::getSlotString($item, '[[EpochNanoseconds]]'); } if ($item instanceof JsUndefined || $item instanceof JsNull) { diff --git a/src/BuiltIn/Temporal/InstantSection.php b/src/BuiltIn/Temporal/InstantSection.php index 5173ef4c..6578fe62 100644 --- a/src/BuiltIn/Temporal/InstantSection.php +++ b/src/BuiltIn/Temporal/InstantSection.php @@ -210,7 +210,7 @@ private static function installInstant(JsObject $temporal, Environment $env): Js // Constructor $ctor = JsFunction::fromCallable('Instant', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.Instant must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -231,7 +231,7 @@ private static function installInstant(JsObject $temporal, Environment $env): Js $ctor->defineOwnProperty('from', PropertyDescriptor::data( JsFunction::fromCallable('from', function (JsValue $this_, array $args): JsValue { $item = $args[0] ?? JsUndefined::instance(); - if ($item instanceof JsObject && $item->has('[[EpochNanoseconds]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[EpochNanoseconds]]')) { $ns = self::requireInstant($item); return self::createInstantObject($ns); } diff --git a/src/BuiltIn/Temporal/MethodRegistrationHelpers.php b/src/BuiltIn/Temporal/MethodRegistrationHelpers.php index ae891e06..07c8a5a3 100644 --- a/src/BuiltIn/Temporal/MethodRegistrationHelpers.php +++ b/src/BuiltIn/Temporal/MethodRegistrationHelpers.php @@ -173,7 +173,7 @@ private static function resolveLocaleCalendar(?JsValue $localeArg): ?string $localeArg ?? JsUndefined::instance(), JsUndefined::instance(), ]); - $resolved = $obj->get('[[Calendar]]'); + $resolved = $obj->getNativeSlot('[[Calendar]]'); return $resolved instanceof JsString ? $resolved->value : null; } @@ -599,8 +599,8 @@ private static function temporalToLocaleString( $optionsArg = $args[1] ?? JsUndefined::instance(); if ( $this_ instanceof JsObject - && $this_->has('[[EpochNanoseconds]]') - && !$this_->has('[[IsZonedDateTime]]') + && $this_->hasNativeSlot('[[EpochNanoseconds]]') + && !$this_->hasNativeSlot('[[IsZonedDateTime]]') ) { $optionsArg = self::ensureFullDateTimeOptions($optionsArg); } @@ -610,20 +610,20 @@ private static function temporalToLocaleString( if ( $this_ instanceof JsObject && ( - $this_->has('[[IsPlainDate]]') - || $this_->has('[[IsPlainDateTime]]') - || $this_->has('[[IsPlainYearMonth]]') - || $this_->has('[[IsPlainMonthDay]]') + $this_->hasNativeSlot('[[IsPlainDate]]') + || $this_->hasNativeSlot('[[IsPlainDateTime]]') + || $this_->hasNativeSlot('[[IsPlainYearMonth]]') + || $this_->hasNativeSlot('[[IsPlainMonthDay]]') ) ) { - $instCal = $this_->get('[[Calendar]]'); + $instCal = $this_->getNativeSlot('[[Calendar]]'); $instCalStr = $instCal instanceof JsString ? $instCal->value : 'iso8601'; // PlainMonthDay (no year) and PlainYearMonth (no day) require // the formatter's calendar to match the instance's calendar: // even ISO instances reject a non-ISO locale calendar. Plain // date/datetime accept ISO instances on any locale. - $isStrict = $this_->has('[[IsPlainMonthDay]]') - || $this_->has('[[IsPlainYearMonth]]'); + $isStrict = $this_->hasNativeSlot('[[IsPlainMonthDay]]') + || $this_->hasNativeSlot('[[IsPlainYearMonth]]'); if ($instCalStr !== 'iso8601' || $isStrict) { // Resolve the formatter's effective calendar by // accounting for an explicit options.calendar @@ -677,10 +677,10 @@ private static function temporalToLocaleString( if ( $this_ instanceof JsObject && ( - (($this_->has('[[IsPlainDate]]') || $this_->has('[[IsPlainYearMonth]]') - || $this_->has('[[IsPlainMonthDay]]')) - && !$obj->get('[[TimeStyle]]') instanceof JsUndefined) - || ($this_->has('[[IsPlainTime]]') && !$obj->get('[[DateStyle]]') instanceof JsUndefined) + (($this_->hasNativeSlot('[[IsPlainDate]]') || $this_->hasNativeSlot('[[IsPlainYearMonth]]') + || $this_->hasNativeSlot('[[IsPlainMonthDay]]')) + && !$obj->getNativeSlot('[[TimeStyle]]') instanceof JsUndefined) + || ($this_->hasNativeSlot('[[IsPlainTime]]') && !$obj->getNativeSlot('[[DateStyle]]') instanceof JsUndefined) ) ) { throw new TypeError('Temporal type does not support the requested date or time style'); diff --git a/src/BuiltIn/Temporal/PlainDateSection.php b/src/BuiltIn/Temporal/PlainDateSection.php index 53aec15f..1af039ef 100644 --- a/src/BuiltIn/Temporal/PlainDateSection.php +++ b/src/BuiltIn/Temporal/PlainDateSection.php @@ -481,7 +481,7 @@ private static function installPlainDate(JsObject $temporal, Environment $env): // Constructor $ctor = JsFunction::fromCallable('PlainDate', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.PlainDate must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -533,11 +533,12 @@ private static function installPlainDate(JsObject $temporal, Environment $env): // For known Temporal types: convert first, then read options. if ( $item instanceof JsObject && ( - ($item->has('[[ISOYear]]') && !$item->has('[[IsPlainTime]]') && !$item->has('[[IsPlainDateTime]]') - && !$item->has('[[IsPlainYearMonth]]') && !$item->has('[[IsPlainMonthDay]]') - && !$item->has('[[IsZonedDateTime]]') && !$item->has('[[IsDuration]]') && !$item->has('[[EpochNanoseconds]]')) - || $item->has('[[IsPlainDateTime]]') - || $item->has('[[IsZonedDateTime]]') + ($item->hasNativeSlot('[[ISOYear]]') && !$item->hasNativeSlot('[[IsPlainTime]]') && !$item->hasNativeSlot('[[IsPlainDateTime]]') + && !$item->hasNativeSlot('[[IsPlainYearMonth]]') && !$item->hasNativeSlot('[[IsPlainMonthDay]]') + && !$item->hasNativeSlot('[[IsZonedDateTime]]') && !$item->hasNativeSlot('[[IsDuration]]') + && !$item->hasNativeSlot('[[EpochNanoseconds]]')) + || $item->hasNativeSlot('[[IsPlainDateTime]]') + || $item->hasNativeSlot('[[IsZonedDateTime]]') ) ) { $result = self::toPlainDate($item, 'constrain'); diff --git a/src/BuiltIn/Temporal/PlainDateTimeSection.php b/src/BuiltIn/Temporal/PlainDateTimeSection.php index 3f4c63b9..85ad2c83 100644 --- a/src/BuiltIn/Temporal/PlainDateTimeSection.php +++ b/src/BuiltIn/Temporal/PlainDateTimeSection.php @@ -576,7 +576,7 @@ private static function installPlainDateTime(JsObject $temporal, Environment $en self::installTemporalToPrimitive($proto, 'PlainDateTime'); $ctor = JsFunction::fromCallable('PlainDateTime', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.PlainDateTime must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -652,9 +652,9 @@ private static function installPlainDateTime(JsObject $temporal, Environment $en if ( $item instanceof JsObject && ( - $item->has('[[IsPlainDateTime]]') - || $item->has('[[IsPlainDate]]') - || $item->has('[[IsZonedDateTime]]') + $item->hasNativeSlot('[[IsPlainDateTime]]') + || $item->hasNativeSlot('[[IsPlainDate]]') + || $item->hasNativeSlot('[[IsZonedDateTime]]') ) ) { $result = self::toPlainDateTime($item); diff --git a/src/BuiltIn/Temporal/PlainMonthDaySection.php b/src/BuiltIn/Temporal/PlainMonthDaySection.php index 20a51820..53fc2e4d 100644 --- a/src/BuiltIn/Temporal/PlainMonthDaySection.php +++ b/src/BuiltIn/Temporal/PlainMonthDaySection.php @@ -201,7 +201,7 @@ private static function installPlainMonthDay(JsObject $temporal, Environment $en self::installTemporalToPrimitive($proto, 'PlainMonthDay'); $ctor = JsFunction::fromCallable('PlainMonthDay', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.PlainMonthDay must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -259,7 +259,7 @@ private static function installPlainMonthDay(JsObject $temporal, Environment $en $item = $args[0] ?? JsUndefined::instance(); $rawOptions = $args[1] ?? JsUndefined::instance(); // For strings and PlainMonthDay instances, process first then validate options. - if ($item instanceof JsString || ($item instanceof JsObject && $item->has('[[IsPlainMonthDay]]'))) { + if ($item instanceof JsString || ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainMonthDay]]'))) { $result = self::toPlainMonthDay($item); $options = self::getOptionsObject($rawOptions); self::getOverflow($options); diff --git a/src/BuiltIn/Temporal/PlainTimeSection.php b/src/BuiltIn/Temporal/PlainTimeSection.php index 7f2a68fc..82c8fb62 100644 --- a/src/BuiltIn/Temporal/PlainTimeSection.php +++ b/src/BuiltIn/Temporal/PlainTimeSection.php @@ -261,7 +261,7 @@ private static function installPlainTime(JsObject $temporal, Environment $env): self::installTemporalToPrimitive($proto, 'PlainTime'); $ctor = JsFunction::fromCallable('PlainTime', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.PlainTime must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -298,7 +298,8 @@ private static function installPlainTime(JsObject $temporal, Environment $env): // For strings/PlainTime/PlainDateTime/ZonedDateTime: convert first, then validate options. if ( $item instanceof JsString || ($item instanceof JsObject && ( - $item->has('[[IsPlainTime]]') || $item->has('[[IsPlainDateTime]]') || $item->has('[[IsZonedDateTime]]') + $item->hasNativeSlot('[[IsPlainTime]]') || $item->hasNativeSlot('[[IsPlainDateTime]]') + || $item->hasNativeSlot('[[IsZonedDateTime]]') )) ) { $result = self::toPlainTime($item); diff --git a/src/BuiltIn/Temporal/PlainYearMonthSection.php b/src/BuiltIn/Temporal/PlainYearMonthSection.php index 117e89b5..3a6c489d 100644 --- a/src/BuiltIn/Temporal/PlainYearMonthSection.php +++ b/src/BuiltIn/Temporal/PlainYearMonthSection.php @@ -319,7 +319,7 @@ private static function installPlainYearMonth(JsObject $temporal, Environment $e self::installTemporalToPrimitive($proto, 'PlainYearMonth'); $ctor = JsFunction::fromCallable('PlainYearMonth', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.PlainYearMonth must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -384,7 +384,7 @@ private static function installPlainYearMonth(JsObject $temporal, Environment $e ) { return self::toPlainYearMonth($item); } - if ($item instanceof JsObject && $item->has('[[IsPlainYearMonth]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainYearMonth]]')) { $options = self::getOptionsObject($args[1] ?? JsUndefined::instance()); self::getOverflow($options); return self::createPlainYearMonthObject( diff --git a/src/BuiltIn/Temporal/TemporalUnitsHelpers.php b/src/BuiltIn/Temporal/TemporalUnitsHelpers.php index 826ccfb3..9621a5cf 100644 --- a/src/BuiltIn/Temporal/TemporalUnitsHelpers.php +++ b/src/BuiltIn/Temporal/TemporalUnitsHelpers.php @@ -286,7 +286,7 @@ private static function resolveCalendarId(string $cal, bool $allowAnnotations = private static function toCalendarSlotValue(JsValue $calVal, bool $allowAnnotations = true): string { // Temporal objects with [[Calendar]] slot: extract directly. - if ($calVal instanceof JsObject && $calVal->has('[[Calendar]]')) { + if ($calVal instanceof JsObject && $calVal->hasNativeSlot('[[Calendar]]')) { return self::getSlotString($calVal, '[[Calendar]]'); } if ($calVal instanceof JsNull) { diff --git a/src/BuiltIn/Temporal/TimezoneHelpers.php b/src/BuiltIn/Temporal/TimezoneHelpers.php index c334c1cf..a97d9123 100644 --- a/src/BuiltIn/Temporal/TimezoneHelpers.php +++ b/src/BuiltIn/Temporal/TimezoneHelpers.php @@ -625,7 +625,7 @@ private static function roundCalendarDuration( } // Resolve year/month/day for the ref calendar context. For a ZDT, use // its wall-time components. - $refZdt = ($ref instanceof JsObject && $ref->has('[[IsZonedDateTime]]')) ? $ref : null; + $refZdt = ($ref instanceof JsObject && $ref->hasNativeSlot('[[IsZonedDateTime]]')) ? $ref : null; // DST-aware path is only needed when the ZDT's time zone can actually // produce non-24h days. UTC and fixed offset zones are always 24h. $refDstZdt = null; @@ -641,9 +641,9 @@ private static function roundCalendarDuration( $refM = $refParts['month']; $refD = $refParts['day']; } else { - $refY = $ref instanceof JsObject && $ref->has('[[ISOYear]]') ? self::getSlotInt($ref, '[[ISOYear]]') : 2000; - $refM = $ref instanceof JsObject && $ref->has('[[ISOMonth]]') ? self::getSlotInt($ref, '[[ISOMonth]]') : 1; - $refD = $ref instanceof JsObject && $ref->has('[[ISODay]]') ? self::getSlotInt($ref, '[[ISODay]]') : 1; + $refY = $ref instanceof JsObject && $ref->hasNativeSlot('[[ISOYear]]') ? self::getSlotInt($ref, '[[ISOYear]]') : 2000; + $refM = $ref instanceof JsObject && $ref->hasNativeSlot('[[ISOMonth]]') ? self::getSlotInt($ref, '[[ISOMonth]]') : 1; + $refD = $ref instanceof JsObject && $ref->hasNativeSlot('[[ISODay]]') ? self::getSlotInt($ref, '[[ISODay]]') : 1; } if ($smallestUnit === 'year' || $smallestUnit === 'month') { $destination = self::durationDestinationEpochNs($ref, $dur); diff --git a/src/BuiltIn/Temporal/TypeConversionHelpers.php b/src/BuiltIn/Temporal/TypeConversionHelpers.php index dbe23493..d784f3c6 100644 --- a/src/BuiltIn/Temporal/TypeConversionHelpers.php +++ b/src/BuiltIn/Temporal/TypeConversionHelpers.php @@ -36,9 +36,10 @@ private static function toPlainDate(JsValue $item, string $overflow = 'constrain { if ($item instanceof JsObject) { if ( - $item->has('[[ISOYear]]') && !$item->has('[[IsPlainTime]]') && !$item->has('[[IsPlainDateTime]]') - && !$item->has('[[IsPlainYearMonth]]') && !$item->has('[[IsPlainMonthDay]]') - && !$item->has('[[IsZonedDateTime]]') && !$item->has('[[IsDuration]]') && !$item->has('[[EpochNanoseconds]]') + $item->hasNativeSlot('[[ISOYear]]') && !$item->hasNativeSlot('[[IsPlainTime]]') && !$item->hasNativeSlot('[[IsPlainDateTime]]') + && !$item->hasNativeSlot('[[IsPlainYearMonth]]') && !$item->hasNativeSlot('[[IsPlainMonthDay]]') + && !$item->hasNativeSlot('[[IsZonedDateTime]]') && !$item->hasNativeSlot('[[IsDuration]]') + && !$item->hasNativeSlot('[[EpochNanoseconds]]') ) { return self::createPlainDateObject( self::getSlotInt($item, '[[ISOYear]]'), @@ -47,7 +48,7 @@ private static function toPlainDate(JsValue $item, string $overflow = 'constrain self::getSlotString($item, '[[Calendar]]'), ); } - if ($item->has('[[IsPlainDateTime]]')) { + if ($item->hasNativeSlot('[[IsPlainDateTime]]')) { return self::createPlainDateObject( self::getSlotInt($item, '[[ISOYear]]'), self::getSlotInt($item, '[[ISOMonth]]'), @@ -55,7 +56,7 @@ private static function toPlainDate(JsValue $item, string $overflow = 'constrain self::getSlotString($item, '[[Calendar]]'), ); } - if ($item->has('[[IsZonedDateTime]]')) { + if ($item->hasNativeSlot('[[IsZonedDateTime]]')) { $parts = self::zonedDateTimeParts($item); return self::createPlainDateObject( $parts['year'], @@ -154,8 +155,8 @@ private static function toRelativeToPlainDate(JsValue $item): JsObject self::$relativeToZdtCache = null; if ($item instanceof JsObject) { if ( - $item->has('[[IsPlainDate]]') || $item->has('[[IsPlainDateTime]]') - || $item->has('[[IsZonedDateTime]]') || $item->has('[[ISOYear]]') + $item->hasNativeSlot('[[IsPlainDate]]') || $item->hasNativeSlot('[[IsPlainDateTime]]') + || $item->hasNativeSlot('[[IsZonedDateTime]]') || $item->hasNativeSlot('[[ISOYear]]') ) { return self::toPlainDate($item); } @@ -409,7 +410,7 @@ private static function toPlainTime( if ($item instanceof JsUndefined || $item instanceof JsNull) { throw new TypeError('Cannot convert undefined/null to PlainTime'); } - if ($item instanceof JsObject && $item->has('[[IsPlainTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainTime]]')) { // Return a copy, not the same object. return self::createPlainTimeObject( self::getSlotInt($item, '[[ISOHour]]'), @@ -420,7 +421,7 @@ private static function toPlainTime( self::getSlotInt($item, '[[ISONanosecond]]'), ); } - if ($item instanceof JsObject && $item->has('[[IsPlainDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainDateTime]]')) { return self::createPlainTimeObject( self::getSlotInt($item, '[[ISOHour]]'), self::getSlotInt($item, '[[ISOMinute]]'), @@ -430,7 +431,7 @@ private static function toPlainTime( self::getSlotInt($item, '[[ISONanosecond]]'), ); } - if ($item instanceof JsObject && $item->has('[[IsZonedDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsZonedDateTime]]')) { $parts = self::zonedDateTimeParts($item); return self::createPlainTimeObject( $parts['hour'], @@ -640,7 +641,7 @@ private static function parsePlainTimeString(string $str): JsObject private static function toPlainDateTime(JsValue $item, string $overflow = 'constrain', ?JsValue $rawOptions = null): JsObject { - if ($item instanceof JsObject && $item->has('[[IsPlainDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainDateTime]]')) { // Return a copy per spec. return self::createPlainDateTimeObject( self::getSlotInt($item, '[[ISOYear]]'), @@ -655,7 +656,7 @@ private static function toPlainDateTime(JsValue $item, string $overflow = 'const self::getSlotString($item, '[[Calendar]]'), ); } - if ($item instanceof JsObject && $item->has('[[IsPlainDate]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainDate]]')) { return self::createPlainDateTimeObject( self::getSlotInt($item, '[[ISOYear]]'), self::getSlotInt($item, '[[ISOMonth]]'), @@ -795,7 +796,7 @@ private static function parsePlainDateTimeString(string $str): JsObject private static function toPlainYearMonthWithLazyOptions(JsValue $item, JsValue $rawOptions): JsObject { - if (!$item instanceof JsObject || $item->has('[[IsPlainYearMonth]]')) { + if (!$item instanceof JsObject || $item->hasNativeSlot('[[IsPlainYearMonth]]')) { // Should not happen via from() since instances and primitives are handled before. $options = self::getOptionsObject($rawOptions); $overflow = self::getOverflow($options); @@ -814,7 +815,7 @@ private static function toPlainYearMonth(JsValue $item, string $overflow = 'cons if ($item instanceof JsUndefined || $item instanceof JsNull) { throw new TypeError('Cannot convert undefined/null to PlainYearMonth'); } - if ($item instanceof JsObject && $item->has('[[IsPlainYearMonth]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainYearMonth]]')) { return self::createPlainYearMonthObject( self::getSlotInt($item, '[[ISOYear]]'), self::getSlotInt($item, '[[ISOMonth]]'), @@ -942,7 +943,7 @@ private static function toPlainMonthDay(JsValue $item, string $overflow = 'const throw new TypeError('Cannot convert primitive to a Temporal.PlainMonthDay'); } - if ($item instanceof JsObject && $item->has('[[IsPlainMonthDay]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsPlainMonthDay]]')) { // Copy the PlainMonthDay, do not return the same object. return self::createPlainMonthDayObject( self::getSlotInt($item, '[[ISOMonth]]'), diff --git a/src/BuiltIn/Temporal/ZonedDateTimeSection.php b/src/BuiltIn/Temporal/ZonedDateTimeSection.php index 62ed13e9..dc5b6608 100644 --- a/src/BuiltIn/Temporal/ZonedDateTimeSection.php +++ b/src/BuiltIn/Temporal/ZonedDateTimeSection.php @@ -1015,7 +1015,7 @@ private static function installZonedDateTime(JsObject $temporal, Environment $en self::installTemporalToPrimitive($proto, 'ZonedDateTime'); $ctor = JsFunction::fromCallable('ZonedDateTime', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Temporal.ZonedDateTime must be called with new'); } self::applyNewTargetPrototype($this_, $proto); @@ -1025,7 +1025,7 @@ private static function installZonedDateTime(JsObject $temporal, Environment $en $tzArg = $args[1] ?? JsUndefined::instance(); // Constructor uses canonicalizeTimeZone (rejects ISO strings), not toTemporalTimeZoneIdentifier. if (!($tzArg instanceof JsString)) { - if ($tzArg instanceof JsObject && $tzArg->has('[[IsZonedDateTime]]')) { + if ($tzArg instanceof JsObject && $tzArg->hasNativeSlot('[[IsZonedDateTime]]')) { $timeZone = self::getSlotString($tzArg, '[[TimeZone]]'); } else { throw new TypeError('Expected a string for TimeZone'); @@ -1051,7 +1051,7 @@ private static function installZonedDateTime(JsObject $temporal, Environment $en $item = $args[0] ?? JsUndefined::instance(); $rawOptions = $args[1] ?? JsUndefined::instance(); // For ZDT or string: parse first, then validate options. - if ($item instanceof JsObject && $item->has('[[IsZonedDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsZonedDateTime]]')) { $result = self::createZonedDateTimeObject( self::getSlotString($item, '[[EpochNanoseconds]]'), self::getSlotString($item, '[[TimeZone]]'), @@ -1097,7 +1097,7 @@ private static function installZonedDateTime(JsObject $temporal, Environment $en /** Convert an item to a ZonedDateTime. */ private static function toZonedDateTime(JsValue $item, ?JsValue $rawOptions = null): JsObject { - if ($item instanceof JsObject && $item->has('[[IsZonedDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsZonedDateTime]]')) { return $item; } if ($item instanceof JsString) { @@ -1215,7 +1215,7 @@ private static function zonedDateTimeFromPropertyBag(JsObject $item, ?JsValue $r private static function toZonedDateTimeNs(JsValue $item): string { - if ($item instanceof JsObject && $item->has('[[IsZonedDateTime]]')) { + if ($item instanceof JsObject && $item->hasNativeSlot('[[IsZonedDateTime]]')) { return self::getSlotString($item, '[[EpochNanoseconds]]'); } $zdt = self::toZonedDateTime($item); @@ -1765,7 +1765,7 @@ private static function toTemporalTimeZoneIdentifier(JsValue $item): string { // Per spec: if it's an Object, check for ZonedDateTime brand, otherwise throw. if ($item instanceof JsObject) { - if ($item->has('[[IsZonedDateTime]]')) { + if ($item->hasNativeSlot('[[IsZonedDateTime]]')) { return self::getSlotString($item, '[[TimeZone]]'); } throw new TypeError('Expected a string for TimeZone'); diff --git a/src/BuiltIn/TemporalObject.php b/src/BuiltIn/TemporalObject.php index 7af98564..ec35c38b 100644 --- a/src/BuiltIn/TemporalObject.php +++ b/src/BuiltIn/TemporalObject.php @@ -74,7 +74,7 @@ class TemporalObject */ private static function applyNewTargetPrototype(JsObject $receiver, JsObject $defaultProto): void { - $ntDesc = $receiver->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $receiver->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); $receiver->setPrototype( diff --git a/src/BuiltIn/TextDecoderConstructor.php b/src/BuiltIn/TextDecoderConstructor.php index ef3593aa..0c225e77 100644 --- a/src/BuiltIn/TextDecoderConstructor.php +++ b/src/BuiltIn/TextDecoderConstructor.php @@ -55,7 +55,7 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'TextDecoder', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor TextDecoder requires 'new'"); } @@ -92,7 +92,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; diff --git a/src/BuiltIn/TextEncoderConstructor.php b/src/BuiltIn/TextEncoderConstructor.php index 1579ff5b..5260f228 100644 --- a/src/BuiltIn/TextEncoderConstructor.php +++ b/src/BuiltIn/TextEncoderConstructor.php @@ -41,12 +41,12 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'TextEncoder', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor TextEncoder requires 'new'"); } // Per spec OrdinaryCreateFromConstructor: take prototype from newTarget. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $useProto = $ntProto instanceof JsObject ? $ntProto : $proto; diff --git a/src/BuiltIn/TypedArray/BufferTypes.php b/src/BuiltIn/TypedArray/BufferTypes.php index 7767efd2..19a3e7f9 100644 --- a/src/BuiltIn/TypedArray/BufferTypes.php +++ b/src/BuiltIn/TypedArray/BufferTypes.php @@ -44,7 +44,7 @@ private static function installArrayBuffer(Environment $env): void 'ArrayBuffer', function (JsValue $this_, array $args) use ($proto): JsValue { // Step 1: If NewTarget is undefined, throw a TypeError. - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor ArrayBuffer requires \'new\''); } $arg0 = $args[0] ?? JsNumber::of(0.0); @@ -68,7 +68,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { // which accesses NewTarget.prototype. When called via Reflect.construct // with a custom newTarget, this may trigger a getter or throw. $useProto = $proto; - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); if ($ntProto instanceof JsObject) { @@ -442,7 +442,7 @@ private static function installSharedArrayBuffer(Environment $env): void $constructor = JsFunction::fromCallable( 'SharedArrayBuffer', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor SharedArrayBuffer requires \'new\''); } $arg0 = $args[0] ?? JsNumber::of(0.0); @@ -462,7 +462,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } $useProto = $proto; - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); if ($ntDesc !== null && $ntDesc->value instanceof JsFunction) { $ntProto = $ntDesc->value->get('prototype'); if ($ntProto instanceof JsObject) { diff --git a/src/BuiltIn/TypedArray/DataView.php b/src/BuiltIn/TypedArray/DataView.php index abea4a91..1c713c06 100644 --- a/src/BuiltIn/TypedArray/DataView.php +++ b/src/BuiltIn/TypedArray/DataView.php @@ -42,7 +42,7 @@ private static function installDataView(Environment $env): void 'DataView', function (JsValue $this_, array $args) use ($proto): JsValue { // Step 1: NewTarget undefined → TypeError, checked before any arg coercion. - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError('Constructor DataView requires \'new\''); } $buffer = $args[0] ?? JsUndefined::instance(); @@ -94,8 +94,8 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } $effectiveProto = $proto; - if ($this_->has('[[NewTarget]]')) { - $newTarget = $this_->get('[[NewTarget]]'); + if ($this_->hasNativeSlot('[[NewTarget]]')) { + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { // Per spec OrdinaryCreateFromConstructor, the // newTarget.prototype lookup is observable and may diff --git a/src/BuiltIn/TypedArray/TypedArrayHelpers.php b/src/BuiltIn/TypedArray/TypedArrayHelpers.php index bc88b7cc..fab4802d 100644 --- a/src/BuiltIn/TypedArray/TypedArrayHelpers.php +++ b/src/BuiltIn/TypedArray/TypedArrayHelpers.php @@ -304,7 +304,7 @@ function (JsValue $this_, array $args) use ($typeName, $bpe, $proto, $ctorBox): // Per spec: if NewTarget is undefined, throw TypeError. if ( !$this_ instanceof JsObject - || $this_->getOwnPropertyDescriptor('[[NewTarget]]') === null + || $this_->getNativeSlotDescriptor('[[NewTarget]]') === null ) { throw new TypeError( "Constructor {$typeName} requires 'new'" @@ -313,7 +313,7 @@ function (JsValue $this_, array $args) use ($typeName, $bpe, $proto, $ctorBox): // Primitive lengths are coerced before prototype lookup. Buffer // and TypedArray sources resolve it before source validation. // Defer the lookup so constructTypedArray preserves that order. - $ntDesc = $this_->getOwnPropertyDescriptor('[[NewTarget]]'); + $ntDesc = $this_->getNativeSlotDescriptor('[[NewTarget]]'); $nt = ($ntDesc->value instanceof JsFunction) ? $ntDesc->value : null; diff --git a/src/BuiltIn/Url/SearchParams.php b/src/BuiltIn/Url/SearchParams.php index 7db4f72d..b45d9d24 100644 --- a/src/BuiltIn/Url/SearchParams.php +++ b/src/BuiltIn/Url/SearchParams.php @@ -485,9 +485,9 @@ function (JsValue $this_, array $args) use ($proto): JsValue { // Allow both `new URLSearchParams()` and bare-call (per // legacy compat). The latter is non-standard but harmless. - if ($this_ instanceof JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { $obj = $this_; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof JsObject) { @@ -644,7 +644,7 @@ private static function getIteratorPrototype(): JsObject if (!$this_ instanceof JsObject) { throw new TypeError('not a URLSearchParams iterator'); } - $stateDesc = $this_->getOwnPropertyDescriptor('[[USPIterState]]'); + $stateDesc = $this_->getNativeSlotDescriptor('[[USPIterState]]'); if ($stateDesc === null) { throw new TypeError('not a URLSearchParams iterator'); } diff --git a/src/BuiltIn/UrlConstructor.php b/src/BuiltIn/UrlConstructor.php index 49eceba9..547a1ee3 100644 --- a/src/BuiltIn/UrlConstructor.php +++ b/src/BuiltIn/UrlConstructor.php @@ -59,10 +59,10 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'URL', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Failed to construct 'URL': Please use the 'new' operator"); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $ntProto = $newTarget->get('prototype'); $this_->setPrototype($ntProto instanceof JsObject ? $ntProto : $proto); diff --git a/src/BuiltIn/UrlPatternConstructor.php b/src/BuiltIn/UrlPatternConstructor.php index e0635198..cf18a614 100644 --- a/src/BuiltIn/UrlPatternConstructor.php +++ b/src/BuiltIn/UrlPatternConstructor.php @@ -61,7 +61,7 @@ public static function install(Environment $env): void $ctor = JsFunction::fromCallable( 'URLPattern', static function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor URLPattern requires 'new'"); } $first = $args[0] ?? JsUndefined::instance(); diff --git a/src/BuiltIn/WeakMapConstructor.php b/src/BuiltIn/WeakMapConstructor.php index 583fd7c1..341b533b 100644 --- a/src/BuiltIn/WeakMapConstructor.php +++ b/src/BuiltIn/WeakMapConstructor.php @@ -33,11 +33,11 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'WeakMap', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor WeakMap requires \'new\''); } $effectiveProto = $proto; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof JsObject) { diff --git a/src/BuiltIn/WeakRefConstructor.php b/src/BuiltIn/WeakRefConstructor.php index 261cbcde..06942179 100644 --- a/src/BuiltIn/WeakRefConstructor.php +++ b/src/BuiltIn/WeakRefConstructor.php @@ -30,7 +30,7 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'WeakRef', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor WeakRef requires \'new\''); } $target = $args[0] ?? JsUndefined::instance(); @@ -43,7 +43,7 @@ function (JsValue $this_, array $args) use ($proto): JsValue { } // Per spec OrdinaryCreateFromConstructor: use NewTarget's prototype. - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ctorProto = $newTarget->get('prototype'); $useProto = $ctorProto instanceof JsObject ? $ctorProto : $proto; diff --git a/src/BuiltIn/WeakSetConstructor.php b/src/BuiltIn/WeakSetConstructor.php index e3937069..06051444 100644 --- a/src/BuiltIn/WeakSetConstructor.php +++ b/src/BuiltIn/WeakSetConstructor.php @@ -32,11 +32,11 @@ public static function install(Environment $env): void $constructor = JsFunction::fromCallable( 'WeakSet', function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || $this_->get('[[NewTarget]]') instanceof JsUndefined) { + if (!$this_ instanceof JsObject || $this_->getNativeSlot('[[NewTarget]]') instanceof JsUndefined) { throw new TypeError('Constructor WeakSet requires \'new\''); } $effectiveProto = $proto; - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsFunction) { $ntProto = $newTarget->get('prototype'); if ($ntProto instanceof JsObject) { diff --git a/src/BuiltIn/WebAssemblyObject.php b/src/BuiltIn/WebAssemblyObject.php index 902e9d63..e9044eb0 100644 --- a/src/BuiltIn/WebAssemblyObject.php +++ b/src/BuiltIn/WebAssemblyObject.php @@ -72,7 +72,7 @@ private static function buildMemory(): JsFunction $ctor = JsFunction::fromCallable( 'Memory', static function (JsValue $this_, array $args) use ($proto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("WebAssembly.Memory(): WebAssembly.Memory must be invoked with 'new'"); } $descriptor = $args[0] ?? JsUndefined::instance(); @@ -114,7 +114,7 @@ static function (JsValue $this_, array $args) use ($proto): JsValue { ); } - $newTarget = $this_->get('[[NewTarget]]'); + $newTarget = $this_->getNativeSlot('[[NewTarget]]'); if ($newTarget instanceof JsObject) { $targetProto = $newTarget->get('prototype'); $this_->setPrototype($targetProto instanceof JsObject ? $targetProto : $proto); diff --git a/src/BuiltIn/WebSocketConstructor.php b/src/BuiltIn/WebSocketConstructor.php index 70c27ccf..197302dd 100644 --- a/src/BuiltIn/WebSocketConstructor.php +++ b/src/BuiltIn/WebSocketConstructor.php @@ -79,7 +79,7 @@ public static function install(Environment $env): void $ctor = JsFunction::fromCallable( 'WebSocket', static function (JsValue $this_, array $args) use ($proto, $env): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor WebSocket requires 'new'"); } $url = TypeConversion::toString($args[0] ?? JsUndefined::instance()); diff --git a/src/BuiltIn/XMLHttpRequestConstructor.php b/src/BuiltIn/XMLHttpRequestConstructor.php index 292468e0..a3be98b8 100644 --- a/src/BuiltIn/XMLHttpRequestConstructor.php +++ b/src/BuiltIn/XMLHttpRequestConstructor.php @@ -74,7 +74,7 @@ public static function install(Environment $env): void 'XMLHttpRequest', static function (JsValue $this_, array $args) use ($proto): JsValue { unset($args); - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor XMLHttpRequest requires 'new'"); } $this_->setPrototype($proto); @@ -158,7 +158,7 @@ static function (JsValue $this_): JsValue { $peCtor = JsFunction::fromCallable( 'ProgressEvent', static function (JsValue $this_, array $args) use ($peProto): JsValue { - if (!$this_ instanceof JsObject || !$this_->has('[[NewTarget]]')) { + if (!$this_ instanceof JsObject || !$this_->hasNativeSlot('[[NewTarget]]')) { throw new TypeError("Constructor ProgressEvent requires 'new'"); } $type = TypeConversion::toString($args[0] ?? JsUndefined::instance()); diff --git a/src/Bytecode/VM.php b/src/Bytecode/VM.php index b05c7269..89f14bac 100644 --- a/src/Bytecode/VM.php +++ b/src/Bytecode/VM.php @@ -60,7 +60,7 @@ private function lookupMember(JsValue $obj, string $name): JsValue throw new TypeError("Cannot read properties of {$type} (reading '{$name}')"); } if ($obj instanceof JsObject) { - $primSlot = $obj->getOwnPropertyDescriptor('[[PrimitiveValue]]'); + $primSlot = $obj->getNativeSlotDescriptor('[[PrimitiveValue]]'); if ($primSlot !== null && $primSlot->value instanceof \Phasis\Value\JsSymbol) { return $this->interp->vmLookupPrimitiveMember($obj, $name); } diff --git a/src/Engine.php b/src/Engine.php index 81a8ae42..49871aad 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -623,7 +623,7 @@ private function installBuiltins(): void $bigIntFn = \Phasis\Value\JsFunction::fromCallable( 'BigInt', function (\Phasis\Value\JsValue $this_, array $args): \Phasis\Value\JsValue { - if ($this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]')) { + if ($this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]')) { throw new \Phasis\Exceptions\TypeError('BigInt is not a constructor'); } $val = $args[0] ?? \Phasis\Value\JsUndefined::instance(); @@ -673,7 +673,7 @@ function (\Phasis\Value\JsValue $this_, array $args): \Phasis\Value\JsValue { $bigint = $this_ instanceof \Phasis\Value\JsBigInt ? $this_ : null; if ($bigint === null && $this_ instanceof \Phasis\Value\JsObject) { // BigInt wrapper object. - $v = $this_->get('[[BigIntData]]'); + $v = $this_->getNativeSlot('[[BigIntData]]'); $bigint = $v instanceof \Phasis\Value\JsBigInt ? $v : null; } if ($bigint === null) { @@ -737,7 +737,7 @@ function (\Phasis\Value\JsValue $this_, array $args): \Phasis\Value\JsValue { return $this_; } if ($this_ instanceof \Phasis\Value\JsObject) { - $v = $this_->get('[[BigIntData]]'); + $v = $this_->getNativeSlot('[[BigIntData]]'); if ($v instanceof \Phasis\Value\JsBigInt) { return $v; } @@ -758,7 +758,7 @@ function (\Phasis\Value\JsValue $this_, array $args): \Phasis\Value\JsValue { function (\Phasis\Value\JsValue $this_, array $args): \Phasis\Value\JsValue { $bigint = $this_ instanceof \Phasis\Value\JsBigInt ? $this_ : null; if ($bigint === null && $this_ instanceof \Phasis\Value\JsObject) { - $v = $this_->get('[[BigIntData]]'); + $v = $this_->getNativeSlot('[[BigIntData]]'); $bigint = $v instanceof \Phasis\Value\JsBigInt ? $v : null; } if ($bigint === null) { @@ -1126,7 +1126,7 @@ function (\Phasis\Value\JsValue $this_, array $args) use ($bigUintN): \Phasis\Va $arg0 = $args[0] ?? \Phasis\Value\JsUndefined::instance(); $arg1 = $args[1] ?? \Phasis\Value\JsUndefined::instance(); - $calledAsNew = $this_ instanceof \Phasis\Value\JsObject && $this_->has('[[NewTarget]]'); + $calledAsNew = $this_ instanceof \Phasis\Value\JsObject && $this_->hasNativeSlot('[[NewTarget]]'); // Per spec 22.2.3.1: IsRegExp check using @@match. If matcher is // undefined, fall back to whether the argument has the @@ -1144,7 +1144,7 @@ function (\Phasis\Value\JsValue $this_, array $args) use ($bigUintN): \Phasis\Va if ($arg0 instanceof \Phasis\Value\JsProxy) { $patternIsRegExp = false; } else { - $patternIsRegExp = $arg0->getOwnPropertyDescriptor('[[PCREPattern]]') !== null; + $patternIsRegExp = $arg0->getNativeSlotDescriptor('[[PCREPattern]]') !== null; } } else { $patternIsRegExp = \Phasis\Spec\TypeConversion::toBoolean($matchProp); @@ -1167,7 +1167,7 @@ function (\Phasis\Value\JsValue $this_, array $args) use ($bigUintN): \Phasis\Va $isSubclass = false; $newTarget = null; if ($calledAsNew) { - $ntd = $this_->getOwnPropertyDescriptor("[[NewTarget]]"); + $ntd = $this_->getNativeSlotDescriptor("[[NewTarget]]"); $baseRegExp = $globalEnv->has("RegExp") ? $globalEnv->get("RegExp") : null; if ($ntd !== null && $ntd->value !== null && $ntd->value !== $baseRegExp) { $isSubclass = true; @@ -1183,12 +1183,12 @@ function (\Phasis\Value\JsValue $this_, array $args) use ($bigUintN): \Phasis\Va $cachedPattern = null; $cachedFlags = null; if ($patternIsRegExp && $arg0 instanceof \Phasis\Value\JsObject) { - $srcDesc = $arg0->getOwnPropertyDescriptor('[[OriginalSource]]'); + $srcDesc = $arg0->getNativeSlotDescriptor('[[OriginalSource]]'); if ($srcDesc !== null && $srcDesc->value instanceof \Phasis\Value\JsString) { $cachedPattern = $srcDesc->value->value; } if ($arg1 instanceof \Phasis\Value\JsUndefined) { - $flgDesc = $arg0->getOwnPropertyDescriptor('[[OriginalFlags]]'); + $flgDesc = $arg0->getNativeSlotDescriptor('[[OriginalFlags]]'); if ($flgDesc !== null && $flgDesc->value instanceof \Phasis\Value\JsString) { $cachedFlags = $flgDesc->value->value; } diff --git a/src/Runtime/Parts/ExpressionEvaluation.php b/src/Runtime/Parts/ExpressionEvaluation.php index 43472d60..509baa77 100644 --- a/src/Runtime/Parts/ExpressionEvaluation.php +++ b/src/Runtime/Parts/ExpressionEvaluation.php @@ -1321,7 +1321,7 @@ private function evalCallExpression(CallExpression $node, Environment $env, bool } $superHadNT = false; if ($currentThis instanceof JsObject) { - $superHadNT = !($currentThis->get('[[NewTarget]]') instanceof JsUndefined); + $superHadNT = !($currentThis->getNativeSlot('[[NewTarget]]') instanceof JsUndefined); if (!$superHadNT && $superNewTarget instanceof JsFunction) { $currentThis->defineOwnProperty( '[[NewTarget]]', @@ -3893,7 +3893,7 @@ private function callFunctionInner( // requires `TE === other.TypeError`. if ($fn->isClassConstructor()) { $calledAsNew = $thisValue instanceof JsObject - && !($thisValue->get('[[NewTarget]]') instanceof JsUndefined); + && !($thisValue->getNativeSlot('[[NewTarget]]') instanceof JsUndefined); if (!$calledAsNew) { $msg = "Class constructor {$fn->getName()} cannot be invoked without 'new'"; $fnRealm = $fn->realm; @@ -4301,7 +4301,7 @@ private function executeFunction( // Store the newObj so super() can pass it to the parent constructor. $fnEnv->defineVar('[[PendingThis]]', $thisValue); $ntDesc = $thisValue instanceof JsObject - ? $thisValue->getOwnPropertyDescriptor('[[NewTarget]]') + ? $thisValue->getNativeSlotDescriptor('[[NewTarget]]') : null; if ($ntDesc !== null) { $nt = $ntDesc->value; @@ -4323,7 +4323,7 @@ private function executeFunction( } if (!$vmCanSkipEnvBindings) { $ntDesc = ($thisValue instanceof JsObject && $fn->isConstructable()) - ? $thisValue->getOwnPropertyDescriptor('[[NewTarget]]') + ? $thisValue->getNativeSlotDescriptor('[[NewTarget]]') : null; if ($ntDesc !== null) { $nt = $ntDesc->value; @@ -5895,7 +5895,7 @@ public function vmLookupPrimitiveMember(JsValue $obj, string $name): JsValue } if ($obj instanceof JsObject) { // Symbol-wrapper → fall through to the full slow path. - $primSlot = $obj->getOwnPropertyDescriptor('[[PrimitiveValue]]'); + $primSlot = $obj->getNativeSlotDescriptor('[[PrimitiveValue]]'); if ($primSlot !== null && $primSlot->value instanceof JsSymbol) { $val = $this->lookupSymbolPrototypeProperty($primSlot->value, $name, false, null); if ($val !== null) { @@ -6973,7 +6973,7 @@ private function evalMemberExpression(MemberExpression $node, Environment $env): !$node->computed && $node->property instanceof Identifier && $obj instanceof JsObject - && $obj->getOwnPropertyDescriptor('[[PrimitiveValue]]') === null + && $obj->getNativeSlotDescriptor('[[PrimitiveValue]]') === null ) { return $obj->get($node->property->name); } @@ -7120,7 +7120,7 @@ private function evalMemberExpression(MemberExpression $node, Environment $env): if ($obj instanceof JsObject) { // Symbol wrapper objects created via Object(sym): look up on Symbol.prototype chain // so that description, toString, etc. are accessible with correct this-binding. - $ownDesc = $obj->getOwnPropertyDescriptor('[[PrimitiveValue]]'); + $ownDesc = $obj->getNativeSlotDescriptor('[[PrimitiveValue]]'); if ($ownDesc !== null && $ownDesc->value instanceof JsSymbol) { $val = $this->lookupSymbolPrototypeProperty( $ownDesc->value, diff --git a/src/Runtime/Parts/StatementExecution.php b/src/Runtime/Parts/StatementExecution.php index 56804128..6d8b8cc2 100644 --- a/src/Runtime/Parts/StatementExecution.php +++ b/src/Runtime/Parts/StatementExecution.php @@ -764,7 +764,7 @@ function (JsValue $thisVal, array $args) use ($ref) { // active function's new.target, not the super itself. $newTarget = $ref->fn; if ($thisVal instanceof JsObject) { - $nt = $thisVal->get('[[NewTarget]]'); + $nt = $thisVal->getNativeSlot('[[NewTarget]]'); if ($nt instanceof JsFunction || $nt instanceof \Phasis\Value\JsProxy) { $newTarget = $nt; } diff --git a/src/Value/JsFunction.php b/src/Value/JsFunction.php index fdc0bd59..5f8a7356 100644 --- a/src/Value/JsFunction.php +++ b/src/Value/JsFunction.php @@ -931,7 +931,7 @@ public function call(JsValue $thisValue, array $args): JsValue // class-ctor-realm tests see the matching constructor identity. if ($this->isClassConstructor) { $calledAsNew = $thisValue instanceof JsObject - && !($thisValue->get('[[NewTarget]]') instanceof JsUndefined); + && !($thisValue->getNativeSlot('[[NewTarget]]') instanceof JsUndefined); if (!$calledAsNew) { $msg = "Class constructor {$this->name} cannot be invoked without 'new'"; $fnRealm = $this->realm; diff --git a/src/Value/JsObject.php b/src/Value/JsObject.php index 0dfc766d..b53ecd6a 100644 --- a/src/Value/JsObject.php +++ b/src/Value/JsObject.php @@ -121,6 +121,42 @@ public function getInternalProperty(string $name): mixed return $this->internalSlots[$name] ?? null; } + /** + * Read legacy native metadata without interpreting an ordinary data key as a slot. + * Native proxy/accessor reads can invoke user code and mutate the object. + * + * @phpstan-impure + */ + public function getNativeSlot(string $name): JsValue + { + return $this instanceof JsDataObject ? JsUndefined::instance() : $this->get($name); + } + + /** + * Check legacy native metadata while keeping public data properties unbranded. + * Proxy has traps can mutate state between otherwise identical checks. + * + * @phpstan-impure + */ + public function hasNativeSlot(string $name, bool $own = false): bool + { + if ($this instanceof JsDataObject) { + return false; + } + return $own ? $this->hasOwnProperty($name) : $this->has($name); + } + + /** + * Inspect legacy native metadata without exposing public data as a native slot. + * Exotic descriptors can invoke user code or change between calls. + * + * @phpstan-impure + */ + public function getNativeSlotDescriptor(string $name): ?PropertyDescriptor + { + return $this instanceof JsDataObject ? null : $this->getOwnPropertyDescriptor($name); + } + public static function setGlobalPrototype(JsObject $proto): void { self::$globalPrototype = $proto; diff --git a/tests/Jsx/JsxHydrationTest.php b/tests/Jsx/JsxHydrationTest.php index 1532df26..97483a0d 100644 --- a/tests/Jsx/JsxHydrationTest.php +++ b/tests/Jsx/JsxHydrationTest.php @@ -291,6 +291,8 @@ public function testDataKeysCannotBecomeHiddenNativeSlots(): void $this->assertStringContainsString('[[public]],[[PrimitiveValue]]', $result->html); $this->assertStringContainsString('"[[public]]":"visible"', $result->html); $this->assertStringContainsString('"[[PrimitiveValue]]":42', $result->html); + $this->assertStringContainsString('[object Object]', $result->html); + $this->assertStringContainsString('TypeError', $result->html); } public function testLeadingNullBytesInDataKeysRemainJsonProperties(): void diff --git a/tests/Jsx/hydration/Root.jsx b/tests/Jsx/hydration/Root.jsx index 99a30930..87888169 100644 --- a/tests/Jsx/hydration/Root.jsx +++ b/tests/Jsx/hydration/Root.jsx @@ -38,7 +38,15 @@ export function Shapes({ data }) { } export function Keys(props) { - return
{Object.keys(props).sort().join(',')}:{Reflect.ownKeys(props.data).join(',')}:{JSON.stringify(props.data)}
; + let brand; + try { + Number.prototype.valueOf.call(props.data); + brand = 'number'; + } catch (error) { + brand = error.name; + } + return
{Object.keys(props).sort().join(',')}:{Reflect.ownKeys(props.data).join(',')}:{JSON.stringify(props.data)}
+        :{String(props.data)}:{Object.prototype.toString.call(props.data)}:{brand}
; } export function Document() { From c14ce4fc049e6e4dea9c6dfd7bef716e342f3aab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 25 Oct 2026 10:38:41 +0000 Subject: [PATCH 7/8] Update COMPAT.md from CI [skip ci] --- COMPAT.md | 44 ++++++++--------- compat.json | 140 ++++++++++++++++++++++++++-------------------------- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 29d74e84..06485c9c 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -3,13 +3,13 @@ Generated from a completed `test262` pass. Do not edit by hand. - Refresh: `./bin/compat-report` -- Snapshot time: `2026-10-25T10:12:22+00:00` +- Snapshot time: `2026-10-25T10:38:39+00:00` - Chunk size: `25` - Timeout: `300s` - Jobs: `4` - Groups: `161` - Test files: `53616` -- Git: `feat/react-hydration` @ `f8905d39` (dirty) +- Git: `feat/react-hydration` @ `194f0635` (dirty) ## Summary @@ -198,26 +198,26 @@ Generated from a completed `test262` pass. Do not edit by hand. | Group | Kind | Duration | Files | Sample | |---|---|---:|---:|---| -| staging/sm | NORMAL | 178.996s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js` | -| staging/sm | NORMAL | 178.664s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js` | -| staging/sm | NORMAL | 178.023s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js` | -| staging/sm | NORMAL | 177.820s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js` | -| staging/sm | NORMAL | 177.799s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js` | -| staging/sm | NORMAL | 166.032s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js` | -| built-ins/decodeURIComponent | NORMAL | 160.372s | 1 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js` | -| staging/sm | NORMAL | 154.132s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js` | -| staging/sm | NORMAL | 148.807s | 1 | `test262/test/staging/sm/TypedArray/sort_large_countingsort.js` | -| built-ins/decodeURI | NORMAL | 148.468s | 1 | `test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js` | -| staging/sm | NORMAL | 143.142s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js` | -| staging/sm | NORMAL | 119.585s | 1 | `test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js` | -| built-ins/decodeURI | NORMAL | 74.512s | 25 | `test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js`
...
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js` | -| built-ins/decodeURIComponent | NORMAL | 74.306s | 25 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js`
...
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js` | -| built-ins/TypedArray | NORMAL | 72.127s | 25 | `test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached-prototype.js`
`test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached.js`
...
`test262/test/built-ins/TypedArray/prototype/copyWithin/not-a-constructor.js`
`test262/test/built-ins/TypedArray/prototype/copyWithin/prop-desc.js` | -| intl402/DateTimeFormat | NORMAL | 66.659s | 12 | `test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js`
`test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js`
...
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/dangi-calendar-dates.js`
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/date-constructor-not-called.js` | -| annexB/built-ins/RegExp | NORMAL | 63.918s | 25 | `test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js`
`test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js`
...
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js`
`test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 59.933s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Shavian.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Siddham.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Sidetic.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 59.472s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Old_Uyghur.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Oriya.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Osage.js` | -| built-ins/RegExp/property-escapes/generated | NORMAL | 59.432s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Palmyrene.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Pau_Cin_Hau.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Phags_Pa.js` | +| built-ins/decodeURIComponent | NORMAL | 161.263s | 1 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js` | +| built-ins/decodeURI | NORMAL | 145.859s | 1 | `test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js` | +| staging/sm | NORMAL | 100.749s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js` | +| staging/sm | NORMAL | 98.881s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js` | +| staging/sm | NORMAL | 96.500s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js` | +| staging/sm | NORMAL | 94.585s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js` | +| staging/sm | NORMAL | 91.265s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js` | +| staging/sm | NORMAL | 83.485s | 1 | `test262/test/staging/sm/TypedArray/sort_large_countingsort.js` | +| staging/sm | NORMAL | 81.532s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js` | +| staging/sm | NORMAL | 78.526s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js` | +| staging/sm | NORMAL | 77.426s | 1 | `test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js` | +| built-ins/decodeURI | NORMAL | 74.256s | 25 | `test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.12_T1.js`
...
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T1.js`
`test262/test/built-ins/decodeURI/S15.1.3.1_A1.8_T2.js` | +| built-ins/decodeURIComponent | NORMAL | 72.042s | 25 | `test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.12_T1.js`
...
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T1.js`
`test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.8_T2.js` | +| language/literals | NORMAL | 71.990s | 25 | `test262/test/language/literals/regexp/S7.8.5_A1.1_T1.js`
`test262/test/language/literals/regexp/S7.8.5_A1.1_T2.js`
...
`test262/test/language/literals/regexp/S7.8.5_A2.2_T2.js`
`test262/test/language/literals/regexp/S7.8.5_A2.3_T1.js` | +| staging/sm | NORMAL | 62.725s | 1 | `test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js` | +| intl402 | NORMAL | 62.168s | 22 | `test262/test/intl402/constructors-string-and-single-element-array.js`
`test262/test/intl402/constructors-taint-Object-prototype-2.js`
...
`test262/test/intl402/supportedLocalesOf-throws-if-element-not-string-or-object.js`
`test262/test/intl402/supportedLocalesOf-unicode-extensions-ignored.js` | +| intl402/DateTimeFormat | NORMAL | 60.538s | 25 | `test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js`
`test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js`
...
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/name.js`
`test262/test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.903s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hanifi_Rohingya.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hanunoo.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hatran.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.787s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tibetan.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tifinagh.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tirhuta.js` | +| built-ins/RegExp/property-escapes/generated | NORMAL | 59.667s | 3 | `test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kharoshthi.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Khitan_Small_Script.js`
`test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Khmer.js` | ## Runner Caveats diff --git a/compat.json b/compat.json index 55cb46aa..24c6b184 100644 --- a/compat.json +++ b/compat.json @@ -1,10 +1,10 @@ { - "generatedAt": "2026-10-25T10:12:22+00:00", - "updatedAt": "2026-10-25T10:12:22+00:00", + "generatedAt": "2026-10-25T10:38:39+00:00", + "updatedAt": "2026-10-25T10:38:39+00:00", "complete": true, "git": { "branch": "feat/react-hydration", - "commit": "f8905d39", + "commit": "194f0635", "isDirty": true }, "environment": { @@ -19,8 +19,8 @@ }, "state": { "directory": ".compat-state", - "fingerprint": "4aa5c611c9bb9f4096c7be91527d157724bd4055", - "runIdentity": "a0d30642b348b33d3b54cb703f1f397a0e9fa52ef6f6102c8d227a2ae2e7fd72" + "fingerprint": "974314a028e0dd34ea2ae73d89fa4eb934451513", + "runIdentity": "278a834f4bf51053e32f434fea52e857994d14c14767b778073250df87ad87ec" }, "discovery": { "groupCount": 161, @@ -2717,27 +2717,27 @@ "blockedChunks": [], "slowChunks": [ { - "group": "staging/sm", + "group": "built-ins/decodeURIComponent", "kind": "normal", - "durationSec": 178.996, + "durationSec": 161.263, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js" + "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js" ] }, { - "group": "staging/sm", + "group": "built-ins/decodeURI", "kind": "normal", - "durationSec": 178.664, + "durationSec": 145.859, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js" + "test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 178.023, + "durationSec": 100.749, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/Date/dst-offset-caching-7-of-8.js" @@ -2746,88 +2746,79 @@ { "group": "staging/sm", "kind": "normal", - "durationSec": 177.82, + "durationSec": 98.881, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-4-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 177.799, + "durationSec": 96.5, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-2-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 166.032, + "durationSec": 94.585, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js" - ] - }, - { - "group": "built-ins/decodeURIComponent", - "kind": "normal", - "durationSec": 160.372, - "filesCount": 1, - "sampleFiles": [ - "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A2.5_T1.js" + "test262/test/staging/sm/Date/dst-offset-caching-3-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 154.132, + "durationSec": 91.265, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-8-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 148.807, + "durationSec": 83.485, "filesCount": 1, "sampleFiles": [ "test262/test/staging/sm/TypedArray/sort_large_countingsort.js" ] }, { - "group": "built-ins/decodeURI", + "group": "staging/sm", "kind": "normal", - "durationSec": 148.468, + "durationSec": 81.532, "filesCount": 1, "sampleFiles": [ - "test262/test/built-ins/decodeURI/S15.1.3.1_A2.5_T1.js" + "test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 143.142, + "durationSec": 78.526, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/Date/dst-offset-caching-1-of-8.js" + "test262/test/staging/sm/Date/dst-offset-caching-5-of-8.js" ] }, { "group": "staging/sm", "kind": "normal", - "durationSec": 119.585, + "durationSec": 77.426, "filesCount": 1, "sampleFiles": [ - "test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js" + "test262/test/staging/sm/Date/dst-offset-caching-6-of-8.js" ] }, { "group": "built-ins/decodeURI", "kind": "normal", - "durationSec": 74.512, + "durationSec": 74.256, "filesCount": 25, "sampleFiles": [ "test262/test/built-ins/decodeURI/S15.1.3.1_A1.11_T2.js", @@ -2840,7 +2831,7 @@ { "group": "built-ins/decodeURIComponent", "kind": "normal", - "durationSec": 74.306, + "durationSec": 72.042, "filesCount": 25, "sampleFiles": [ "test262/test/built-ins/decodeURIComponent/S15.1.3.2_A1.11_T2.js", @@ -2851,75 +2842,84 @@ ] }, { - "group": "built-ins/TypedArray", + "group": "language/literals", "kind": "normal", - "durationSec": 72.127, + "durationSec": 71.99, "filesCount": 25, "sampleFiles": [ - "test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached-prototype.js", - "test262/test/built-ins/TypedArray/prototype/copyWithin/coerced-values-end-detached.js", + "test262/test/language/literals/regexp/S7.8.5_A1.1_T1.js", + "test262/test/language/literals/regexp/S7.8.5_A1.1_T2.js", "...", - "test262/test/built-ins/TypedArray/prototype/copyWithin/not-a-constructor.js", - "test262/test/built-ins/TypedArray/prototype/copyWithin/prop-desc.js" + "test262/test/language/literals/regexp/S7.8.5_A2.2_T2.js", + "test262/test/language/literals/regexp/S7.8.5_A2.3_T1.js" ] }, { - "group": "intl402/DateTimeFormat", + "group": "staging/sm", "kind": "normal", - "durationSec": 66.659, - "filesCount": 12, + "durationSec": 62.725, + "filesCount": 1, "sampleFiles": [ - "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js", - "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js", + "test262/test/staging/sm/TypedArray/element-setting-converts-using-ToNumber.js" + ] + }, + { + "group": "intl402", + "kind": "normal", + "durationSec": 62.168, + "filesCount": 22, + "sampleFiles": [ + "test262/test/intl402/constructors-string-and-single-element-array.js", + "test262/test/intl402/constructors-taint-Object-prototype-2.js", "...", - "test262/test/intl402/DateTimeFormat/prototype/formatToParts/dangi-calendar-dates.js", - "test262/test/intl402/DateTimeFormat/prototype/formatToParts/date-constructor-not-called.js" + "test262/test/intl402/supportedLocalesOf-throws-if-element-not-string-or-object.js", + "test262/test/intl402/supportedLocalesOf-unicode-extensions-ignored.js" ] }, { - "group": "annexB/built-ins/RegExp", + "group": "intl402/DateTimeFormat", "kind": "normal", - "durationSec": 63.918, + "durationSec": 60.538, "filesCount": 25, "sampleFiles": [ - "test262/test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js", - "test262/test/annexB/built-ins/RegExp/RegExp-decimal-escape-class-range.js", + "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plainmonthday-formatting-timezonename.js", + "test262/test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-plaintime-formatting-timezonename.js", "...", - "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js", - "test262/test/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js" + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/name.js", + "test262/test/intl402/DateTimeFormat/prototype/formatToParts/offset-timezone-correct.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 59.933, + "durationSec": 59.903, "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Shavian.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Siddham.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Sidetic.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hanifi_Rohingya.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hanunoo.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Hatran.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 59.472, + "durationSec": 59.787, "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Old_Uyghur.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Oriya.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Osage.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tibetan.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tifinagh.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Tirhuta.js" ] }, { "group": "built-ins/RegExp/property-escapes/generated", "kind": "normal", - "durationSec": 59.432, + "durationSec": 59.667, "filesCount": 3, "sampleFiles": [ - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Palmyrene.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Pau_Cin_Hau.js", - "test262/test/built-ins/RegExp/property-escapes/generated/Script_-_Phags_Pa.js" + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Kharoshthi.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Khitan_Small_Script.js", + "test262/test/built-ins/RegExp/property-escapes/generated/Script_Extensions_-_Khmer.js" ] } ] From b276e9e16a6f0f9c68cc15b0a604b315a8297e85 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Sun, 25 Oct 2026 11:47:31 +0100 Subject: [PATCH 8/8] Make framework compatibility permanently local-only [skip ci] --- .github/workflows/ci.yml | 3 + .github/workflows/frameworks.yml | 143 ----------------------------- .github/workflows/release.yml | 3 + AGENTS.md | 10 +- CHANGELOG.md | 1 + README.md | 4 +- docs/modules.md | 8 +- tests/Frameworks/README.md | 19 ++++ tests/Frameworks/express/run.py | 2 +- tests/Frameworks/hono/README.md | 8 +- tests/Frameworks/hono/run.py | 2 +- tests/Frameworks/http2/interop.mjs | 1 + 12 files changed, 49 insertions(+), 155 deletions(-) delete mode 100644 .github/workflows/frameworks.yml create mode 100644 tests/Frameworks/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12abede0..dbd3c2d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI +# Framework compatibility is local-only and must never run in any CI job. +# Do not restore frameworks.yml. See tests/Frameworks/README.md. + on: push: branches: [main] diff --git a/.github/workflows/frameworks.yml b/.github/workflows/frameworks.yml deleted file mode 100644 index c75771a1..00000000 --- a/.github/workflows/frameworks.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Framework Compatibility - -on: - push: - branches: [main] - paths: - - 'src/**' - - 'tests/Frameworks/**' - - 'scripts/node-*/**' - - 'composer.json' - - 'composer.lock' - - '.github/workflows/frameworks.yml' - pull_request: - branches: [main] - paths: - - 'src/**' - - 'tests/Frameworks/**' - - 'scripts/node-*/**' - - 'composer.json' - - 'composer.lock' - - '.github/workflows/frameworks.yml' - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: frameworks-${{ github.ref }} - cancel-in-progress: true - -env: - XDEBUG_MODE: 'off' - TZ: UTC - -jobs: - upstream: - name: PHP ${{ matrix.php }} / ${{ matrix.suite }} - runs-on: ubuntu-latest - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - php: ['8.4', '8.5'] - suite: [express, hono-main, hono-jsx-runtime-default, hono-jsx-runtime-dom, hono-node, http2] - steps: - - uses: actions/checkout@v5 - - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: mbstring, bcmath, curl, intl, openssl, sodium, zlib, sockets - coverage: none - - - uses: actions/setup-node@v6 - with: - node-version: '26.12.0' - cache: npm - cache-dependency-path: tests/Frameworks/package-lock.json - - - uses: actions/setup-python@v6 - with: - python-version: '3.13' - - - name: Install pinned dependencies - run: | - composer install --no-interaction --prefer-dist - npm ci --prefix tests/Frameworks - - - name: Prepare Express source - if: matrix.suite == 'express' - run: python3 -B tests/Frameworks/express/setup.py - - - name: Prepare Hono source - if: startsWith(matrix.suite, 'hono-') - run: python3 -B tests/Frameworks/hono/setup.py - - - name: Express original tests - if: matrix.suite == 'express' - run: | - python3 -B tests/Frameworks/express/test_runner.py - python3 -B tests/Frameworks/express/run.py --runtime both --jobs 1 --timeout-scale 10 --output framework-results/express - - - name: Hono harness deadline differential - if: startsWith(matrix.suite, 'hono-') - run: | - python3 -B tests/Frameworks/hono/test_runner.py - python3 -B tests/Frameworks/hono/selftest.py - - - name: Hono original runtime tests - if: startsWith(matrix.suite, 'hono-') - env: - FRAMEWORK_SUITE: ${{ matrix.suite }} - run: python3 -B tests/Frameworks/hono/run.py --project "${FRAMEWORK_SUITE#hono-}" --timeout 1800 --timeout-scale 10 - - - name: Hono independent native Vitest baseline - if: startsWith(matrix.suite, 'hono-') && !cancelled() - env: - FRAMEWORK_SUITE: ${{ matrix.suite }} - run: | - mkdir -p framework-results - if [ "$FRAMEWORK_SUITE" = 'hono-main' ]; then - node tests/Frameworks/hono/prepare-toolchain.mjs - fi - PHASIS_HONO_PROJECT="${FRAMEWORK_SUITE#hono-}" node tests/Frameworks/node_modules/vitest/vitest.mjs run \ - --config tests/Frameworks/hono/vitest.config.mjs \ - --reporter=json --outputFile="$PWD/framework-results/hono-native.json" - - - name: Compare every Hono case with native Vitest - if: startsWith(matrix.suite, 'hono-') && !cancelled() - run: | - python3 -B - <<'PY' - import json - from pathlib import Path - import subprocess - results = Path('tests/Frameworks/hono/results') - run_id = json.loads((results / 'manifest.json').read_text())['id'] - manifest = results / 'runs' / run_id / 'manifest.json' - statuses = [subprocess.run([ - 'python3', '-B', 'tests/Frameworks/hono/compare-native.py', - 'framework-results/hono-native.json', str(manifest), '--runtime', runtime, - ]).returncode for runtime in ['node', 'phasis']] - raise SystemExit(int(any(statuses))) - PY - - - name: Hono original declaration tests - if: matrix.suite == 'hono-main' && !cancelled() - run: python3 -B tests/Frameworks/hono/run-toolchain.py - - - name: HTTP2 native Node interoperability - if: matrix.suite == 'http2' - run: | - mkdir -p framework-results - node tests/Frameworks/http2/interop.mjs | tee framework-results/http2.jsonl - - - name: Preserve complete manifests and failure logs - if: always() - uses: actions/upload-artifact@v6 - with: - name: frameworks-php${{ matrix.php }}-${{ matrix.suite }}-${{ github.sha }} - path: | - framework-results/ - tests/Frameworks/hono/results/runs/ - if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b157b6d4..5b07546a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,8 @@ name: Release +# Framework compatibility must never run in CI and is not a release gate. +# See tests/Frameworks/README.md for the owner's local-only policy. + on: release: types: [published] diff --git a/AGENTS.md b/AGENTS.md index 31c31838..3a6af123 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,9 +15,13 @@ run test oracles, but must never become an execution backend. Explicit through `process.execPath` runs the Phasis PHP entry point. New host resources must register cleanup with their owning NodeRuntime. Keep original framework assertions, report all discovered cases and upstream skips, and retain source -identity for complete-run claims. Express and Hono support is experimental; -their CI runners explicitly use `--timeout-scale 10` while performance work is -deferred. Preserve original timeout values and record effective limits in the +identity for complete-run claims. Express and Hono support is experimental. +Framework compatibility suites are local-only and must NEVER run in CI, +including manual workflow dispatches, pull requests, pushes or release gates. +Do not restore the removed `frameworks.yml` workflow or dispatch old revisions. +The owner's 2026-10-25 decision explicitly permits releases without this suite. +See [the framework policy](tests/Frameworks/README.md). Preserve original +timeout values and record effective limits in the results; a scaled run is a functional compatibility result, not a claim to meet upstream performance deadlines. Never relax test262 assertions, coverage or timeouts to accommodate framework workloads. Close and reap owned test diff --git a/CHANGELOG.md b/CHANGELOG.md index afa285a1..e52a60b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Changed - Release download archives use the same export-ignore runtime inventory as Composer before installing their bundled production autoloader. +- Framework compatibility is local-only and must never run in CI or gate a release. Its GitHub Actions workflow is removed and disabled. ### Fixed - Captured data preserves all public JSON keys, including names resembling native slots and leading null bytes. Native metadata readers keep those objects unbranded, so a public `[[PrimitiveValue]]` key cannot make them behave as boxed values. diff --git a/README.md b/README.md index 64beaa8a..461349de 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ composer cs # Full quality gate (PHPStan + PHPCS + PHPUnit + oracle) ./bin/verify-all -# Original Express and Hono suites, with pinned development dependencies +# Local-only Express and Hono suites. NEVER run these in CI. npm ci --prefix tests/Frameworks python3 tests/Frameworks/express/setup.py python3 tests/Frameworks/hono/setup.py @@ -128,7 +128,7 @@ python3 tests/Frameworks/hono/run.py --timeout-scale 10 ./bin/test262 --category built-ins/Array --jobs 4 ``` -Express and Hono support is experimental. Framework CI uses ten times the upstream test timeouts while retaining the original assertions and recording both timeout values. Omit `--timeout-scale 10` to check the original deadlines. This setting does not change test262 limits or engine execution budgets. +Express and Hono support is experimental. **Framework compatibility suites must never run in CI**, including manual dispatches or release gates. The framework workflow is removed and disabled; releases do not require these suites. See the [local-only framework policy](./tests/Frameworks/README.md). Local runs may use `--timeout-scale 10` while retaining the original assertions and recording both timeout values. Omit it to check the original deadlines. This setting does not change test262 limits or engine execution budgets. CI runs PHPStan, PHPCS, the unit suite and the oracle regression on every push to `main` and every pull request; the Web Platform Tests and popular-package suites run when `src/` or their fixtures change. The full test262 matrix (`compat-matrix.yml`) is started by hand, not on every push; it runs against the test262 revision pinned by the submodule and commits the result to `COMPAT.md`, which records the snapshot time. diff --git a/docs/modules.md b/docs/modules.md index 8d4e2950..4b1c9fe9 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -142,8 +142,12 @@ failures, blocked tests, source hashes and process timeouts. Framework versions and suite revisions are recorded there; passing a framework suite does not imply that the complete Node test suite or Hono's Bun/Deno/Fastly/workerd hosts pass. -Framework CI passes `--timeout-scale 10` to the Express and Hono runners so slow -PHP workloads can complete their original assertions. Reports retain the +Framework compatibility suites are **local-only and must never run in CI**, +including manual workflow dispatches or release gates. The workflow is removed +and disabled; releases do not require a passing framework suite. + +For local investigation, pass `--timeout-scale 10` to the Express and Hono +runners so slow PHP workloads can complete their original assertions. Reports retain the original and effective timeouts; scaled results do not establish upstream performance parity. The default scale is 1 for checking the original deadlines. This configuration does not affect test262 or engine execution budgets. diff --git a/tests/Frameworks/README.md b/tests/Frameworks/README.md new file mode 100644 index 00000000..a200438a --- /dev/null +++ b/tests/Frameworks/README.md @@ -0,0 +1,19 @@ +# Local-only framework compatibility + +**Never run these suites in CI.** This includes Express, Hono and the associated +HTTP/2 compatibility checks, through automatic triggers, manual dispatches, +release gates or workflows from old revisions. + +The owner made this decision on 2026-10-25. The Framework Compatibility workflow +is removed from this repository and disabled in GitHub Actions. Do not restore +it, dispatch it, or add these runners to another workflow. A cancelled or absent +framework check does not block merging or releasing. + +Keep the runners for deliberate local investigation. Preserve original +assertions, source identities, upstream skips, original/effective deadlines and +owned-process cleanup. A local timeout scale is a functional compatibility +check, not a performance-parity claim. The main README and Hono guide document +the local commands. + +Engine unit tests, oracle regressions, JSX/hydration, WPT, popular packages, +test262 and dependency/quality checks retain their existing CI roles. diff --git a/tests/Frameworks/express/run.py b/tests/Frameworks/express/run.py index a1b4e9c4..be3cda71 100644 --- a/tests/Frameworks/express/run.py +++ b/tests/Frameworks/express/run.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Run the complete pinned Express suite with bounded, isolated worker groups.""" +"""Local-only Express compatibility. Never run in CI; see ../README.md.""" import argparse import concurrent.futures diff --git a/tests/Frameworks/hono/README.md b/tests/Frameworks/hono/README.md index 40ff46ac..1814fce6 100644 --- a/tests/Frameworks/hono/README.md +++ b/tests/Frameworks/hono/README.md @@ -1,7 +1,9 @@ # Experimental Hono compatibility -Hono support is experimental. CI checks functional compatibility with an explicit -timeout scale of 10; meeting the original performance deadlines is deferred. +Hono support is experimental. These suites are **local-only and must never run +in CI**, including manual dispatches or release gates. See the +[framework policy](../README.md). Meeting the original performance deadlines +is deferred. The ignored source tree in `upstream/hono-4.13.13` is the unchanged Hono v4.13.13 archive at commit `08a023cbfde55b434fb0fb30fae35d42e4bf20aa`. The repository, @@ -35,7 +37,7 @@ runs both and compares every discovered case's identity and outcome. Each file runs in a separate process, sequentially, with a 1,800 second outer wall limit (`--timeout` overrides it). This lets large files finish hundreds of cases while retaining the original per-test and per-hook deadlines by default. -Pass `--timeout-scale 10` to match CI, or set +For a local scaled run, pass `--timeout-scale 10`, or set `PHASIS_FRAMEWORK_TIMEOUT_SCALE=10`; an explicit CLI value takes precedence. The scale applies equally to the Node harness and Phasis, and reports record the scale plus each case's original and effective timeout. The outer worker diff --git a/tests/Frameworks/hono/run.py b/tests/Frameworks/hono/run.py index 6671e5c2..75022d20 100644 --- a/tests/Frameworks/hono/run.py +++ b/tests/Frameworks/hono/run.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Run unchanged transpiled Hono tests in bounded Node/Phasis subprocesses.""" +"""Local-only Hono compatibility. Never run in CI; see ../README.md.""" import argparse import json import hashlib diff --git a/tests/Frameworks/http2/interop.mjs b/tests/Frameworks/http2/interop.mjs index aa3d2663..30fad4a2 100644 --- a/tests/Frameworks/http2/interop.mjs +++ b/tests/Frameworks/http2/interop.mjs @@ -67,3 +67,4 @@ try { for (const child of running) child.kill('SIGKILL'); rmSync(materialDirectory, { recursive: true, force: true }); } +// Local-only framework interoperability. Never run in CI; see ../README.md.