Design do LFortran

Visão Geral de Alto Nível

LFortran está estruturado em torno de dois módulos independentes, AST e ASR, ambos autônomos (completamente independentes do resto do projeto) e os utilizadores são encorajados a utilizá-los independentemente para outras aplicações e a construir ferramentas com base nisso:

  • Árvore de Sintaxe Abstrata (AST), módulo lfortran.ast: representa qualquer código-fonte Fortran, estritamente baseado na sintaxe, nenhuma semântica está incluída. O módulo AST pode ser convertido em código em Fortran.

  • Representação Semântica Abstrata (ASR), o módulo lfortran.asr: representa um código-fonte Fortran válido, toda a semântica está incluída. Não é permitido códigos inválidos em Fortran (será dado um erro). O módulo ASR pode ser convertido em uma AST.

O compilador LFortran é então composto pelas seguintes etapas independentes:

  • Análise: converte o código-fonte Fortran em uma AST

  • Semântica: converte uma AST em uma ASR

  • Otimizações de alto nível: otimiza a ASR para uma simplificá-la e possivelmente torná-la mais rápida (coisas como alinhamento de funções, eliminação de expressões ou comandos redundantes, etc.)

  • Geração de código LLVM IR e otimizações de baixo nível: converte uma ASR em uma LLVM IR. Nesta etapa são feitas outras otimizações que não produzem uma ASR, porém ainda faz sentido fazê-las antes de passá-las ao LLVM IR.

  • Geração de código de máquina: LLVM fará todas as suas otimizações e gerará código de máquina (como um executável, uma biblioteca, um arquivo objeto, ou carregado e executado usando JIT como parte de uma sessão interativa LFortran ou em um kernel do Jupyter).

O LFortran está estruturado em forma de uma biblioteca, podendo ser usado para obter uma AST, ou usar o analisador semântico para obter uma ASR, podendo usá-las em uma aplicação. Também é possível gerar uma ASR diretamente (Ex: a partir do SymPy) e depois convertê-la a uma AST, para em seguida montar um código em Fortran, ou usar o LFortran para compilá-la em código de máquina diretamente. Em outras palavras, é fácil usar o LFortran para converter facilmente entre às três representações equivalentes:

  • Código-fonte em Fortran

  • Árvore de Sintaxe Abstrata (AST)

  • Representação Semântica Abstrata (ASR)

São todos equivalentes, no seguinte sentido:

  • Qualquer ASR pode ser convertida em uma AST equivalente

  • Qualquer AST pode sempre ser convertida em um código Fortran equivalente

  • Qualquer código-fonte em Fortran pode ser sempre convertido em uma AST equivalente ou é emitido um erro de sintaxe

  • Qualquer AST pode ser sempre convertida em uma ASR equivalente ou é emitido um erro de semântica

Então, quando a conversão pode ser realizada, são equivalentes, e esta pode sempre ser realizada, exceto no caso em que o código é invalido.

Detalhes do design da ASR

A ASR foi imaginada para ter os seguintes recursos:

  • A ASR sempre é equivalente semanticamente ao código original Fortran (não perde nenhuma informação). A ASR pode ser convertida em uma AST, e a AST em código-fonte cuja funcionalidade é idêntica a original.

  • A ASR é a mais simples possível: não contém nenhuma informação que não pode ser inferida de uma ASR.

  • The ASR C++ classes (down the road) are designed similarly to SymEngine: they are constructed once and after that they are immutable. The constructor checks in Debug more that all the requirements are met (e.g., that all Variables in a Function have a dummy argument set, that explicit-shape arrays are not allocatable and all other Fortran requirements to make it a valid code), but in Release mode it quickly constructs the class without checks. Then there are builder classes that construct the ASR C++ classes to meet requirements (checked in Debug mode) and the builder gives an error message if a code is not a valid Fortran code, and if it doesn’t give an error message, then the ASR C++ classes are constructed correctly. Thus by construction, the ASR classes always contain valid Fortran code and the rest of LFortran can depend on it.

Notas:

Informação perdida ao construir a ASR a partir do código-fonte: espaços em branco, distinção entre os comandos if na mesma linha e if ao longo de várias linhas, além das letras maiúsculas e minúsculas em palavras-chave.

