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 的其余部分可以依赖它。

注意:

将源解析为 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 函数集(例如 sizesincos…)

  • 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 编译器都可以编译它。