RPython 타이퍼(Typer)

RPython 타이퍼는 rpython/rtyper/ 디렉터리에 있습니다.

개요

RPython 타이퍼(Typer)는 애너테이터(Annotator)와 코드 생성기 사이를 잇는 다리입니다. 애너테이션은 애너테이터(Annotator)의 것으로, 리스트나 사용자 정의 클래스의 인스턴스와 같은 RPython 타입을 기술한다는 점에서 고수준입니다.

코드를 생성하려면 이러한 고수준 주석을 대상 언어의 저수준 모델로 표현해야 합니다. C의 경우, 이는 구조체와 포인터, 배열을 의미합니다. Typer는 각 주석에 적합한 저수준 타입을 결정하는 동시에, 제어 흐름 그래프의 각 고수준 연산을 하나 또는 몇 개의 저수준 연산으로 대체합니다. 저수준 타입과 마찬가지로, 저수준 연산 역시 구조체의 필드를 읽거나 쓰는 것과 같이 상당히 제한된 집합만 존재합니다.

이론상으로는 이 단계는 선택적입니다. 코드 생성기가 고수준 타입을 직접 읽을 수 있을지도 모릅니다. 그러나 저희의 경험에 따르면 이는 실용적일 가능성이 매우 낮습니다. 고수준 타입을 저수준 타입으로 “컴파일”하는 일은 예상보다 훨씬 지저분합니다. 이것이 바로 이 단계를 명시적으로 만들고 한 곳에 격리시킨 동기였습니다. RTyping 이후에는 그래프에 이미 대상 언어 수준에 존재하는 연산만 포함되어, 코드 생성기의 작업이 훨씬 단순해집니다.

예시: 정수 연산

정수 연산이 가장 쉽습니다. 다음 연산을 포함하는 그래프를 가정합니다:

v3 = add(v1, v2)

어노테이트된:

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

그렇다면 당연히 이를 타입 지정하여 다음으로 대체하고자 합니다:

v3 = int_add(v1, v2)

여기서 – C 표기법으로 – 세 변수 v1, v2, v3는 모두 int로 타입이 지정됩니다. 이는 v1, v2, v3(Variable 또는 어쩌면 Constant의 인스턴스일 수 있음)에 concretetype 속성을 붙임으로써 이루어집니다. 우리 모델에서, 이 concretetyperpython.rtyper.lltypesystem.lltype.Signed입니다. 물론, add라는 연산을 int_add로 바꾸는 목적은 코드 생성기가 그것이 어떤 종류의 덧셈(혹은 어쩌면 연결?)을 의미하는지 더 이상 신경 쓸 필요가 없다는 데 있습니다.

프로세스 상세

RPython 타이퍼(Typer)는 Annotator와 유사한 구조를 가지고 있으며, 둘 다 흐름 그래프(flow graph)의 각 블록을 차례로 검토하면서 각 연산에 대해 분석을 수행합니다. 두 경우 모두 연산에 대한 분석은 그 입력 인자들의 애노테이션에 의존합니다. 이는 소스 파일에서 동일한 __extend__ 구문을 사용하는 데서 드러납니다(예를 들어 rpython/annotator/binaryop.pyrpython/rtyper/rint.py를 비교해 보십시오).

하지만 비유는 여기까지입니다: Annotator가 실행되는 동안에는 애너테이션을 계산하는 중이므로, 고정점(fixpoint)에 도달할 때까지 재순회하며 일반화해야 할 수도 있습니다. 이와 반대로 Typer는 Annotator가 계산한 최종 애너테이션을, 전역적으로 일관성이 있다고 가정한 채 변경하지 않고 사용합니다. 재순회할 필요는 없습니다: Typer는 각 블록을 한 번만 고려합니다. 그리고 Annotator와 달리 Typer는 각 연산을 몇몇 저수준 연산으로 대체함으로써 흐름 그래프(flow graph)를 완전히 수정합니다.

연산을 대체하는 것 외에도, RTyper는 흐름 그래프의 모든 변수와 상수에 concretetype 속성을 생성하며, 이 속성은 코드 생성기에게 각각에 사용할 타입을 알려줍니다. 이 속성은 아래에서 설명하듯이 저수준 타입입니다.

표현

