본문으로 건너뛰기

알려진 제한 사항

Phasis은 관찰 가능한 모든 명세 테스트에서 V8과 동일하게 동작하는 것을 목표로 합니다. 이 페이지에서는 실제 사용에서 그 동일성이 깨지는 부분을 설명합니다. 대개 트리를 순회하는 PHP 인터프리터와 JIT 컴파일되는 C++ 엔진 사이의 근본적인 차이 때문입니다.

성능 상한

이 엔진은 트리 워커와 기회주의적 바이트코드 VM, 그리고 JsToPhp 트랜스파일러로 구성됩니다. 이 트랜스파일러는 숫자 연산이나 로컬 변수 사용이 많은 본문(obj.method(), new Ctor(), this.member 형태 포함)을 네이티브 PHP 클로저로 변환하고, PHP 8.5의 추적 JIT가 이를 기계어로 컴파일할 수 있습니다. 대부분의 핫 디스패치는 VM의 스위치 루프 내부나 JIT 컴파일된 PHP 안에서 직접 처리되므로 AST를 순회하지 않습니다. 하지만 VM 연산 코드마다 여전히 PHP 디스패치 비용이 들고, JsToPhp가 아직 처리하지 못하는 형태는 더 느린 VM으로 되돌아갑니다. 디스패치가 병목인 작업의 성능 상한은 V8보다 약 100배 느린 수준입니다.

임베딩 작업, 예를 들어 사용자가 제공한 로직으로 PHP 데이터를 처리하는 일, 콘텐츠 변환 파이프라인, 유효성 검사 규칙에서는 이 상한이 문제가 되는 경우가 드뭅니다. V8은 100ms이고, Phasis는 10초이므로 둘 다 요청 시간 제한보다 훨씬 짧습니다. 격차를 더 줄이려면 JsToPhp가 더 많은 형태(매개변수 타입 추론, await / yield* 변환)를 처리하도록 하거나 eval()를 거치지 않고 opcache opcode를 직접 출력해야 합니다. 벤치마크 § 로드맵을 참조하세요.

멀티 에이전트 동시성

JavaScript의 SharedArrayBuffer + Atomics.wait + postMessage 모델은 호스트가 실제 OS 스레드를 실행할 수 있다고 가정합니다. PHP은 그렇게 할 수 없습니다(모든 호스트에서 사용할 수 없는 pcntl_fork 없이는 불가능합니다).

Phasis은 단일 에이전트를 시뮬레이션합니다. Atomics.wait은 즉시 "not-equal" 또는 "timed-out"을 반환하고, notify는 항상 0을 반환합니다. 단일 에이전트로 실행되는 모든 test262 테스트(입력 유효성 검사, 오류 경로, 알림 없음)는 통과하며, 이는 376 Atomics 테스트의 376을 차지합니다.

서로를 관찰하는 두 에이전트의 동시 실행이 필요한 테스트는 지원하지 않습니다. 실제 병렬 처리가 필요하면 PHP 수준에서 작업자 큐(Redis, Beanstalkd, …)를 사용하세요.

Realm 의미론

ShadowRealm은 구현되어 있지만 각 하위 realm은 호스트와 동일한 PHP 프로세스에서 실행됩니다. 호스트 함수가 realm 사이에 참조를 누출할 수 있으므로 PHP 코드를 방화벽으로 격리해야 합니다. 명세는 이를 허용하지만 V8은 별도의 v8::Isolate 인스턴스로 더 엄격하게 격리합니다.

createRealm()(test262 호스트 훅)은 내부적으로 새 Phasis\Engine를 만들고 표준 라이브러리를 연결합니다.

정규식 엔진 선택

Phasis은 기본 정규식 백엔드로 PHP의 PCRE2를 사용하고, PCRE2가 정확히 처리하지 못하는 경우(캡처가 포함된 후방 탐색, 리셋이 있는 반복 그룹, 특정 /v 플래그 의미론)를 위한 맞춤형 매처를 사용합니다. 맞춤형 매처는 PCRE2보다 느리며, 디스패치에서 호환되지 않는 패턴이 감지될 때만 사용됩니다.

