Diseño de LFortran

Descripción general de alto nivel

LFortran está estructurado en torno a dos módulos independientes, AST y ASR, los cuales son independientes (completamente independientes del resto de LFortran) y se anima a los usuarios a usarlos de forma independiente para otras aplicaciones y crear herramientas en la parte superior:

  • Árbol de sintaxis abstracta (AST), módulo lfortran.ast: representa cualquier código fuente de Fortran, basado estrictamente en la sintaxis, no se incluye semántica. El módulo AST puede convertirse en código fuente de Fortran.

  • Representación semántica abstracta (ASR), módulo lfortran.asr: Representa un código fuente de Fortran válido, se incluye toda la semántica. No se permite el código Fortran no válido (se dará un error). El módulo ASR puede convertirse en un AST.

El compilador LFortran está compuesto por las siguientes etapas independientes:

  • Análisis: convierte el código fuente de Fortran en un AST

  • Semántica: Convertir un AST para un ASR

  • Optimización de alto nivel: optimizar ASR para un ASR posiblemente mas simple y mas rápido (cosas como alinear funciones, eliminar expresiones o declaraciones redundantes, etc.)

  • Generación de código LLVM IR y optimizaciones de nivel inferior: convierte un ASR en un LLVM IR. Esta etapa también realiza todas las demás optimizaciones que no producen un ASR, pero que aún tiene sentido antes de pasar a LLVM IR.

  • Generación de código de máquina: LLVM luego realiza todas sus optimizaciones y genera código de máquina (como un ejecutable binario, una biblioteca, un archivo de objeto, o se carga y ejecuta usando JIT como parte de la sesión interactiva de LFortran o en un núcleo de Jupyter).

LFortran está estructurado como una biblioteca, por lo que, por ejemplo, se puede usar el analizador para obtener un AST y hacer algo con él, o se puede usar el analizador semántico para obtener ASR y hacer algo con él. Uno puede generar la ASR directamente (por ejemplo, desde SymPy) y luego convertirlo a AST y a un código fuente de Fortran, o usar LFortran para compilarlo en código de máquina directamente. En otras palabras, uno puede usar LFortran para convertir fácilmente entre las tres representaciones equivalentes:

  • Código fuente de Fortran

  • Árbol de sintaxis abstracta (AST)

  • Representación semántica abstracta (ASR)

Estos son todos equivalentes en el siguiente sentido:

  • Cualquier ASR puede siempre ser convertido a un AST equivalente

  • Cualquier AST puede ser siempre convertido a un código fuente Fortran equivalente

  • Cualquier código fuente de Fortran siempre se puede convertir a un AST equivalente o se obtiene un error de sintaxis

  • Cualquier AST siempre se puede convertir a un ASR equivalente o se obtiene un error semántico

Así, cuando se puede realizar una conversión estos son equivalentes, y la conversión siempre se puede realizar a menos que el código no sea válido.

Detalles de diseño de una ASR

Una ASR está diseñada para tener las características siguientes:

  • Una ASR sigue siendo semánticamente equivalente al código Fortran original (esta no pierde información semántica). Una ASR puede ser convertida a un AST, y un AST a un código fuente Fortran que es funcionalmente equivalente al original.

  • Una ASR es lo más simple posible: no contiene ninguna información que no pueda deducirse de una ASR.

  • Las clases ASR C++ (en el futuro) están diseñadas de manera similar a SymEngine: se construyen una vez y luego son inmutables. El constructor verifica en modo depuración que se cumplan todos los requisitos (por ejemplo, que todas las variables en una función tengan un conjunto de argumentos ficticios, que las matrices de forma explícita no se puedan asignar y todos los demás requisitos de Fortran para que sea un código válido), pero en modo de lanzamiento, construye rápidamente la clase sin controles. Luego, hay clases de constructores que construyen las clases ASR C++ para cumplir con los requisitos (verificados en el modo de depuración) y el constructor da un mensaje de error si un código no es un código Fortran válido, y si no da un mensaje de error, entonces las clases ASR C++ están construidas correctamente. Así, por construcción, las clases ASR siempre contienen código Fortran válido y el resto de LFortran puede depender de él.

Notas:

Información que se pierde al analizar la fuente en un AST: espacios en blanco, distinción de declaración if de varias líneas/una sola línea, distinción entre mayúsculas y minúsculas de palabras clave.

Información que se pierde al pasar de un AST a una ASR: sintaxis detallada, cómo se definieron las variables y el orden de los atributos de tipo (si la dimensión de matriz usa el atributo dimensión o paréntesis en la variable, o cuántas variables hay por línea de declaración o su orden), ya que una ASR solo representa la información de tipo agregada en la tabla de símbolos.

