RPython 类型器

RPython 类型器位于目录 rpython/rtyper/ 中。

概述

RPython 类型器是 注释器 和代码生成器之间的桥梁。 注释器 的注释是高级的,因为它们描述了像列表或用户定义类的实例这样的 RPython 类型。

要发出代码,我们需要在目标语言的低级模型中表示这些高级注释;对于 C 来说,这意味着结构、指针和数组。类型器既确定每个注释的适当低级类型,又用一个或几个低级操作替换控制流图中的每个高级操作。就像低级类型一样,低级操作也只有一组相当受限的操作,类似于读取或写入结构的字段。

理论上,这一步是可选的;代码生成器可能能够直接读取高级类型。然而,我们的经验表明,这不太可能在实践中实现。“编译”高级类型到低级类型比人们预期的要复杂得多。这就是将此步骤明确化并在一个地方隔离的原因。在 RTyping 之后,图中只包含已经在目标语言级别上的操作,从而使代码生成器的任务变得更加简单。

示例:整数运算

整数运算最简单。假设一个图包含以下操作

v3 = add(v1, v2)

已注释

v1 -> SomeInteger()
v2 -> SomeInteger()
v3 -> SomeInteger()

然后显然我们希望对其进行类型化并替换为

v3 = int_add(v1, v2)

其中——在 C 表示法中——所有三个变量 v1、v2 和 v3 的类型均为 int。这是通过将属性 concretetype 附加到 v1、v2 和 v3(可能是 Variable 或 Constant 的实例)来完成的。在我们的模型中,此 concretetyperpython.rtyper.lltypesystem.lltype.Signed。当然,用 int_add 替换名为 add 的操作的目的是,代码生成器不再需要担心它意味着哪种类型的加法(或者可能是连接?)。

更详细的过程

RPython 类型器的结构类似于 注释器,两者都依次考虑每个流图块,并对每个操作执行一些分析。在这两种情况下,操作的分析都取决于其输入参数的注释。这反映在源文件中相同 __extend__ 语法的用法中(例如,比较 rpython/annotator/binaryop.pyrpython/rtyper/rint.py)。

不过,类比到此结束:在运行时,注释器正在计算注释的过程中,因此它可能需要重新流动和泛化,直到达到不动点。相反,类型器处理注释器计算的最终注释,而不会更改它们,假设它们在全局上是一致的。无需重新流动:类型器只考虑每个块一次。与注释器不同,类型器通过用一些低级操作替换每个操作来完全修改流图。

除了替换操作外,RTyper 还在流图中的所有变量和常量上创建了一个 concretetype 属性,该属性告诉代码生成器为每个变量使用哪种类型。此属性是 低级类型,如下所述。

表示

表示——Repr 类——是 RTyper 使用的最重要的内部类。(它们是内部的,因为它们是“实现细节”,并且它们的实例在 RTyper 完成后就会消失;代码生成器应该只使用 concretetype 属性,它们不是 Repr 实例,而是 低级类型。)

表示包含将特定 SomeXxx() 注释映射到特定低级类型的所有逻辑。目前,RTyper 假设每个 SomeXxx() 实例只需要一个“规范”表示。例如,所有使用 SomeInteger() 注释的变量都将通过 IntegerRepr 表示对应于 Signed 低级类型。更微妙的是,使用 SomeList() 注释的变量可以对应于一个保存正确类型项的数组的结构,或者——如果该列表只是一个具有恒定步长的 range()——一个只有 start 和 stop 字段的结构。

此示例表明,对于相同的高级操作,两种表示可能需要非常不同的低级实现。这就是将表示转换为显式对象的原因。

基础 Repr 类在 rpython/rtyper/rmodel.py 中定义。大多数 rpython/r*.py 文件定义一个或几个 Repr 的子类。RTyper 的 getrepr() 方法将为每个 SomeXxx() 实例构建并缓存一个 Repr 实例;此外,两个相等的 SomeXxx() 实例将获得相同的 Repr 实例。

Repr 实例的关键属性称为 lowleveltype,它会被复制到已赋予此表示的变量的 concretetype 属性中。RTyper 还为常量计算 concretetype,以匹配它们在低级操作中的使用方式(例如,int_add(x, 1) 需要一个 Constant(1),其中 concretetype=Signed)。

除了 lowleveltype 之外,每个 Repr 子类都提供了一组名为 rtype_op_xxx() 的方法,这些方法定义了如何将每个高级操作 op_xxx 转换为低级操作。

低级类型

RPython 类型器使用一个标准的低级模型,我们认为它可以直接对应于各种目标语言,例如 C。此模型在 rpython/rtyper/lltypesystem/lltype.py 的第一部分中实现。