표현(Representation) – 즉 Repr 클래스들 – 은 RTyper가 사용하는 가장 중요한 내부 클래스입니다. (이들은 “구현 세부사항(implementation detail)”이라는 의미에서 내부적이며, RTyper가 끝나면 그 인스턴스는 그냥 사라집니다. 코드 생성기는 Repr 인스턴스가 아니라 저수준 타입concretetype 속성만 사용해야 합니다.)

표현(representation)은 특정 SomeXxx() 어노테이션을 특정 저수준 타입에 매핑하는 모든 로직을 포함합니다. 현재로서는, RTyper는 각 SomeXxx() 인스턴스가 오직 하나의 “정규(canonical)” 표현만 필요하다고 가정합니다. 예를 들어, SomeInteger()로 어노테이션된 모든 변수는 IntegerRepr 표현을 통해 Signed 저수준 타입에 대응됩니다. 더 미묘하게는, SomeList()로 어노테이션된 변수는 올바른 타입의 항목 배열을 담는 구조체에 대응되거나 – 해당 리스트가 상수 스텝을 갖는 단순한 range()인 경우 – start와 stop 필드만 가진 구조체에 대응될 수 있습니다.

이 예시는 두 표현이 동일한 고수준 연산에 대해 매우 다른 저수준 구현이 필요할 수 있음을 보여줍니다. 이것이 바로 표현을 명시적인 객체로 전환하는 이유입니다.

기본 Repr 클래스는 rpython/rtyper/rmodel.py에 정의되어 있습니다. 대부분의 rpython/r*.py 파일은 Repr의 하위 클래스를 하나 또는 몇 개 정의합니다. RTyper의 getrepr() 메서드는 SomeXxx() 인스턴스마다 단일 Repr 인스턴스를 만들어 캐시합니다. 게다가 서로 같은 두 SomeXxx() 인스턴스는 동일한 Repr 인스턴스를 얻습니다.

Repr 인스턴스의 핵심 속성은 lowleveltype이라고 하며, 이는 이 표현(representation)이 부여된 Variable들의 concretetype 속성에 복사되는 것입니다. RTyper는 또한 저수준 연산에서 사용되는 방식에 맞춰 Constant에 대한 concretetype도 계산합니다(예를 들어, int_add(x, 1)concretetype=SignedConstant(1)을 필요로 합니다).

lowleveltype에 더해, 각 Repr 서브클래스는 rtype_op_xxx()라는 메서드 집합을 제공하며, 이는 각 고수준 연산 op_xxx가 어떻게 저수준 연산으로 변환되는지를 정의합니다.

저수준 타입

RPython Typer는 C와 같은 다양한 대상 언어에 상당히 직접적으로 대응할 수 있다고 여겨지는 표준 저수준 모델을 사용합니다. 이 모델은 rpython/rtyper/lltypesystem/lltype.py의 앞부분에 구현되어 있습니다.

테스트 목적으로, rpython/rtyper/lltypesystem/lltype.py의 두 번째 부분은 이 타입들의 실행 가능한 구현입니다. 이를 통해 malloc() 함수를 사용하여 구조체와 배열을 획득하고 조작하는 평범한 Python 코드를 작성하고 테스트할 수 있습니다. 예를 들어, 이는 ‘list’와 같은 RPython 타입을 그 연산 및 메서드와 함께 구현하고 테스트하는 데 유용합니다.

기본 가정은 변수(즉, 지역 변수와 함수 인자, 반환값)가 모두 “단순한” 값을, 기본적으로는 정수 또는 포인터만을 담는다는 것입니다. 모든 “컨테이너” 자료 구조(구조체와 배열)는 힙에 할당되며, 항상 포인터를 통해서만 다뤄집니다. (struct 타입의 지역 변수라는 C의 개념에 대응하는 것은 없습니다.)

다음은 간단한 둘러보기입니다:

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

여기 몇 가지 원시 저수준 타입과 이를 알아내는 typeOf() 함수가 있습니다:

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

정수 필드 “x”와 “y”를 가진 구조체인 “point” 타입을 만들고 싶다고 가정해 보겠습니다:

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

이 구조체는 GcStruct이며, 이는 힙에 할당되었다가 결국 어떤 가비지 컬렉터에 의해 해제될 수 있는 구조체를 의미합니다. (참조 카운팅을 사용하는 플랫폼의 경우, GcStruct를 참조 카운터 필드가 추가된 구조체로 생각하십시오.)

