아키텍처
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을 참조하세요.