Una ASR es la forma más sencilla de generar código Fortran, ya que uno no tiene que preocuparse por la sintaxis detallada (como en un AST) sobre cómo y dónde se declaran las cosas. Uno especifica la tabla de símbolos para un módulo, luego para cada símbolo (funciones, variables globales, tipos, …) uno especifica las variables locales y si se trata de una interfaz, entonces uno necesita especificar dónde puede encontrar una implementación, de lo contrario un cuerpo se suministra con declaraciones, esos nodos son casi los mismos que en un AST, excepto que cada variable es solo una referencia a un símbolo en la tabla de símbolos (por lo que, por construcción, uno no puede tener variables indefinidas). La tabla de símbolos para cada nodo, como Función o Módulo, también hace referencia a su padre (por ejemplo, una función hace referencia a un módulo, un módulo hace referencia al ámbito global).

Una ASR se puede convertir directamente en un AST sin recopilar ninguna otra información. Y un AST directamente al código fuente de Fortran.

Una ASR siempre representa un código Fortran semánticamente válido. Esto se aplica mediante comprobaciones en los constructores de una ASR C++ (en la compilación de depuración). Cuando se utiliza una ASR, se puede suponer que es válido.

Fortran 2008

Fortran 2008 estándar capítulo 2 «Conceptos de Fortran» especifica que el código Fortran es una colección de unidades de programa (ya sea todo en un archivo , o en archivos separados), donde cada unidad de programa es una de:

  • programa principal

  • módulo o submódulo

  • función o subrutina

Nota: También puede ser una unidad de programa de bloques de datos, que se utiliza para proporcionar valores iniciales para objetos de datos en bloques comunes con nombre, pero no recomendamos el uso de bloques comunes (utilice módulos en su lugar).

Extensión LFortran

Extendemos el lenguaje Fortran introduciendo un alcance global, que no es sola la lista de unidades de programa (como en F2800) sino que puede también incluir declaraciones, expresiones y declaraciones de uso. Definimos alcance global como una colección de los elementos siguientes:

  • programa principal

  • módulo o submódulo

  • función o subrutina

  • declaración de uso

  • declaración

  • declaración

  • expresión

Además, si una variable no está definida en una instrucción de asignación (como x = 5+3), el tipo de variable se deduce del lado derecho (por ejemplo, x en x = 5+3). sería de tipointeger, y yeny = 5._dpsería de tiporeal(dp)`). Esta regla solo se aplica en el nivel superior de alcance global. Los tipos deben especificarse completamente dentro de los programas, módulos, funciones y subrutinas principales, al igual que en F2008.

El alcance global tiene su propia tabla de símbolos. El programa principal y el módulo/submódulo no ven ningún símbolo de esta tabla de símbolos. Pero las funciones, subrutinas, declaraciones y expresiones en el nivel superior del alcance global usan y operan en esta tabla de símbolos.

El alcance global tiene los siguientes símbolos predefinidos en la tabla de símbolos:

  • el conjunto estándar habitual de funciones de Fortran (como size, sin, cos, …)

  • el símbolo de doble precisión dp, por lo que se puede usar 5._dp para doble precisión.

Cada elemento del alcance global se interpreta de la siguiente manera: el programa principal se compila en un ejecutable con el mismo nombre y se ejecuta; se compilan y cargan módulos, funciones y subrutinas; use sentencia y declaración agrega esos símbolos con el tipo adecuado en la tabla de símbolos alcance global, pero no genera ningún código; la declaración se envuelve en una subrutina anónima sin argumentos, se compila, se carga y se ejecuta; expresión se envuelve en una función anónima sin argumentos que devuelven la expresión, compilada, cargada, ejecutada y el valor de retorno se devuelve al usuario.

El alcance global siempre se interpreta, elemento por elemento, según el párrafo anterior. Está destinado a permitir el uso interactivo, la experimentación y la escritura de scripts simples. El código en alcance global debe interpretarse usando lfortran. Para código más complejo (de producción), se recomienda convertirlo en módulos y programas (envolviendo sentencias sueltas en subrutinas o funciones y agregando declaraciones de tipo) y compilarlo con lfortran o cualquier otro compilador de Fortran.

Aquí algunos ejemplos de código válidos en alcance global:

Ejemplo 1

a = 5
print *, a

Ejemplo 2

a = 5

subroutine p()
print *, a
end subroutine

call p()

Ejemplo 3

module a
implicit none
integer :: i
end module

use a, only: i
i = 5

Ejemplo 4

x = [1, 2, 3]
y = [1, 2, 1]
call plot(x, y, "o-")

Consideraciones de diseño

La extensión LFortran de Fortran se eligió de manera que se minimice el número de cambios. En particular, solo el nivel superior del alcance global ha relajado algunas de las reglas de Fortran (como hacer que la especificación de tipos sea opcional) para permitir un uso interactivo simple y rápido, pero dentro de las funciones, subrutinas, módulos o programas, esta relajación no se aplica.

El número de cambios se mantuvo al mínimo para que sea sencillo convertir el código en alcance global en código Fortran compatible con el estándar utilizando programas y módulos, de modo que pueda ser compilado por cualquier compilador de Fortran.