GcStruct에 이름(‘point’)을 부여하는 것은 단지 명확성을 위한 것으로, 이는 표현(representation)에서 사용됩니다.

>>> 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으로 초기화한 뒤, 그것을 가리키는 포인터를 반환합니다. 이 모든 것의 요점은 매우 제한적이고 쉽게 제어할 수 있는 타입 집합으로 작업하며, 이 기초적인 세계에서 list와 같은 타입의 구현을 정의하는 것입니다. malloc() 함수는 일종의 플레이스홀더로서, 결국에는 대상 플랫폼용 코드 생성기가 제공해야 합니다. 하지만 방금 살펴본 것처럼 rpython/rtyper/lltypesystem/lltype.py에 있는 파이썬 구현도 작동하며, 이는 주로 테스트, 대화형 탐색 등에 유용합니다.

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보다 더 복잡합니다. 다음 페이지들은 참조 가이드입니다.

원시 타입(Primitive Types)

Signed
하나의 머신 워드에 담기는 부호 있는 정수(C 언어의 long형)
부호 없는 정수(Unsigned)
한 머신 워드(unsigned long) 크기의 부호 없는 정수
Float
64비트 부동소수점(double)
Char
단일 문자(char)
Bool
불리언 값
Void
상수입니다. 생성된 코드에서 사라져야 하는 변수, 함수 인자, 구조체 필드 등을 위한 것입니다.

구조체 타입

구조체 타입은 rpython.rtyper.lltypesystem.lltype.Struct의 인스턴스로 구성됩니다:

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

이것은 주어진 타입을 가진 지정된 이름의 필드를 포함하는 구조체(또는 Pascal record)를 선언합니다. 필드 이름은 밑줄로 시작할 수 없습니다. 위에서 언급했듯이, 구조체 객체를 직접 조작할 수는 없으며, 힙에 있는 구조체에 대한 포인터만 조작할 수 있습니다.

반면, 필드 자체는 원시(primitive), 포인터, 또는 컨테이너 타입일 수 있습니다. 어떤 구조체가 다른 구조체를 필드로 포함할 경우, 후자가 전자에 “인라인(inlined)”되었다고 말합니다. 즉, 더 큰 구조체가 메모리 레이아웃의 일부로서 더 작은 구조체를 포함합니다.

구조체는 인라인 배열을 포함할 수도 있지만(아래 참조), 마지막 필드로만 가능합니다. 이 경우 이는 “가변 크기” 구조체이며, 메모리 레이아웃은 비가변 필드로 시작해 가변 개수의 배열 항목으로 끝납니다. 이 개수는 구조체가 힙에 할당될 때 결정됩니다. 가변 크기 구조체는 다른 구조체에 인라인될 수 없습니다.

GcStruct는 플랫폼별 GC 헤더(예: 참조 카운터)를 가지며, 오직 이러한 GcStruct만 동적으로 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)에 대한 포인터는 자신이 가리키는 대상에 대한 참조를 유지합니다. 컨테이너가 해제될 때 사라질 수 있는 non-GC 구조체(Struct, Array)에 대한 포인터는 주의해서 다루어야 합니다: 하위 구조체에 대한 Ptr이 여전히 사용 중인 동안, 그것이 속한 더 큰 구조체가 해제될 수 있습니다. 일반적으로, malloc()된 구조체의 인라인된 하위 구조체에 대한 포인터를 여기저기 전달하는 것은 피하는 것이 좋습니다. (rpython/rtyper/lltypesystem/lltype.py의 테스트 구현은 약한 참조(weak reference)를 사용하여, 컨테이너가 해제된 후 구조체에 대한 포인터를 사용하려고 하지 않는지 어느 정도 검사합니다. 하지만 non-GC 구조체에 대한 포인터는 공식적으로 약한 참조(weak reference)로 의도된 것이 아닙니다: 가리키는 대상이 해제된 후 그것을 사용하면 그냥 크래시가 발생합니다.)

