본문으로 건너뛰기

아키텍처

Phasis는 기회가 있을 때 바이트코드 VM을 사용하는 4단계 트리 워커입니다. 데이터 흐름은 다음과 같습니다:

코드
JS text
  │
  ▼
┌──────────┐
│  Lexer   │  src/Lexer/
└────┬─────┘
     ▼ tokens
┌──────────┐
│  Parser  │  src/Parser/
└────┬─────┘
     ▼ AST nodes
┌──────────────┐
│ Interpreter  │  src/Runtime/
│  + Bytecode  │  src/Bytecode/
│      VM      │
└────┬─────────┘
     ▼ JsValue
PHP host

각 단계는 다음 단계가 사용하는 작은 표면을 가진 PHP 클래스입니다. 링크 단계는 없습니다. 파싱 결과로 AST가 생성되고 인터프리터가 이를 직접 순회합니다.

렉서

src/Lexer/Lexer.php는 소스 텍스트를 Token 인스턴스 스트림으로 변환합니다. 각 토큰은 읽기 전용 구조체이며 타입(TokenType 열거형, 약 80개 사례), 값, 줄, 열을 포함합니다.

렉서는 다음을 처리합니다:

  • 숫자 리터럴 — 10진수, 16진수(0x), 8진수(0o), 2진수(0b), 지수 표기, 숫자 구분자.
  • 문자열 리터럴 — 작은따옴표와 큰따옴표, 모든 이스케이프 시퀀스, 중첩 표현식이 포함된 템플릿 리터럴.
  • 정규 표현식 리터럴 — /pattern/flags 토큰화 전체를 지원하며, /를 정규식으로 볼지 나눗셈으로 볼지 판단합니다.
  • 식별자, 키워드, 구두점.
  • 주석(한 줄 및 블록)은 트리비아로 보존됩니다.
  • Unicode 소스 — UTF-8 바이트를 JS UTF-16 소스로 해석합니다.

출력은 토큰을 지연 생성하는 제너레이터입니다. 파서는 한 토큰을 미리 읽어 이를 소비합니다.

파서

src/Parser/Parser.php는 직접 작성한 Pratt 파서입니다. 식의 우선순위는 src/Parser/Precedence.php에 정의되어 있습니다. 문은 자체 재귀 하강 방식으로 처리됩니다.

src/Ast/에서 불변 AST 노드를 생성합니다. 각 노드에는 스택 추적과 오류 보고를 위한 소스 위치가 포함됩니다.

주요 구조:

  • 표현식: Literal, Identifier, BinaryExpression, UnaryExpression, AssignmentExpression, CallExpression, MemberExpression, ArrowFunction, ObjectExpression, ArrayExpression, ConditionalExpression, TemplateLiteral, SpreadElement, NewExpression, ThisExpression, SequenceExpression, TaggedTemplate, ClassExpression, YieldExpression, AwaitExpression.
  • 문: BlockStatement, IfStatement, ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoWhileStatement, SwitchStatement, ReturnStatement, ThrowStatement, TryStatement, BreakStatement, ContinueStatement, ExpressionStatement.
  • 선언: VariableDeclaration, FunctionDeclaration, ClassDeclaration, ImportDeclaration.
  • 패턴(구조 분해): ArrayPattern, ObjectPattern, RestElement, AssignmentPattern.

자동 세미콜론 삽입은 사양에 따라 렉서가 아닌 파서에서 구현됩니다.

인터프리터

src/Runtime/Interpreter.php는 AST 워커입니다. 각 노드를 방문해 JsValue를 반환하거나 제어 흐름을 변경하는 문에 대해서는 Completion를 반환합니다.

주요 지원 클래스:

  • Environment (src/Runtime/Environment.php) — 렉시컬 스코프 체인입니다. 함수 호출마다 렉시컬 부모를 가리키는 새 환경을 만듭니다. var는 함수 스코프로 호이스팅되고, let / const는 TDZ가 적용되는 블록 스코프입니다.
  • CallStack (src/Runtime/CallStack.php) — 깊이 제한이 있는 명시적 스택입니다. 함수와 메서드 호출은 프레임을 푸시하고, 예외가 발생하면 프레임이 해제됩니다.
  • Completion (src/Runtime/Completion.php) — normal, return, throw, break, continue의 판별 유니온과 선택적 값 및 레이블입니다.
  • Reference (src/Runtime/Reference.php) — 할당과 delete에서 좌변을 평가하는 사양의 Reference 타입입니다.

값

JavaScript 값은 src/Value/ 아래의 PHP 객체입니다. 각각 JsValue 인터페이스를 구현하고 자체 타입 변환 메서드(toNumber(), toString(), toBoolean())를 가집니다.

