Перейти до вмісту

Архітектура

Phasis — це чотириетапний обхід дерева з віртуальною машиною байткоду, яка за нагоди застосовується. Потік даних:

Код
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 варіантів), значення, рядок, стовпець.

Лексер обробляє:

  • Числові літерали — десяткові, шістнадцяткові (0x), вісімкові (0o), двійкові (0b), експоненційні, з роздільниками чисел.
  • Рядкові літерали — одинарні й подвійні лапки, усі керівні послідовності, шаблонні рядки з вкладеними виразами.
  • Літерали регулярних виразів — повна токенізація /pattern/flags з урахуванням того, чи має / бути регулярним виразом або діленням.
  • Ідентифікатори, ключові слова, пунктуація.
  • Коментарі (рядкові й блокові), що зберігаються як тривіальні елементи.
  • Джерело Unicode — байти UTF-8, інтерпретовані як джерело JS UTF-16.

Результатом є генератор, який ліниво видає токени. Парсер споживає їх, заглядаючи на один токен наперед.

Парсер

src/Parser/Parser.php — написаний вручну парсер Pratt. Пріоритетність виразів задається в src/Parser/Precedence.php. Для інструкцій використовується власний рекурсивний спуск.

Він створює незмінні вузли AST з src/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) — тип Reference зі специфікації для обчислення лівої частини присвоювань і delete.

Значення

Значення JavaScript — це об’єкти PHP у середовищі src/Value/. Кожен із них реалізує інтерфейс JsValue і має власні методи перетворення типів (toNumber(), toString(), toBoolean()).

Клас Тип JS
JsUndefined undefined (одиночний екземпляр)
JsNull null (одиночний екземпляр)
JsBoolean true / false
JsNumber IEEE 754 double; зберігає NaN, ±Infinity, ±0
JsString семантика рядків UTF-16
JsObject базовий об’єкт із мапою властивостей і ланцюжком прототипів
JsArray розширює JsObject, відстежуючи довжину
JsFunction замикання зі збереженим Environment
JsSymbol примітив Symbol
JsBigInt довільна точність через bcmath
JsProxy усі 13 перехоплень, із можливістю відкликання
JsPromise синхронний виконавець + черга мікрозавдань
JsGenerator два режими: знімок кадру (тіла, скомпільовані в байткод; див. Віртуальна машина байткоду § Генератори зі знімками кадрів) з резервним варіантом на основі PHP Fiber для yield*, try-with-yield і асинхронних генераторів
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry колекції
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray двійкові дані

Обробка чисел — найбільше джерело історичних помилок: NaN !== NaN має бути true, -0 === 0 має бути true, а крайові випадки ToNumber потребують точної побітової обробки. Клас Spec/TypeConversion дослівно реалізує алгоритми специфікації та пройшов вичерпне тестування.

Вбудована бібліотека

src/BuiltIn/ надає всі об’єкти стандартної бібліотеки: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console, а також пакети Web Platform / 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 викликає їх у порядку залежностей. Кожен із них визначає ланцюжок прототипів, встановлює методи як екземпляри JsFunction, обгорнуті навколо замикань PHP, і надає конструктор як глобальний об’єкт.

Міст PHP↔JS

Перетворення значень через межу відбувається у двох місцях:

  • Phasis\Interop\PhpToJs::convert() — значення PHP у JsValue; використовується для аргументів setGlobal(), call() і значень, які повертають хост-функції. Функція PHP стає JsFunction, тіло якої її викликає.
  • PhpToJs::toPhp() — JsValue у PHP для аргументів хост-функції.
  • Engine::toPhp() (приватний) — JsValue у PHP для результатів eval() і call().

Моста для об’єктів немає: об’єкти PHP, які не є викликаними, перетворюються на undefined (issue #18). Клас JsToPhp у src/Bytecode/ не пов’язаний із взаємодією систем; це транспілятор, описаний нижче. Правила перетворення наведено в розділі Перетворення значень.

Обробка помилок

Неперехоплений JS throw стає PHP Phasis\Exceptions\JsThrowable; помилки, які генерує сам рушій, з’являються як Phasis\Exceptions\TypeError, RangeError або ReferenceError — усі вони є підкласами Phasis\Exceptions\RuntimeError. Усередині рушія JS try / catch перехоплює JsThrowable, будь-який RuntimeError і Phasis\Exceptions\SyntaxError. Інші винятки PHP, зокрема ті, що виникають у хост-функціях, не перетворюються й передаються безпосередньо виклику PHP (issue #20).

Є два обмеження: межа глибини стека викликів (CallStack::maxDepth, 4,096 кадрів, плюс захист на 8,192 кадрів для транспільованих замикань) і maxLoopIterations, що задається через Engine::setLimit() і контролюється лише інтерпретатором дерева (issue #19). Обидва спричиняють Phasis\Exceptions\InternalError. Обмежень на довжину рядків, розмір виводу чи час виконання немає.

Віртуальна машина байткоду

Гарячі функції оптимізуються двома способами. JsToPhp (у src/Bytecode/) генерує замикання PHP для тіл із числовими операціями чи багатьма локальними змінними, яке трасувальний JIT-компілятор PHP може скомпілювати в машинний код; конвеєр оптимізує читання new Ctor(args), obj.method(args) і this.member, чого достатньо, щоб типовий шаблон із методом класу в циклі повністю оминув віртуальну машину. Усе інше компілюється у плоский байткод, який обробляє VM::execute із власним стеком викликів: кожен виклик JS залишається в межах одного кадру PHP, а для читання методів прототипу використовується кеш для кожного PC. Прості тіла генераторів також компілюються в байткод; Op::YIELD зберігає знімок кадру в купі замість переходу до Fiber PHP.

Будь-яка форма, яку компілятор не оптимізує (e.g. with, try із повторним прив’язуванням у catch, yield*, асинхронні генератори), обробляється інтерпретатором дерева. Повний опис механізму див. у розділі Віртуальна машина байткоду.