En relación a “Tutorial: Cómo hacer un compilador de PHP. Proyecto NPhp : Introducción #php #compiladores #dotnet

Lo básico:

Partes:

NPhp está separado actualmente en 3 partes fundamentales:

Entry Point

Para ejecutar un archivo PHP lo primero que se hace es instanciar Php54Runtime

var Runtime = new Php54Runtime(InteractiveErrors: false);

https://github.com/soywiz/nphp/blob/master/irony/NPhp/NPhp/Runtime/Php54Runtime.cs

El Runtime se encarga de instanciar una gramática que permitirá parsear el código, también se instancian varios ámbitos: ámbito de funciones, ámbito de variables globales y ámbito de constantes globales.

Posteriormente se le añaden las funciones globales nativas de forma lazy: Runtime.FunctionScope.LoadAllNativeFunctions();

El runtime permite generar un método nativo de .NET que se podrá ejecutar posteriormente:

var Function = Runtime.CreateMethodFromPhpFile(File.ReadAllText(args[0]), File: args[0], DumpTree: false, DoDebug: false);

Function es un Php54Function con un delegado de .NET generado dinámicamente on the fly.

Ejecutamos dicha función con un ámbito concreto, que para el caso de un archivo será el ámbito global:

Function.Execute(Runtime.GlobalScope);

Finalizamos el runtime, que finalizará los bufferes ob_ y que lanzará los callbacks de register_shutdown_function:

finally Runtime.Shutdown();

Grammar:

La gramática define el lenguaje y define la generación del Ast.

La gramática tiene dos elementos: terminales y no terminales. Los terminales son tokens que finalmente se generarán y que forman los elementos indivisibles de la gramática.

Los no terminales definen las partes por las que estará compuesto un nodo Ast, definiendo los terminales y no terminales por los que estará compuesto.

Los no terminales se pueden definir como un conjunto de disyuntivas de nodos o de una sucesión.

Ejemplo:

La sentencia return, está compuesta por el terminal “return” sucedido por un no terminal Expression y terminado por el terminal “;” (sucesión)

ReturnStatement.Rule = ToTerm("return") + Expression + ToTerm(";");

Un cast en php está formado por cualquiera de estos terminales (disyuntiva).

CastTypes.Rule =
 ToTerm("int")
 | ToTerm("bool")
 | ToTerm("float")
 | ToTerm("string")
 | ToTerm("array")
 | ToTerm("object")
 | ToTerm("unset")
;

Mientras que un cast en sí está formado por un “(“ seguido por cualquiera de los terminales anteriores y terminado por un “)”.

CastOperation.Rule = ToTerm("(") + CastTypes + ToTerm(")");

Las sentencias IF tiene dos construcciones o una construcción con un else opcional. Actualmente lo he definido así:

IfStatement.Rule = ToTerm("if") + ToTerm("(") + Expression + ToTerm(")") + Statement;

IfElseStatement .Rule = ToTerm("if") + "(" + Expression + ")" +  Statement  + PreferShiftHere() + ToTerm("else") +  Statement;

Y una sentencia (“Statement”) se puede definir:

Statement.Rule =
 ExpressionStatement
 | IfStatement
 | IfElseStatement
 | CurlyStatement
;

CurlyStatement.Rule = ToTerm("{") + StatementList + ToTerm("}");

Mientras que una lista de sentencias es cero o más sentencias (MakeStarRule es para 0 o más repeticiones como un * en una expresión regular, MakePlusRule sería lo mismo que un + en una expresión regular y serían 1 o más veces).

StatementList.Rule = MakeStarRule(StatementList, Sentence);

En cuanto a las expresiones, de normal se construiría una regla por cada nivel de precedencia de operadores, mientras que con Irony, puedes definir directamente la precedencia y hacer un único nivel.

Con lo cual una expresión puede estar compuesta por un literal, y también por ejemplo por una operación binaria.

BinaryOperation.Rule = Expression + BinaryOperator + Expression;
TernaryOperation.Rule = Expression + "?" + Expression + ":" + Expression;

BinaryOperator.Rule =
 ToTerm("<")
 | "===" | "!=="
 | "||" | "&&" | "|" | "^" | "&" | "==" | "!=" | ">" | "<=" | ">=" | "<<" | ">>" | "+" | "-" | "*" | "/" | "%" | "."
 //| "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
 | "is" | "as" | "??"
;

RegisterOperators(15, "&", "&&", "|", "||");
RegisterOperators(19, "===", "!==");
RegisterOperators(20, "==", "<", "<=", ">", ">=", "!=");
RegisterOperators(30, "+", "-");
RegisterOperators(40, "*", "/");

