Saltar al contenido

Arquitectura

Phasis es un recorrido de árbol de cuatro etapas con una máquina virtual de bytecode oportunista. El flujo de datos es:

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

Cada etapa es una clase PHP con una interfaz pequeña que consume la etapa siguiente. No hay paso de enlazado: el análisis sintáctico produce un AST que el intérprete recorre directamente.

Analizador léxico

src/Lexer/Lexer.php convierte el texto fuente en un flujo de instancias Token. Cada token es una estructura de solo lectura: tipo (enumeración TokenType, ~80 casos), valor, línea y columna.

El analizador léxico procesa:

  • Literales numéricos: decimales, hexadecimales (0x), octales (0o), binarios (0b), exponenciales y separadores numéricos.
  • Literales de cadena: comillas simples y dobles, todas las secuencias de escape y literales de plantilla con expresiones anidadas.
  • Literales de expresiones regulares: tokenización /pattern/flags completa, que detecta cuándo / debe interpretarse como expresión regular o como división.
  • Identificadores, palabras clave y signos de puntuación.
  • Comentarios de línea y de bloque, preservados como elementos triviales.
  • Fuente Unicode: bytes UTF-8 interpretados como fuente UTF-16 JS.

El resultado es un generador que produce tokens de forma diferida. El analizador sintáctico los consume con un token de anticipación.

Analizador

src/Parser/Parser.php es un analizador sintáctico Pratt escrito a mano. La precedencia de las expresiones está en src/Parser/Precedence.php. Las instrucciones tienen su propio análisis descendente recursivo.

Produce nodos AST inmutables a partir de src/Ast/. Cada nodo contiene su ubicación en el código fuente para los seguimientos de pila y los informes de errores.

Estructuras destacadas:

  • Expresión: Literal, Identifier, BinaryExpression, UnaryExpression, AssignmentExpression, CallExpression, MemberExpression, ArrowFunction, ObjectExpression, ArrayExpression, ConditionalExpression, TemplateLiteral, SpreadElement, NewExpression, ThisExpression, SequenceExpression, TaggedTemplate, ClassExpression, YieldExpression, AwaitExpression.
  • Instrucción: BlockStatement, IfStatement, ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoWhileStatement, SwitchStatement, ReturnStatement, ThrowStatement, TryStatement, BreakStatement, ContinueStatement, ExpressionStatement.
  • Declaración: VariableDeclaration, FunctionDeclaration, ClassDeclaration, ImportDeclaration.
  • Patrón (desestructuración): ArrayPattern, ObjectPattern, RestElement, AssignmentPattern.

La inserción automática de punto y coma se implementa en el analizador sintáctico (no en el léxico), según la especificación.

Intérprete

src/Runtime/Interpreter.php recorre el AST. Visita cada nodo y devuelve un JsValue (o un Completion para las instrucciones que cambian el flujo de control).

Clases de apoyo principales:

  • Environment (src/Runtime/Environment.php): cadena de ámbitos léxicos. Cada llamada a función crea un entorno que apunta a su padre léxico. var se eleva al ámbito de la función; let / const tienen ámbito de bloque y una zona muerta temporal (TDZ).
  • CallStack (src/Runtime/CallStack.php): pila explícita con límite de profundidad. Las llamadas a funciones y métodos apilan marcos; las excepciones los desenrollan.
  • Completion (src/Runtime/Completion.php): unión discriminada de normal, return, throw, break, continue, más un valor y una etiqueta opcionales.
  • Reference (src/Runtime/Reference.php): tipo Reference de la especificación para evaluar el lado izquierdo en asignaciones y delete.

Valores

Los valores JavaScript son objetos PHP bajo src/Value/. Cada uno implementa la interfaz JsValue y contiene sus propios métodos de conversión de tipo (toNumber(), toString(), toBoolean()).

