LFortran Design¶
高级概述¶
LFortran 围绕两个独立的模块 AST 和 ASR 构建,这两个模块都是独立的(完全独立于 LFortran 的其余部分),鼓励用户将它们独立用于其他应用程序并在其上构建工具:
抽象语法树 (AST),模块
lfortran.ast
:表示任何 Fortran 源代码,严格基于语法,不包含语义。 AST 模块可以将自身转换为 Fortran 源代码。抽象语义表示 (ASR),模块
lfortran.asr
:表示有效的 Fortran 源代码,包括所有语义。不允许使用无效的 Fortran 代码(将给出错误)。 ASR 模块可以将自身转换为 AST。
LFortran 编译器由以下独立阶段组成:
解析:将 Fortran 源代码转换为 AST
语义:将 AST 转换为 ASR
高级优化:将 ASR 优化为可能更快/更简单的 ASR(内联函数、消除冗余表达式或语句等)
LLVM IR 代码生成和较低级别的优化:将 ASR 转换为 LLVM IR。此阶段还执行所有其他不产生 ASR,但在传递给 LLVM IR 之前仍然有意义的优化。
机器代码生成:LLVM 然后进行所有优化并生成机器代码(例如二进制可执行文件、库、目标文件,或者使用 JIT 作为交互式 LFortran 会话的一部分或在 Jupyter 内核中加载和执行)。
LFortran 被构造为一个库,因此可以使用解析器获取 AST 并对其进行处理,或者然后可以使用语义分析器获取 ASR 并对其进行处理。可以直接生成 ASR(例如,从 SymPy),然后转换为 AST 和 Fortran 源代码,或者使用 LFortran 直接将其编译为机器代码。换句话说,可以使用 LFortran 在三种等效表示之间轻松转换:
Fortran 源代码
抽象语法树 (AST)
抽象语义表示 (ASR)
它们在以下意义上都是等价的:
任何 ASR 始终可以转换为等效的 AST
任何 AST 始终可以转换为等效的 Fortran 源代码
任何 Fortran 源代码总是可以转换为等效的 AST 或出现语法错误
任何 AST 总是可以转换为等效的 ASR 或者出现语义错误
因此,当可以进行转换时,它们是等效的,并且除非代码无效,否则始终可以进行转换。
ASR 设计细节¶
ASR 旨在具有以下功能:
ASR 在语义上仍然等同于原始 Fortran 代码(它没有丢失任何语义信息)。 ASR 可以转换为 AST,AST 可以转换为 Fortran 源代码,在功能上与原始代码相同。
ASR 尽可能简单:它不包含任何无法从 ASR 推断出的信息。
ASR C++ 类(未来)的设计类似于 SymEngine:它们被构造一次,之后它们是不可变的。构造函数在 Debug 中检查是否满足所有要求(例如,函数中的所有变量都有一个虚拟参数集,显式形状数组不可分配以及所有其他 Fortran 要求使其成为有效代码),但在发布模式它无需检查即可快速构建类。然后是构建 ASR C++ 类以满足要求的构建器类(在调试模式下检查),如果代码不是有效的 Fortran 代码,构建器会给出错误消息,如果它没有给出错误消息,则 ASR C++ 类构造正确。因此,通过构造,ASR 类总是包含有效的 Fortran 代码,而 LFortran 的其余部分可以依赖它。
Compilation Modes¶
We support two compilation modes:
Monolithic Compilation Mode: The standard compilation mode. In this mode, LFortran produces empty object files (
.o
files) with-c
flag (the only reason to produce those is to satisfy existing build systems that typically expect.o
files to be created). The object code is generated only when main program is encountered: all modules are loaded from.mod
files and everything compiled and linked at once. When a module is compiled, only a.mod
file is generated with full code. For files with global procedures, LFortran identifies those automatically and setsgenerate_code_for_global_procedures
compiler option totrue
(not exposed to user), which then generates object code only for global procedures (you must thus link these generated.o
object files with the main program), and rest of the modules are serialized to.mod
files which do not contain global procedure.Separate Compilation Mode: This mode is enabled with
--separate-compilation
flag. In this mode, LFortran generates full code for each file into object files (.o
files) with full symbol information. This is usually the default mode used by most other Fortran compilers. We create object code, and we still create.mod
files for modules and they contain everything just like for direct mode but when any.mod
file is loaded, we change all symbols toExternalUndefined
ABI. We don’t change the ABI forbind(c)
(since those are undefined already in object code, the user is responsible to provide an implementation at link time).
Note: If you enable separate compilation mode, you have to enable it for all the files.
注意:¶
将源解析为 AST 时丢失的信息:空格、多行/单行 if 语句区分、关键字的区分大小写。
从 AST 到 ASR 时丢失的信息:如何定义变量的详细语法以及类型属性的顺序(数组维度是否使用 dimension
属性,或变量处的括号;或每个声明行有多少个变量或它们的顺序),因为 ASR 仅表示符号表中的聚合类型信息。
ASR 是生成 Fortran 代码的最简单方法,因为不必担心关于如何以及在何处声明事物的详细语法(如在 AST 中)。一个为模块指定符号表,然后为每个符号(函数、全局变量、类型……)指定局部变量,如果这是一个接口,则需要指定在哪里可以找到实现,否则为body 提供了语句,这些节点与 AST 中的几乎相同,除了每个变量只是对符号表中的符号的引用(因此通过构造一个不能有未定义的变量)。每个节点(例如 Function 或 Module)的符号表也引用其父节点(例如,函数引用模块,模块引用全局作用域)。
ASR 可以直接转换为 AST,而无需收集任何其他信息。而AST直接转为Fortran源代码。
ASR 始终表示语义上有效的 Fortran 代码。这是通过检查 ASR C++ 构造函数(在调试版本中)来强制执行的。当使用 ASR 时,可以假定它是有效的。
Fortran 2008¶
Fortran 2008 标准 第 2 章“Fortran 概念”指定 Fortran 代码是_程序单元_的集合(全部在一个文件中) ,或在单独的文件中),其中每个 程序单元 是以下之一:
主程序
模块或子模块
函数或子程序
注意:它也可以是_block data_程序单元,用于为命名的_common blocks_中的数据对象提供初始值,但我们不建议使用_common blocks_(改用模块)。
LFortran 扩展¶
我们通过引入_全局作用域_来扩展 Fortran 语言,它不仅是_程序单元_列表(如 F2008 中),还可以包括语句、声明、使用语句和表达式。我们将_全局作用域_定义为以下项目的集合:
主程序
模块或子模块
函数或子程序
使用声明
声明
声明
表达
此外,如果变量没有在赋值语句中定义(例如x = 5+3
),则从右侧推断变量的类型(例如,x
in x = 5+3
将是 integer
类型,而 y = 5._dp
中的 y
将是 real(dp)
类型)。此规则仅适用于 全局作用域 的顶层。类型必须在主程序、模块、函数和子程序中完全指定,就像在 F2008 中一样。
全局作用域 有自己的符号表。主程序和模块/子模块看不到此符号表中的任何符号。但是_全局作用域_顶层的函数、子例程、语句和表达式使用和操作这个符号表。
全局作用域 在符号表中预定义了以下符号:
通常的标准 Fortran 函数集(例如
size
、sin
、cos
…)dp
双精度符号,因此可以使用5._dp
表示双精度。
_全局作用域_中的每一项解释如下:主程序编译成同名可执行文件并执行;编译和加载模块、函数和子程序; use 语句和声明将那些具有正确类型的符号添加到_全局作用域_符号表中,但不生成任何代码;语句被包装到一个没有参数的匿名子例程中,编译、加载和执行;表达式被包装到一个匿名函数中,没有参数返回表达式,编译、加载、执行并将返回值返回给用户。
全局作用域 总是按照上一段逐项解释。它旨在允许交互式使用、实验和编写简单的脚本。 全局作用域 中的代码必须使用 lfortran
解释。对于更复杂的(生产)代码,建议将其转换为模块和程序(通过将松散的语句包装成子例程或函数并添加类型声明)并使用 lfortran
或任何其他 Fortran 编译器对其进行编译。
以下是 全局作用域 中的一些有效代码示例:
示例 1¶
a = 5
print *, a
示例 2¶
a = 5
subroutine p()
print *, a
end subroutine
call p()
示例 3¶
module a
implicit none
integer :: i
end module
use a, only: i
i = 5
示例 4¶
x = [1, 2, 3]
y = [1, 2, 1]
call plot(x, y, "o-")
设计注意事项¶
选择 Fortran 的 LFortran 扩展是为了尽量减少更改的数量。特别是,只有_全局作用域_的顶层放宽了一些 Fortran 规则(例如使指定类型可选)以允许简单快速的交互使用,但在函数、子例程、模块或程序内部,这种放宽并不适用.
更改的数量保持在最低限度,以便使用程序和模块将_全局作用域_中的代码直接转换为符合标准的 Fortran 代码,以便任何 Fortran 编译器都可以编译它。