rpython/rtyper/lltypesystem/lltype.py 的第二部分是这些类型的可运行实现,用于测试目的。它允许我们编写并测试使用 malloc() 函数获取和操作结构和数组的普通 Python 代码。例如,这对于实现和测试 RPython 类型(如“列表”及其操作和方法)很有用。

基本假设是变量(即局部变量、函数参数和返回值)都包含“简单”值:基本上,只是整数或指针。所有“容器”数据结构(结构和数组)都在堆中分配,并且始终通过指针进行操作。(没有等同于 C 概念的局部变量的 struct 类型。)

以下是快速浏览

>>> from rpython.rtyper.lltypesystem.lltype import *

以下是一些基本低级类型,以及用于确定它们的 typeOf() 函数

>>> Signed
<Signed>
>>> typeOf(5)
<Signed>
>>> typeOf(r_uint(12))
<Unsigned>
>>> typeOf('x')
<Char>

假设我们想构建一个类型“point”,它是一个具有两个整数字段“x”和“y”的结构

>>> POINT = GcStruct('point', ('x', Signed), ('y', Signed))
>>> POINT
<GcStruct point { x: Signed, y: Signed }>

该结构是 GcStruct,这意味着一个可以在堆中分配并最终由某个垃圾收集器释放的结构。(对于我们使用引用计数的平台,请将 GcStruct 视为一个具有额外引用计数字段的结构。)

为 GcStruct 指定名称('point')仅是为了清晰起见:它在表示中使用。

>>> p = malloc(POINT)
>>> p
<* struct point { x=0, y=0 }>
>>> p.x = 5
>>> p.x
5
>>> p
<* struct point { x=5, y=0 }>

malloc() 从堆中分配一个结构,将其初始化为 0(当前),并返回指向它的指针。所有这一切的目的是使用一组非常有限、易于控制的类型,并在这个基本世界中定义像列表这样的类型的实现。 malloc() 函数是一种占位符,最终必须由目标平台的代码生成器提供;但正如我们刚刚在 rpython/rtyper/lltypesystem/lltype.py 中看到的,它的 Python 实现也有效,这主要用于测试、交互式探索等。

malloc() 的参数是结构类型本身,但它返回指向结构的指针,如 typeOf() 所示

>>> typeOf(p)
<* GcStruct point { x: Signed, y: Signed }>

为了创建具有指向其他结构的指针的结构,我们可以显式声明指针类型

>>> typeOf(p) == Ptr(POINT)
True
>>> BIZARRE = GcStruct('bizarre', ('p1', Ptr(POINT)), ('p2', Ptr(POINT)))
>>> b = malloc(BIZARRE)
>>> b.p1
<* None>
>>> b.p1 = b.p2 = p
>>> b.p1.y = 42
>>> b.p2.y
42

低级类型的世界比整数和 GcStructs 更复杂。接下来的页面是参考指南。

基本类型