Clase Tipo JS
JsUndefined undefined (singleton)
JsNull null (singleton)
JsBoolean true / false
JsNumber double IEEE 754; conserva NaN, ±Infinity, ±0
JsString semántica de cadenas UTF-16
JsObject objeto base con mapa de propiedades y cadena de prototipos
JsArray amplía JsObject con seguimiento de longitud
JsFunction cierre con Environment capturado
JsSymbol primitivo Symbol
JsBigInt precisión arbitraria mediante bcmath
JsProxy los 13 interceptores, revocables
JsPromise ejecutor síncrono + cola de microtareas
JsGenerator modo dual: instantánea de marco (cuerpos compilados a bytecode; véase Bytecode VM § Generadores con instantánea de marco) con alternativa de Fiber PHP para yield*, try-with-yield y generadores asíncronos
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry colecciones
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray datos binarios

El manejo de números es la mayor fuente histórica de errores: NaN !== NaN debe ser verdadero, -0 === 0 debe ser verdadero, y los casos límite de ToNumber requieren precisión a nivel de bits. La clase Spec/TypeConversion implementa literalmente los algoritmos de la especificación y se ha probado exhaustivamente.

Biblioteca integrada

src/BuiltIn/ proporciona todos los objetos de la biblioteca estándar: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console, además de los paquetes Web Platform / Fetch (URL, Headers, Request, Response, Blob, FormData, Streams completo, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest y el bucle de eventos (setTimeout, setInterval, queueMicrotask, AsyncContext).

Cada función integrada es un método estático install() de una clase. Engine las invoca en orden de dependencias durante la construcción. Cada una define su cadena de prototipos, instala métodos como instancias JsFunction envueltas en cierres PHP y expone el constructor como global.

Puente PHP↔JS

La conversión de valores entre ambos lados se realiza en dos lugares:

  • Phasis\Interop\PhpToJs::convert(): valor PHP a JsValue, usado por setGlobal(), los argumentos de call() y los valores devueltos por funciones del host. Una función invocable PHP se convierte en un JsFunction cuyo cuerpo la llama.
  • PhpToJs::toPhp(): JsValue a PHP para los argumentos de una función del host.
  • Engine::toPhp() (privado): JsValue a PHP para los resultados de eval() y call().

No hay puente para objetos: los objetos PHP que no son invocables se convierten en undefined (issue #18). La clase JsToPhp de src/Bytecode/ no está relacionada con la interoperabilidad; es el transpilador descrito más abajo. Las reglas de conversión se documentan en Conversión de valores.

Gestión de errores

Un JS throw no capturado se convierte en un PHP Phasis\Exceptions\JsThrowable; los errores que genera el propio motor se presentan como Phasis\Exceptions\TypeError, RangeError o ReferenceError, todos subclases de Phasis\Exceptions\RuntimeError. Dentro del motor, un JS try / catch captura JsThrowable, cualquier RuntimeError y Phasis\Exceptions\SyntaxError. Las demás excepciones PHP, incluidas las generadas por funciones del host, no se convierten y se propagan directamente al llamador PHP (issue #20).

Hay dos límites: el máximo de la pila de llamadas (CallStack::maxDepth, 4,096 marcos, más un límite de 8,192 marcos para cierres transpilados) y maxLoopIterations, que se configura mediante Engine::setLimit() y solo aplica el recorrido del árbol (issue #19). Ambos generan Phasis\Exceptions\InternalError. No hay límites de longitud de cadena, tamaño de salida ni tiempo de ejecución.

Máquina virtual de bytecode

Las funciones de uso intensivo se compilan de dos maneras. JsToPhp (en src/Bytecode/) genera un cierre PHP para cuerpos con muchas operaciones numéricas o variables locales que el JIT de trazado de PHP puede compilar a código nativo; la canalización compila lecturas de new Ctor(args), obj.method(args) y this.member, lo que basta para que el patrón canónico de método de clase dentro de un bucle evite por completo la máquina virtual. Todo lo demás se compila a bytecode plano que consume VM::execute, con una pila de llamadas personalizada que mantiene cada llamada JS dentro de un único marco PHP y una caché en línea por PC para las lecturas de métodos de prototipo. Los cuerpos sencillos de generador también se compilan a bytecode, y Op::YIELD captura una instantánea del marco en el montón en lugar de pasar por Fiber PHP.

Cualquier estructura que el compilador no pueda compilar (e.g. with, try con un catch reasignado, yield*, generadores asíncronos) vuelve al recorrido del árbol. Consulte Máquina virtual de bytecode para conocer el mecanismo completo.