malloc() 연산은 새로운 GC 구조체나 배열에 대한 Ptr을 할당하고 반환합니다. 참조 카운팅 구현에서는 malloc()이 실제 구조체 앞에 참조 카운터를 위한 충분한 공간을 할당하고, 이를 1로 초기화합니다. 테스트용 구현에서는 malloc()이 키워드 인자 immortal=True로 non-GC 구조체나 배열을 할당하는 것도 허용한다는 점에 유의하십시오. 이 기능의 목적은 코드 생성기가 정적이고 불멸(immortal)인 non-GC 데이터로 변환할 사전 구축된(prebuilt) 데이터 구조체를 선언하고 초기화하는 것입니다.

함수 타입

선언:

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

주어진 타입의 인자를 받아 주어진 타입의 결과를 반환하는 함수 타입을 선언합니다. 이 모든 타입은 원시 타입이거나 포인터여야 합니다. 함수 타입 자체는 “컨테이너(container)” 타입으로 간주됩니다. 즉, 말하자면 함수는 실행 코드를 구성하는 바이트를 담고 있습니다. 구조체 및 배열과 마찬가지로, 이들은 포인터를 통해서만 조작할 수 있습니다.

테스트용 구현은 functionptr(TYPE, name, **attrs)를 호출하여 함수를 “생성”할 수 있게 해줍니다. 추가 속성들은 아직 완전히 명세되지 않은 방식으로 함수를 설명하지만, 다음 속성들이 존재할 수도 있습니다:

_callable:a Python callable, typically a function object.
graph:the flow graph of the function.

불투명 타입

불투명(opaque) 타입은 백엔드에 특화된 방식으로 구현된 데이터를 나타냅니다. 이 데이터는 검사하거나 조작할 수 없습니다.

미리 정의된 불투명 타입 RuntimeTypeInfo가 있습니다. 실행 시점에 RuntimeTypeInfo 타입의 값은 저수준 타입을 나타냅니다. 실제로는 GcStruct와 GcArray 타입을 나타낼 수 있는 것으로 충분할 것입니다. 이것은 Ptr(S) 타입의 포인터가 있을 때 유용한데, 이 포인터는 실행 시점에 malloc으로 할당된 S 자체를 가리킬 수도 있고, malloc으로 할당된 더 큰 구조체의 첫 번째 필드인 S를 가리킬 수도 있습니다. 그것이 가리키는 정확한 더 큰 타입에 대한 정보는 Ptr(RuntimeTypeInfo)로 계산되거나 전달될 수 있습니다. Ptr(RuntimeTypeInfo)에 대한 포인터 동등성 비교는 실행 시점에 타입을 확인하는 데 사용될 수 있습니다.

현재 메모리 관리 목적상, 일부 백엔드는 다음과 같은 상황에서 실제로 이러한 정보가 실행 시점에 사용 가능해야 합니다: GcStruct가 다른 GcStruct를 첫 번째 필드로 가지는 경우입니다. 참조 카운팅(reference-counting) 백엔드는 추가 필드들도 decref할 수 있도록, 더 작은 구조체에 대한 포인터가 실제로는 더 큰 구조체를 가리키는 시점을 알 수 있어야 합니다. 상황에 따라서는, 더 작은 GcStruct의 모든 인스턴스에 플래그를 저장하지 않고도 이 정보를 재구성할 수 있습니다. 예를 들어, 클래스 계층의 인스턴스는 중첩된 GcStruct들로 구현될 수 있으며, 이때 서브클래스의 인스턴스는 인스턴스의 부모 부분을 첫 번째 필드로 포함시킴으로써 부모 클래스의 인스턴스를 확장합니다. 이 경우, 아마도 이미 인스턴스의 실행 시점 클래스를 알아낼 방법(예: vtable 포인터)이 있겠지만, 백엔드는 이를 추측할 수 없습니다. 바로 이것이 RuntimeTypeInfo가 원래 도입된 이유입니다: GcStruct가 생성된 직후, attachRuntimeTypeInfo() 함수를 호출하여 시그니처가 Ptr(GcStruct) -> Ptr(RuntimeTypeInfo)인 저수준 함수를 해당 GcStruct에 붙여야 합니다. 이 함수는 백엔드에 의해 컴파일되어 실행 시점에 자동으로 호출됩니다. 위 예시에서는, vtable 포인터를 따라가서 vtable 자체로부터 불투명한 Ptr(RuntimeTypeInfo)를 가져오게 됩니다. (참조 카운팅 GenC 백엔드는 할당 해제 함수에 대한 포인터를 불투명한 RuntimeTypeInfo로 사용합니다.)