주목할 만한 결과는 PCRE2의 대소문자 접기 테이블이 V8의 ICU 버전과 다를 수 있다는 점입니다. 특히 오래된 Linux 배포판에서 그렇습니다(Ubuntu 22.04에는 ICU 70이 포함되고, Phasis은 /iu 패턴에서 호스트 환경에 따른 차이를 덮어쓰도록 ICU 76 이상의 접기 테이블을 포함합니다).

메모리 모델

PHP의 참조 카운팅 GC는 V8의 추적 수집기와 다릅니다. 호스트 클로저가 캡처한 PHP 객체는 엔진이 해당 클로저를 보유하는 동안 유지되며, 객체 간 순환 참조는 Engine이 파괴될 때까지 정리되지 않습니다. 캐시에서 유지하고 싶지 않은 항목에는 WeakMap / WeakRef를 사용하세요.

process.memoryUsage() 및 이와 유사한 V8 전용 내부 검사 API는 구현되어 있지 않습니다.

모듈 로더

ES 모듈 로더는 파일 시스템을 기준으로 상대 경로 import를 확인하지만 번들러 규칙은 이해하지 못합니다:

  • node_modules 해석을 지원하지 않습니다.
  • package.json exports 맵을 지원하지 않습니다.
  • 파일 확장자를 추론하지 않습니다(import './foo'가 아니라 import './foo.mjs'라고 작성해야 합니다).
  • HTTP import를 지원하지 않습니다.

이는 기본 ECMAScript 명세에는 부합하지만 Node의 동작과는 다릅니다. 사용자 지정 모듈 확인을 위한 훅은 없습니다. npm 패키지를 사용하려면 esbuild 같은 도구로 단일 스크립트나 모듈로 번들링한 뒤 Phasis에 전달하세요.

Wasm

WebAssembly은 구현되어 있지 않습니다. 연결할 PHP wasm 런타임이 없습니다.

지원하지 않는 브라우저 전용 웹 API

Phasis은 전체 Web Platform Pack(URL, TextEncoder/TextDecoder, atob/btoa, structuredClone, performance, DOMException), 전체 Fetch Pack(fetch, Request, Response, Headers, Body, AbortController/AbortSignal, Blob/File, FormData, EventTarget/Event, 전체 WHATWG Streams, navigator), 그리고 crypto(getRandomValues, randomUUID, 전체 SubtleCrypto: digest, HMAC, AES-GCM/CBC/CTR, RSA-OAEP/PSS/PKCS1, ECDSA, ECDH, HKDF, PBKDF2), WebSocket(생성자 + RFC 6455 프레임 코덱, 교체 가능한 전송 계층), XMLHttpRequest(fetch 전송 계층 기반), 실제 이벤트 루프(setTimeout, setInterval, clearTimeout, clearInterval, queueMicrotask, AsyncContext)를 제공합니다. 웹 API를 참조하세요.

제공하지 않는 기능:

  • window, document, DOM 트리 — Phasis은 브라우저가 아닙니다.
  • localStorage, sessionStorage, IndexedDB — 호스트 측 기능이며 영속성은 임베더가 담당합니다.
  • Worker, SharedWorker, MessageChannel — 실제 OS 스레드가 필요하지만 PHP은 이를 제공하지 않습니다.
  • Service Workers, Push API, Notifications, BroadcastChannel — 브라우저 밖에서는 쓸모가 없습니다.

Node 전역 객체

process, Buffer, __dirname, __filename, require() — 이 중 어느 것도 제공되지 않습니다. 이는 언어 기능이 아니라 Node 호스트의 규칙입니다.

Node 코드를 포팅하는 경우 require('foo')을 import 'foo'으로 바꾸고, process.env.X를 호스트에 연결된 env.X로 교체한 뒤, PHP에서 __dirname에 해당하는 경로를 전달하세요.

소수 정밀도

ECMAScript 숫자는 IEEE 754 배정밀도 부동소수점 수입니다. 유효 숫자가 15자리를 넘는 십진 연산은 다른 모든 JS 엔진과 마찬가지로 정밀도를 잃습니다. 정확한 정수 연산에는 BigInt을 사용하고, 정확한 소수 연산이 필요하면 JS 십진 라이브러리를 사용하세요.

PHP의 bcmath은 내부적으로 BigInt에 사용됩니다. V8과 실제로 다른 점은 매우 큰 bigint 연산이 V8의 C++ 구현보다 PHP에서 느리다는 것입니다. 수치 정확성은 동일합니다.