Informação perdida quando transformando uma AST em ASR: sintaxe detalhada de como as variáveis foram definidas e a ordem dos atributos de tipo (se a dimensão de um array está usando o atributo dimension ou uma expressão parentesada, ou ainda quantas variáveis existiam na linha, ou sua ordem), já que a ASR apenas representa as informações de tipo em uma tabela de símbolos.

ASR is the simplest way to generate Fortran code, as one does not have to worry about the detailed syntax (as in AST) about how and where things are declared. One specifies the symbol table for a module, then for each symbol (functions, global variables, types, …) one specifies the local variables and if this is an interface then one needs to specify where one can find an implementation, otherwise a body is supplied with statements, those nodes are almost the same as in AST, except that each variable is just a reference to a symbol in the symbol table (so by construction one cannot have undefined variables). The symbol table for each node such as Function or Module also references its parent (for example a function references a module, a module references the global scope).

The ASR can be directly converted to an AST without gathering any other information. And the AST directly to Fortran source code.

The ASR is always representing a semantically valid Fortran code. This is enforced by checks in the ASR C++ constructors (in Debug build). When an ASR is used, one can assume it is valid.

Fortran 2008

Fortran 2008 standard chapter 2 «Fortran concepts» specifies that Fortran code is a collection of program units (either all in one file, or in separate files), where each program unit is one of:

  • Programa principal

  • módulo ou submódulo

  • função ou sub-rotina

Note: It can also be a block data program unit, that is used to provide initial values for data objects in named common blocks, but we do not recommend the use of common blocks (use modules instead).

Extensão do LFortran

We extend the Fortran language by introducing a global scope, which is not only the list of program units (as in F2008) but can also include statements, declarations, use statements and expressions. We define global scope as a collection of the following items:

  • Programa principal

  • módulo ou submódulo

  • função ou sub-rotina

  • comando use

  • declaração

  • comando

  • expressão

In addition, if a variable is not defined in an assignment statement (such as x = 5+3) then the type of the variable is inferred from the right hand side (e.g., x in x = 5+3 would be of type integer, and y in y = 5._dp would be of type real(dp)). This rule only applies at the top level of global scope. Types must be fully specified inside main programs, modules, functions and subroutines, just like in F2008.

The global scope has its own symbol table. The main program and module/submodule do not see any symbols from this symbol table. But functions, subroutines, statements and expressions at the top level of global scope use and operate on this symbol table.

The global scope has the following symbols predefined in the symbol table:

  • the usual standard set of Fortran functions (such as size, sin, cos, …)

  • the dp double precision symbol, so that one can use 5._dp for double precision.

Each item in the global scope is interpreted as follows: main program is compiled into an executable with the same name and executed; modules, functions and subroutines are compiled and loaded; use statement and declaration adds those symbols with the proper type into the global scope symbol table, but do not generate any code; statement is wrapped into an anonymous subroutine with no arguments, compiled, loaded and executed; expression is wrapped into an anonymous function with no arguments returning the expression, compiled, loaded, executed and the return value is returned to the user.

The global scope is always interpreted, item by item, per the previous paragraph. It is meant to allow interactive usage, experimentations and writing simple scripts. Code in global scope must be interpreted using lfortran. For more complex (production) code it is recommended to turn it into modules and programs (by wrapping loose statements into subroutines or functions and by adding type declarations) and compile it with lfortran or any other Fortran compiler.

Here are some examples of valid code in global scope:

Exemplo 1

a = 5
print *, a

Exemplo 2

a = 5

subroutine p()
print *, a
end subroutine

call p()

Exemplo 3

module a
implicit none
integer :: i
end module

use a, only: i
i = 5

Exemplo 4

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

Considerações de Design

The LFortran extension of Fortran was chosen in a way so as to minimize the number of changes. In particular, only the top level of the global scope has relaxed some of the Fortran rules (such as making specifying types optional) so as to allow simple and quick interactive usage, but inside functions, subroutines, modules or programs this relaxation does not apply.

The number of changes were kept to minimum in order to make it straightforward to turn code at global scope into standard compliant Fortran code using programs and modules, so that it can be compiled by any Fortran compiler.