RPython 타입 구현

위에서 암시했듯이, RPython 타입(예: ‘list’)은 malloc()과 그 관련 함수들의 테스트용 구현이 제공하는 저수준 타입만을 다루는 일종의 “restricted-restricted Python” 형식으로 구현됩니다. 그러면 발생하는 일은, 바로 그 (테스트를 거친!) 매우 저수준의 파이썬 코드 – 실제로 C와 매우 흡사해 보이는 – 가 플로우 그래프(flow graph)로 변환되어 사용자 프로그램의 나머지 부분과 통합된다는 것입니다. 다시 말해, SomeList로 어노테이션된 두 변수 사이의 add와 같은 연산을, 이 매우 저수준의 리스트 연결(concatenation)을 호출하는 direct_call 연산으로 대체합니다.

이 리스트 연결(list concatenation) 흐름 그래프(flow graph)는 평소와 마찬가지로 주석이 달리지만, 한 가지 차이점이 있습니다: 어노테이터(Annotator)는 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...)
연산의 인자인 고수준 Variable과 Constant를 읽고, 필요하다면 지정된 표현(representation)을 갖도록 변환합니다. 연산이 가진 인자 수만큼 표현(representation)을 제공해야 합니다. (새로 변환되었을 수도 있는) Variable과 Constant의 리스트를 반환합니다.
hop.inputarg(r, arg=i)
inputargs()와 동일하지만, i번째 인자만 변환하여 반환합니다.
hop.inputconst(lltype, value)
낮은 수준의 타입과 값을 가진 Constant를 반환합니다.

HighLevelOp 인스턴스 조작(예를 들어 메서드 호출을 번역(translation)할 때 암묵적 인자인 ‘self’를 삽입하는 데 사용됩니다):

hop.copy()
아래의 함수들로 조작할 수 있는 새 복사본을 반환합니다.
hop.r_s_popfirstarg()
고수준 연산(operation)의 첫 번째 인자를 제거합니다. 이는 실제로 원본 SpaceOperation을 변경하지는 않지만, inputargs() 같은 메서드가 제거된 인자를 더 이상 보지 못하도록 ‘hop’을 수정합니다.
hop.v_s_insertfirstarg(v_newfirstarg, s_newfirstarg)
hop 앞에 인자를 삽입합니다. 이는 hop.genop() 호출에서와 마찬가지로 Variable과 그에 대응하는 annotation으로 지정해야 합니다.
hop.swap_fst_snd_args()
자명합니다.

예외 처리:

hop.has_implicit_exception(cls)
hop이 예외 ‘cls’를 처리하는 분기(branch)의 범위 안에 있는지 확인합니다. 이는 IndexError를 검사해야 하는지 여부에 따라 여러 저수준 등가물을 갖는 ‘getitem’과 같은 고수준 연산에 유용합니다. has_implicit_exception()을 호출하는 것도 부수 효과(side effect)를 가집니다: rtyper가 이 예외를 명시적으로 처리하고 있음을 기록합니다.
hop.exception_is_here()
llop가 생성되기 직전에 인자 없이 호출됩니다. 이는 해당 llop가 예외 포착(exception catching)에 의해 보호되어야 하는 대상이 됨을 의미합니다. 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 분기를 가지게 되는데, 이는 exception_cannot_occur()가 호출된 경우에만 RTyper가 제거할 수 있습니다.)

LLInterpreter

LLInterpreter는 흐름 그래프(flow graph)를 해석할 수 있는 간단한 코드입니다. 이는 테스트 목적으로, 특히 RPython Typer 작업을 할 때 매우 유용합니다. 이를 위한 가장 유용한 인터페이스는 파일 rpython/rtyper/test/test_llinterp.py에 있는 interpret 함수입니다. 이것은 인자로 함수 하나와, 그 함수를 호출할 때 사용될 인자 목록을 받습니다. 그런 다음 흐름 그래프를 생성하고, 전달된 인자들의 타입에 따라 주석을 달며, 그 결과에 대해 LLInterpreter를 실행합니다. 예제:

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])