클래스 JS 타입
JsUndefined undefined(싱글턴)
JsNull null(싱글턴)
JsBoolean true / false
JsNumber IEEE 754 배정밀도. NaN, ±Infinity, ±0 보존
JsString UTF-16 문자열 의미 체계
JsObject 속성 맵과 프로토타입 체인을 가진 기본 객체
JsArray 길이 추적 기능을 포함하도록 JsObject 확장
JsFunction 캡처한 Environment가 있는 클로저
JsSymbol Symbol 원시 값
JsBigInt bcmath를 통한 임의 정밀도
JsProxy 13개 트랩 모두 지원, 취소 가능
JsPromise 동기 실행기 + 마이크로태스크 큐
JsGenerator 이중 모드: 프레임 스냅샷(바이트코드로 컴파일된 본문. 바이트코드 VM § 프레임 스냅샷 제너레이터 참조)이며, yield*, yield가 포함된 try, 비동기 제너레이터에는 PHP Fiber 대체 방식을 사용합니다
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry 컬렉션
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray 바이너리 데이터

숫자 처리는 역사적으로 버그가 가장 많이 발생한 영역입니다. NaN !== NaN는 참이어야 하고 -0 === 0도 참이어야 하며, ToNumber의 경계 사례는 비트 수준까지 정확히 처리해야 합니다. Spec/TypeConversion 클래스는 사양 알고리즘을 그대로 구현하며 철저한 테스트를 거쳤습니다.

내장 라이브러리

src/BuiltIn/는 모든 표준 라이브러리 객체(Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console)와 웹 플랫폼 및 Fetch 팩(URL, Headers, Request, Response, Blob, FormData, 전체 Streams, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest, 이벤트 루프(setTimeout, setInterval, queueMicrotask, AsyncContext)를 제공합니다.

각 내장 기능은 클래스의 정적 install() 메서드입니다. Engine은 생성 시 의존성 순서대로 호출합니다. 각 기능은 프로토타입 체인을 정의하고, 메서드를 PHP 클로저로 감싼 JsFunction 인스턴스로 설치하며, 생성자를 전역으로 노출합니다.

PHP↔JS 브리지

경계를 넘는 값 변환은 두 곳에서 처리됩니다:

  • Phasis\Interop\PhpToJs::convert() — PHP 값을 JsValue로 변환합니다. setGlobal(), call() 인수, 호스트 함수 반환값에 사용됩니다. PHP 호출 가능 객체는 본문에서 이를 호출하는 JsFunction가 됩니다.
  • PhpToJs::toPhp() — 호스트 함수 인수를 위해 JsValue를 PHP로 변환합니다.
  • Engine::toPhp()(비공개) — eval() 및 call()의 결과를 위해 JsValue을 PHP로 변환합니다.

객체 브리지는 없습니다. 호출 가능 객체가 아닌 PHP 객체는 undefined로 변환됩니다(이슈 #18). src/Bytecode/의 JsToPhp 클래스는 상호 운용과 관련이 없으며, 아래에서 설명하는 트랜스파일러입니다. 변환 규칙은 값 변환에 설명되어 있습니다.

오류 처리

처리되지 않은 JS throw는 PHP Phasis\Exceptions\JsThrowable가 됩니다. 엔진에서 직접 발생하는 오류는 모두 Phasis\Exceptions\RuntimeError의 하위 클래스인 Phasis\Exceptions\TypeError, RangeError 또는 ReferenceError으로 표시됩니다. 엔진 내부에서는 JS try / catch가 JsThrowable, 모든 RuntimeError, Phasis\Exceptions\SyntaxError을 처리합니다. 호스트 함수에서 발생한 예외를 포함한 그 밖의 PHP 예외는 변환되지 않고 PHP 호출자에게 그대로 전파됩니다(이슈 #20).

제한은 두 가지입니다. 호출 스택 한도(CallStack::maxDepth, 4,096 프레임과 트랜스파일된 클로저용 8,192 프레임 가드)와 maxLoopIterations입니다. Engine::setLimit()를 통해 설정하며 트리 워커만 적용합니다(이슈 #19). 둘 다 Phasis\Exceptions\InternalError를 발생시킵니다. 문자열 길이, 출력 크기 또는 실행 시간 제한은 없습니다.

바이트코드 VM

핫 함수는 두 가지 방식으로 변환됩니다. JsToPhp(src/Bytecode/ 내)은 숫자 연산 또는 로컬 변수 사용이 많은 본문에 PHP 클로저를 생성하며, PHP의 추적 JIT는 이를 네이티브 코드로 컴파일할 수 있습니다. 파이프라인은 new Ctor(args), obj.method(args), this.member 읽기를 변환하므로, 일반적인 루프 안의 클래스 메서드 패턴은 VM을 완전히 우회할 수 있습니다. 나머지는 모두 평면 바이트코드로 컴파일되어 VM::execute에서 처리됩니다. 사용자 지정 호출 스택은 모든 JS 호출의 상태를 단일 PHP 프레임에 유지하며, 프로토타입 메서드 읽기용 PC별 인라인 캐시를 사용합니다. 간단한 제너레이터 본문도 바이트코드로 컴파일되며, Op::YIELD은 PHP Fiber를 거치는 대신 힙 프레임 스냅샷을 캡처합니다.

컴파일러가 변환하지 않는 형태(e.g. with, 다시 바인딩하는 catch가 있는 try, yield*, 비동기 제너레이터)는 트리 워커로 대체 처리됩니다. 전체 메커니즘은 바이트코드 VM을 참조하세요.