有符号整数
一个机器字中的有符号整数(在 C 中为 long
无符号整数
一个机器字长内的无符号整数(unsigned long
浮点数
一个64位浮点数(double
字符
单个字符(char
布尔值
一个布尔值
空值
一个常量。用于变量、函数参数、结构体字段等,这些应该从生成的代码中消失。

结构体类型

结构体类型被构建为 rpython.rtyper.lltypesystem.lltype.Struct 的实例

MyStructType = Struct('somename',  ('field1', Type1), ('field2', Type2)...)
MyStructType = GcStruct('somename',  ('field1', Type1), ('field2', Type2)...)

这声明了一个结构体(或 Pascal 的 record),包含指定名称的字段以及给定的类型。字段名称不能以下划线开头。如上所述,您不能直接操作结构体对象,而只能操作指向堆中结构体的指针。

相反,字段本身可以是基本类型、指针类型或容器类型。当一个结构体包含另一个结构体作为字段时,我们说后者在前者中是“内联”的:更大的结构体包含较小的结构体作为其内存布局的一部分。

一个结构体也可以包含一个内联数组(见下文),但只能作为其最后一个字段:在这种情况下,它是一个“可变大小”的结构体,其内存布局以非变量字段开头,以可变数量的数组项结尾。这个数量是在结构体在堆中分配时确定的。可变大小的结构体不能内联到其他结构体中。

GcStructs 具有平台特定的 GC 头(例如,引用计数器);只有这些才能被动态地 malloc()。Struct 的非 GC 版本没有任何头,适用于嵌入(“内联”)到其他结构体中。作为一个例外,GcStruct 可以嵌入到 GcStruct 的第一个字段中:父结构体使用与子结构体相同的 GC 头。

数组类型

数组类型被构建为 rpython.rtyper.lltypesystem.lltype.Array 的实例

MyIntArray = Array(Signed)
MyOtherArray = Array(MyItemType)
MyOtherArray = GcArray(MyItemType)

或者,对于其元素为结构体的数组,可以使用快捷方式

MyArrayType = Array(('field1', Type1), ('field2', Type2)...)

您可以构建元素为基本类型或指针类型,或(非 GC 非可变大小)结构体的数组。

GcArrays 可以被 malloc()。长度必须在调用 malloc() 时指定,并且数组不能调整大小;此长度显式地存储在头文件中。

Array 的非 GC 版本可以作为结构体的最后一个字段使用,以创建可变大小的结构体。然后可以 malloc()整个结构体,并且数组的长度在此期间指定。

指针类型

与 C 一样,指针提供了使引用可修改或可共享所需的间接寻址。指针只能指向结构体、数组或函数(见下文)。如果需要,指向基本类型的指针必须通过指向具有所需类型单个字段的结构体来实现。指针类型通过以下方式声明:

Ptr(TYPE)

在运行时,指向 GC 结构体(GcStruct、GcArray)的指针保存对它们指向内容的引用。指向非 GC 结构体的指针,当其容器被释放时,这些指针也会消失(Struct、Array),必须小心处理:包含它们的较大结构体可能会在指向子结构体的 Ptr 仍在使用时被释放。一般来说,避免传递指向已分配结构体的内联子结构体的指针是一个好主意。(rpython/rtyper/lltypesystem/lltype.py 的测试实现会在一定程度上检查您是否尝试在容器被释放后使用指向结构体的指针,使用弱引用。但指向非 GC 结构体的指针并非官方意义上的弱引用:在它们指向的内容被释放后使用它们会导致崩溃。)

malloc() 操作分配并返回指向新的 GC 结构体或数组的 Ptr。在引用计数实现中,malloc() 会在实际结构体之前分配足够的空间用于引用计数器,并将其初始化为 1。请注意,测试实现还允许 malloc() 使用关键字参数 immortal=True 分配非 GC 结构体或数组。其目的是声明和初始化预构建的数据结构,代码生成器会将其转换为静态的、不朽的、非 GC 的数据。

函数类型

声明

MyFuncType = FuncType([Type1, Type2, ...], ResultType)

声明一个函数类型,它接受给定类型的参数并返回给定类型的结果。所有这些类型必须是基本类型或指针类型。函数类型本身被认为是“容器”类型:如果您愿意,函数包含构成其可执行代码的字节。与结构体和数组一样,它们只能通过指针进行操作。

测试实现允许您通过调用 functionptr(TYPE, name, **attrs) 来“创建”函数。额外的属性以目前尚不完全指定的方式描述函数,但以下属性可能存在

_callable一个 Python 可调用对象,通常是函数对象。
graph函数的流程图。

不透明类型

不透明类型表示以后端特定方式实现的数据。无法检查或操作此数据。

有一个预定义的不透明类型 RuntimeTypeInfo;在运行时,类型为 RuntimeTypeInfo 的值表示低级类型。在实践中,能够表示 GcStruct 和 GcArray 类型可能就足够了。如果我们有一个类型为 Ptr(S) 的指针,它可以在运行时指向单独分配的 S,或者指向更大分配结构体的第一个 S 字段,这将非常有用。有关它指向的确切较大类型的的信息可以计算或作为 Ptr(RuntimeTypeInfo) 传递。可以在 Ptr(RuntimeTypeInfo) 上使用指针相等性来检查运行时的类型。

目前,出于内存管理目的,某些后端实际上需要在以下情况下获得此信息:当 GcStruct 的第一个字段是另一个 GcStruct 时。引用计数后端需要能够知道何时指向较小结构体的指针实际上指向较大结构体,以便它还可以递减额外字段的引用计数。根据具体情况,可以在无需在每个较小 GcStruct 实例中存储标志的情况下重建此信息。例如,类层次结构的实例可以通过嵌套的 GcStructs 实现,子类的实例通过将实例的父部分嵌入为第一个字段来扩展父类的实例。在这种情况下,可能已经有一种方法可以知道实例的运行时类(例如,vtable 指针),但后端无法猜测这一点。这就是最初引入 RuntimeTypeInfo 的原因:在创建 GcStruct 之后,应调用函数 attachRuntimeTypeInfo() 以将签名为 Ptr(GcStruct) -> Ptr(RuntimeTypeInfo) 的低级函数附加到 GcStruct。此函数将由后端编译并在运行时自动调用。在上面的示例中,它将遵循 vtable 指针并从 vtable 本身获取不透明的 Ptr(RuntimeTypeInfo)。(引用计数 GenC 后端使用指向释放函数的指针作为不透明的 RuntimeTypeInfo。)

实现 RPython 类型

如上所述,RPython 类型(例如“列表”)以某种“受限的受限 Python”格式实现,仅操作低级类型,如 malloc() 及其相关函数的测试实现中提供的。然后发生的事情是,相同的(经过测试的!)非常低级的 Python 代码——看起来确实很像 C——然后被转换为流程图并与用户程序的其余部分集成。换句话说,我们将两个注释为 SomeList 的变量之间的 add 操作替换为调用此非常低级列表连接的 direct_call 操作。

然后,此列表连接流程图像往常一样进行注释,但有一点不同:必须向注释器教授 malloc() 以及如何操作由此获得的指针。这会生成一个流程图,希望该流程图完全使用 SomePtr() 注释进行注释。仅为此情况引入的 SomePtr 直接映射到低级指针类型。这是注释器允许其执行非常低级代码片段的类型推断所需的唯一更改。

例如,请参见 rpython/rtyper/rlist.py

HighLevelOp 接口

在没有关于如何实现 RPython 类型的更广泛文档的情况下,以下是出现在各处的“hop”参数的接口和预期用法。“hop”是 HighLevelOp 实例,它表示必须转换为一个或多个低级操作的单个高级操作。

hop.llops
一个类似列表的对象,记录对应于当前块的高级操作的低级操作。
hop.genop(opname, list_of_variables, resulttype=resulttype)
将低级操作附加到 hop.llops。该操作具有给定的 opname 和参数,并返回给定的低级 resulttype。参数应来自下面描述的 hop.input*() 函数。
hop.gendirectcall(ll_function, var1, var2...)
类似于 hop.genop(),但会生成一个 direct_call 操作,调用给定的低级函数,该函数会根据输入参数自动注释为低级类型。
hop.inputargs(r1, r2...)
读取作为操作参数的高级变量和常量,并在需要时转换它们,以便它们具有指定的表示形式。您必须提供与操作参数一样多的表示形式。返回一个(可能已转换的)变量和常量的列表。
hop.inputarg(r, arg=i)
与 inputargs() 相同,但仅转换并返回第 i 个参数。

hop.inputconst(lltype, value)
返回一个具有低级类型和值的常量。

操作 HighLevelOp 实例(例如,用于插入“self”隐式参数以转换方法调用)。

hop.copy()
返回一个可以利用以下函数进行操作的新副本。
hop.r_s_popfirstarg()
删除高级操作的第一个参数。这实际上并没有改变源 SpaceOperation,而是以一种方式修改了“hop”,使得像 inputargs() 这样的方法不再看到被删除的参数。
hop.v_s_insertfirstarg(v_newfirstarg, s_newfirstarg)
在 hop 前面插入一个参数。它必须由一个变量(如 hop.genop() 的调用)和相应的注释指定。
hop.swap_fst_snd_args()
自描述。

异常处理

hop.has_implicit_exception(cls)
检查 hop 是否处于捕获异常“cls”的分支范围内。这对于像“getitem”这样的高级操作很有用,这些操作根据是否应该检查 IndexError 而具有多个低级等效项。调用 has_implicit_exception() 也有副作用:rtyper 记录此异常正在被显式处理。
hop.exception_is_here()
在生成 llop 之前,无需参数即可调用。这意味着相关联的 llop 应该由异常捕获来保护。如果之前调用了 has_implicit_exception(),则 exception_is_here() 会验证图中的所有 except 链接是否都已使用 has_implicit_exception() 进行检查。如果从未调用过 has_implicit_exception(),则不会验证此项——这对“direct_call”和其他可以引发任何异常的操作很有用。
hop.exception_cannot_occur()
RTyper 通常会验证是否确实为处于异常捕获链接范围内的每个高级操作调用了一次 exception_is_here()。通过说 exception_cannot_occur(),表示在所有这些操作之后,此特定操作不会引发任何异常。(意外的异常链接可能会附加到流图上;例如,try:finally: 块内的任何方法调用都将有一个异常分支到 finally 部分,如果调用了 exception_cannot_occur(),则只有 RTyper 可以删除该分支。)

LL 解释器

LL 解释器是一段简单的代码,能够解释流图。这对于测试目的非常有用,尤其是在您使用 RPython 类型检查器时。它最常用的接口是文件 rpython/rtyper/test/test_llinterp.py 中的 interpret 函数。它接受一个函数和一个参数列表作为参数,该函数应该使用这些参数来调用。然后它生成流图,根据传递给它的参数的类型对其进行注释,并在结果上运行 LL 解释器。示例

def test_invert():
    def f(x):
        return ~x
    res = interpret(f, [3])
    assert res == ~3

此外,还有一个函数 interpret_raises,其行为类似于 py.test.raises。它以异常作为第一个参数,要调用的函数作为第二个参数,以及函数参数列表作为第三个参数。示例

def test_raise():
    def raise_exception(i):
        if i == 42:
            raise IndexError
        elif i == 43:
            raise ValueError
        return i
    res = interpret(raise_exception, [41])
    assert res == 41
    interpret_raises(IndexError, raise_exception, [42])
    interpret_raises(ValueError, raise_exception, [43])