La gramática se define así y no tiene mucho más. Luego se asocian clases a cada regla “NoTerminal” y Irony construye un árbol abstracto con las clases que le haya dicho que use.

Con el árbol construido, hacemos que cada nodo implemente una interfaz de generación. En este caso he hecho que todas las clases del árbol tengan un método llamado Generate al que se le pasa un parámetro con un contexto de generación, que tendrá todas las utilidades para la generación de código y estados necesarios.

abstract public void Generate(NodeGenerateContext Context);

Codegen:

Expresiones:

Ya tenemos un Ast construído gracias a Irony. Y ahora queremos generar un método de .NET dinámicamente. El NodeGenerateContext contiene un SafeILGenerator, que simplifica bastante y hace más segura la generación de código IL para generación dinámica de métodos. En .NET podemos generar ensamblados completos o con DynamicMethod métodos dinámicos desechables y que se generan bastante rápido.

Un NumberNode, contendría un único token que sería un número en formato numérico, con lo cual, parseamos el valor y la generación de este elemento es sencillo: un Push en el IL. Recordemos que el Intermediated Language de .NET es stack-based. De forma que las expresiones deberían dejar en la pila un único elemento, y las sentencias no dejar ningún elemento en la pila.

Insertamos el número en la pila y construimos un Php54Var a partir de él con el método estático FromInt.

Context.MethodGenerator.Push(Value);
Context.MethodGenerator.Call((Func<int, Php54Var>)Php54Var.FromInt);

Un BinaryExpressionNode, estaría compuesto por 3 subnodos: Expresión izquierda, expresión derecha y un operador binario.

Así que el generador de este nodo lo que haría es generar por un lado las dos subexpresiones (izquierda y luego derecha), dejando dos elementos en la pila y luego aplicar un operador binario/función que convirtiese  esos dos valores en uno solo. Quedando algo así:

Left.Generate(Context);
Right.Generate(Context);

switch (Operator)
{
 case "+":
  Context.MethodGenerator.Call((Func<Php54Var, Php54Var, Php54Var>)Php54Var.Add);
 break;
 ...
}

Un UnaryExpressionNode, estaría compuesto 2 subnodos: operador unario, y expresión derecha.

La idea es la misma, la expresión derecha colocaría un único valor en la pila, y el operador unario lo que debería es convertir ese valor en otro diferente, pero dejando el mismo número de elementos en la pila.

Y con esto generaríamos código para expresiones formadas únicamente por operandos numéricos y operadores.

Sentencia IF:

La idea de la sentencia IF es parecida. Tenemos subnodos que generarán una serie de elementos en la pila. 0, 1…  Las expresiones dejan un elemento en la pila y las sentencias no dejan ningún elemento. IF es una sentencia y por lo tanto no debe dejar elementos.

Si recordamos la definición básica:

IfStatement.Rule = ToTerm("if") + ToTerm("(") + Expression + ToTerm(")") + Statement;

Tenemos una expresión condicional y luego un statement (que puede ser un conjunto de ellos si se usa un CurlyExpression), pero en cualquier caso aunque sean muchos, dejarán 0 elementos en la pila, que es lo importante.

El funcionamiento del IF a nivel de código máquina/código IL es:

Pseudo código alto nivel:

SI <condición> ENTONCES <ejecutar código>

Pseudo código bajo nivel:

SI NO <condición> GOTO skip;</span></div>
EJECUTAR CÓDIGO
skip:

Simplemente se niega la condición y se salta al final del código que no quiere que se ejecute. De forma que tendríamos que coger la expresión condicional y hacer un branch condicional para saltarse el código de dentro del IF. Quedaría algo así:

var EndLabel = Context.MethodGenerator.DefineLabel("End");
var FalseLabel = Context.MethodGenerator.DefineLabel("False");
// Check condition
{
 (ConditionExpresion.AstNode as Node).Generate(Context);
 Context.MethodGenerator.ConvTo<bool>();
 Context.MethodGenerator.BranchIfFalse(FalseLabel);
}
// True
{
 (TrueSentence.AstNode as Node).Generate(Context);
 Context.MethodGenerator.BranchAlways(EndLabel);
}
// False
FalseLabel.Mark();
if (FalseSentence != null)
{
 (FalseSentence.AstNode as Node).Generate(Context);
 Context.MethodGenerator.BranchAlways(EndLabel);
}
EndLabel.Mark();
//base.Generate(Context);

Y eso es todo por ahora. En próximos posts, veremos otras estructuras de control, cómo se generan los métodos, ámbitos, funciones, etc.