RPython 타이퍼(Typer)¶
Contents
RPython 타이퍼는 rpython/rtyper/ 디렉터리에 있습니다.
개요¶
RPython 타이퍼(Typer)는 애너테이터(Annotator)와 코드 생성기를 잇는 다리입니다. 애너테이터(Annotator)의 애너테이션은 리스트나 사용자 정의 클래스의 인스턴스 같은 RPython 타입을 기술한다는 점에서 고수준입니다.
코드를 생성하려면 이러한 고수준 어노테이션을 대상 언어의 저수준 모델로 표현해야 합니다. C의 경우 이는 구조체와 포인터, 배열을 의미합니다. 타이퍼는 각 어노테이션에 알맞은 저수준 타입을 결정하는 동시에, 제어 흐름 그래프 안의 각 고수준 연산을 하나 또는 몇 개의 저수준 연산으로 대체합니다. 저수준 타입과 마찬가지로 저수준 연산도 구조체의 필드를 읽거나 필드에 쓰는 것과 같은, 상당히 제한된 집합만 존재합니다.
이론상 이 단계는 선택 사항입니다. 코드 생성기가 고수준 타입을 직접 읽을 수도 있습니다. 그러나 저희 경험에 비추어 보면 이것이 실용적일 가능성은 매우 낮습니다. 고수준 타입을 저수준 타입으로 “컴파일”하는 일은 예상보다 상당히 지저분합니다. 이것이 이 단계를 명시적으로 만들어 한곳에 격리한 동기였습니다. RTyping을 거치고 나면 그래프에는 이미 대상 언어 수준에 있는 연산만 남으므로 코드 생성기의 작업이 훨씬 단순해집니다.
예제: 정수 연산¶
정수 연산이 가장 쉽습니다. 다음 연산을 포함하는 그래프가 있다고 가정합시다:
v3 = add(v1, v2)
어노테이션된:
v1 -> SomeInteger()
v2 -> SomeInteger()
v3 -> SomeInteger()
그렇다면 당연히 이것에 타입을 지정하고 다음으로 대체하고자 합니다:
v3 = int_add(v1, v2)
여기서 C 표기법으로 말하면 세 변수 v1, v2, v3는 모두 int 타입입니다. 이는 v1, v2, v3에 concretetype 속성을 붙여서 이루어집니다(v1, v2, v3는 Variable(변수)의 인스턴스이거나 Constant(상수)의 인스턴스일 수 있습니다). 우리 모델에서 이 concretetype은 rpython.rtyper.lltypesystem.lltype.Signed입니다. 물론 add라는 연산을 int_add로 대체하는 목적은, 그 연산이 어떤 종류의 덧셈(혹은 어쩌면 연결?)을 뜻하는지 코드 생성기가 더 이상 신경 쓸 필요가 없게 하는 데 있습니다.
프로세스 상세 설명¶
RPython 타이퍼(Typer)는 Annotator와 비슷한 구조를 가지고 있습니다. 둘 다 흐름 그래프의 각 블록을 차례로 살펴보며 각 연산에 대해 분석을 수행합니다. 두 경우 모두 연산의 분석은 그 연산에 입력되는 인자들의 어노테이션에 따라 달라집니다. 이 점은 소스 파일에서 같은 __extend__ 구문을 사용한다는 데서 드러납니다(예를 들어 rpython/annotator/binaryop.py와 rpython/rtyper/rint.py를 비교해 보십시오).
하지만 이 비유는 여기까지만 성립합니다. 어노테이터는 실행되는 동안 어노테이션을 계산하는 중이므로, 고정점에 도달할 때까지 다시 흐름을 따라가며(reflow) 일반화해야 할 수도 있습니다. 반면 타이퍼는 어노테이터가 계산한 최종 어노테이션을 바꾸지 않고 그대로 사용하며, 이 어노테이션이 전역적으로 일관된다고 가정합니다. 다시 흐름을 따라갈 필요가 없으므로 타이퍼는 각 블록을 한 번만 처리합니다. 또한 타이퍼는 어노테이터와 달리 각 연산을 몇 개의 저수준 연산으로 바꾸어 흐름 그래프를 완전히 수정합니다.
RTyper는 연산을 대체할 뿐만 아니라 흐름 그래프의 모든 변수(Variable)와 상수(Constant)에 concretetype 속성을 만듭니다. 이 속성은 각 변수와 상수에 어떤 타입을 써야 하는지 코드 생성기에 알려 줍니다. 이 속성은 아래에서 설명하는 저수준 타입입니다.
표현¶
표현(representation), 즉 Repr 클래스는 RTyper가 사용하는 가장 중요한 내부 클래스입니다. (이 클래스들이 내부 클래스라는 것은 “구현 세부 사항”이며 RTyper가 작업을 마치면 그 인스턴스가 그냥 사라진다는 의미입니다. 코드 생성기는 concretetype 속성만 사용해야 하며, 이 속성은 Repr 인스턴스가 아니라 Low-Level Types입니다.)
표현(representation)에는 특정 SomeXxx() 어노테이션을 특정 저수준 타입으로 매핑하는 모든 로직이 들어 있습니다. 현재로서는 RTyper가 각 SomeXxx() 인스턴스에 하나의 “정규(canonical)” 표현만 있으면 된다고 가정합니다. 예를 들어 SomeInteger()로 어노테이션된 모든 변수는 IntegerRepr 표현을 통해 Signed 저수준 타입에 대응합니다. 좀 더 미묘한 경우로, SomeList()로 어노테이션된 변수는 올바른 타입의 항목 배열을 담는 구조체에 대응할 수도 있고, – 해당 리스트가 상수 step을 갖는 range()일 뿐이라면 – start와 stop 필드만 있는 구조체에 대응할 수도 있습니다.
이 예제는 두 표현이 같은 고수준 연산에 대해 서로 매우 다른 저수준 구현을 필요로 할 수 있음을 보여 줍니다. 이것이 바로 표현을 명시적인 객체로 만드는 이유입니다.
기본 Repr 클래스는 rpython/rtyper/rmodel.py에 정의되어 있습니다. rpython/r*.py 파일 대부분은 Repr의 서브클래스를 하나 또는 몇 개 정의합니다. RTyper의 getrepr() 메서드는 SomeXxx() 인스턴스마다 Repr 인스턴스를 하나씩 빌드하여 캐시합니다. 또한 서로 같은 두 SomeXxx() 인스턴스는 동일한 Repr 인스턴스를 받습니다.
Repr 인스턴스의 핵심 속성은 lowleveltype이라고 하며, 이 표현이 부여된 변수(Variable)의 concretetype 속성에 이 값이 복사됩니다. RTyper는 상수(Constant)에 대해서도 저수준 연산에서 사용되는 방식에 맞도록 concretetype을 계산합니다(예를 들어 int_add(x, 1)에는 concretetype=Signed인 Constant(1)이 필요합니다).
lowleveltype 외에도 각 Repr 서브클래스는 rtype_op_xxx()라는 메서드 집합을 제공합니다. 이 메서드들은 각 고수준 연산 op_xxx가 저수준 연산으로 어떻게 변환되는지를 정의합니다.
저수준 타입¶
RPython 타이퍼는 표준 저수준 모델을 사용하며, 저희는 이 모델이 C와 같은 다양한 대상 언어에 상당히 직접적으로 대응할 수 있다고 생각합니다. 이 모델은 rpython/rtyper/lltypesystem/lltype.py의 앞부분에 구현되어 있습니다.
rpython/rtyper/lltypesystem/lltype.py의 두 번째 부분은 테스트를 위해 이 타입들을 실제로 실행할 수 있도록 구현한 것입니다. 이를 사용하면 malloc() 함수로 구조체와 배열을 얻고 조작하는 일반 Python 코드를 작성하고 테스트할 수 있습니다. 예를 들어 ‘list’와 같은 RPython 타입을 그 연산 및 메서드와 함께 구현하고 테스트할 때 유용합니다.
기본 가정은 변수(즉, 지역 변수, 함수 인자, 반환값)가 모두 “단순한” 값, 기본적으로 정수나 포인터만을 담는다는 것입니다. 모든 “컨테이너” 자료 구조(struct와 array)는 힙에 할당되며, 항상 포인터를 통해 다루어집니다. (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에 있는 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
하지만 저수준 타입의 세계는 정수와 GcStruct보다 더 복잡합니다. 다음 페이지들은 참조 안내서입니다.
원시 타입¶
- 서명
- 머신 워드 하나 크기의 부호 있는 정수(C에서는
long) - 부호 없음
- 머신 워드 하나 크기의 부호 없는 정수(
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()으로 동적 할당할 수 있습니다. GC가 아닌 버전의 Struct에는 헤더가 전혀 없으며, 다른 구조체 안에 임베드(“인라인”)하기에 적합합니다. 예외적으로, GcStruct는 다른 GcStruct의 첫 번째 필드로 임베드될 수 있습니다. 이때 부모 구조체는 하위 구조체와 같은 GC 헤더를 사용합니다.
배열 타입¶
배열 타입은 rpython.rtyper.lltypesystem.lltype.Array의 인스턴스로 만들어집니다:
MyIntArray = Array(Signed)
MyOtherArray = Array(MyItemType)
MyOtherArray = GcArray(MyItemType)
또는 항목이 구조체인 배열의 경우에는 다음과 같이 줄여 쓸 수도 있습니다:
MyArrayType = Array(('field1', Type1), ('field2', Type2)...)
항목이 원시 타입이나 포인터 타입, 또는 (GC가 아니고 가변 크기가 아닌) 구조체인 배열을 빌드할 수 있습니다.
GcArray는 malloc()으로 할당할 수 있습니다. malloc()을 호출할 때 길이를 지정해야 하며, 배열의 크기는 변경할 수 없습니다. 이 길이는 헤더에 명시적으로 저장됩니다.
비 GC 버전의 Array는 구조체의 마지막 필드로 사용하여 크기가 변수로 정해지는 가변 크기 구조체를 만들 수 있습니다. 그런 다음 구조체 전체를 malloc()으로 할당할 수 있으며, 배열의 길이는 이때 지정합니다.
포인터 타입¶
C에서와 마찬가지로, 포인터는 참조를 수정 가능하거나 공유 가능하게 만드는 데 필요한 간접 참조를 제공합니다. 포인터는 구조체, 배열 또는 함수만 가리킬 수 있습니다(아래 참조). 원시 타입에 대한 포인터가 필요하면, 필요한 타입의 필드 하나만 가진 구조체를 가리키는 방식으로 만들어야 합니다. 포인터 타입은 다음과 같이 선언합니다.:
Ptr(TYPE)
런타임에 GC 구조체(GcStruct, GcArray)를 가리키는 포인터는 자신이 가리키는 대상에 대한 참조를 유지합니다. 컨테이너가 할당 해제될 때 함께 사라질 수 있는 비GC 구조체(Struct, Array)를 가리키는 포인터는 주의해서 다루어야 합니다. 하위 구조체를 가리키는 Ptr 포인터가 아직 사용 중인데도 그 하위 구조체를 포함하는 더 큰 구조체가 해제될 수 있기 때문입니다. 일반적으로 malloc()으로 할당한 구조체에 인라인된 하위 구조체의 포인터를 여기저기 전달하지 않는 것이 좋습니다. (rpython/rtyper/lltypesystem/lltype.py의 테스트용 구현은 약한 참조(weak reference)를 사용하여, 컨테이너가 해제된 뒤에 구조체의 포인터를 사용하려 하지 않는지 어느 정도 검사합니다. 하지만 비GC 구조체를 가리키는 포인터는 공식적으로 약한 참조로 의도된 것이 아닙니다. 가리키는 대상이 해제된 뒤에 포인터를 사용하면 그냥 크래시가 발생합니다.)
malloc() 연산은 새 GC 구조체나 배열을 할당하고, 그것을 가리키는 Ptr을 반환합니다. 참조 카운팅 구현이라면 malloc()은 실제 구조체 앞에 참조 카운터가 들어갈 공간을 충분히 할당하고, 그 값을 1로 초기화할 것입니다. 참고로 테스트용 구현에서는 malloc()이 키워드 인자 immortal=True를 받아 GC 대상이 아닌 구조체나 배열을 할당할 수도 있습니다. 이는 미리 만들어 두는 자료 구조를 선언하고 초기화하기 위한 것이며, 코드 생성기는 이 자료 구조를 GC 대상이 아닌 정적이고 영구적인 데이터로 바꿉니다.
함수 타입¶
선언:
MyFuncType = FuncType([Type1, Type2, ...], ResultType)
주어진 타입의 인자를 받아 주어진 타입의 결과를 반환하는 함수 타입을 선언합니다. 이 타입은 모두 기본 타입이거나 포인터여야 합니다. 함수 타입 자체는 “컨테이너” 타입으로 간주됩니다. 말하자면 함수는 실행 코드를 이루는 바이트를 담고 있습니다. 구조체나 배열과 마찬가지로 함수는 포인터를 통해서만 다룰 수 있습니다.
테스트용 구현에서는 functionptr(TYPE, name, **attrs)를 호출하여 함수를 “생성”할 수 있습니다. 추가 속성은 현재 완전히 정해지지 않은 방식으로 함수를 설명하지만, 다음 속성이 존재할 수도 있습니다:
_callable: a Python callable, typically a function object. graph: the flow graph of the function.
불투명 타입¶
불투명 타입(opaque type)은 백엔드마다 고유한 방식으로 구현된 데이터를 나타냅니다. 이 데이터는 들여다보거나 조작할 수 없습니다.
미리 정의된 불투명 타입 RuntimeTypeInfo가 있습니다. 런타임에 RuntimeTypeInfo 타입의 값은 저수준 타입을 나타냅니다. 실제로는 GcStruct 타입과 GcArray 타입을 나타낼 수 있는 것만으로도 아마 충분할 것입니다. 이는 런타임에 malloc으로 할당된 S 하나만을 가리킬 수도 있고, malloc으로 할당된 더 큰 구조체의 첫 번째 필드인 S를 가리킬 수도 있는 Ptr(S) 타입의 포인터가 있을 때 유용합니다. 포인터가 가리키는 정확한 더 큰 타입에 대한 정보는 Ptr(RuntimeTypeInfo)로 계산하거나 전달할 수 있습니다. Ptr(RuntimeTypeInfo)에 대한 포인터 동등성을 사용하여 런타임에 타입을 확인할 수 있습니다.
현재 일부 백엔드는 메모리 관리를 위해, GcStruct가 첫 번째 필드로 다른 GcStruct를 가지는 경우에 실제로 이러한 정보를 런타임에 사용할 수 있어야 합니다. 참조 카운팅 백엔드는 더 작은 구조체를 가리키는 포인터가 실제로는 더 큰 구조체를 가리키는 때를 알 수 있어야 하며, 그래야 추가 필드도 decref할 수 있습니다. 상황에 따라서는 더 작은 GcStruct의 모든 인스턴스마다 플래그를 저장하지 않고도 이 정보를 재구성할 수 있습니다. 예를 들어 클래스 계층의 인스턴스는 중첩된 GcStruct로 구현할 수 있습니다. 이때 서브클래스의 인스턴스는 인스턴스의 부모 부분을 첫 번째 필드로 포함하는 방식으로 부모 클래스의 인스턴스를 확장합니다. 이 경우 인스턴스의 런타임 클래스를 알아낼 방법(예: vtable 포인터)이 이미 있을 가능성이 높지만, 백엔드는 이를 추측할 수 없습니다. 이것이 RuntimeTypeInfo가 처음 도입된 이유입니다. GcStruct가 생성된 직후에 attachRuntimeTypeInfo() 함수를 호출하여 Ptr(GcStruct) -> Ptr(RuntimeTypeInfo) 시그니처를 가진 저수준 함수를 GcStruct에 연결해야 합니다. 이 함수는 백엔드가 컴파일하며 런타임에 자동으로 호출됩니다. 위의 예에서 이 함수는 vtable 포인터를 따라가서 vtable 자체에서 불투명한 Ptr(RuntimeTypeInfo)를 가져옵니다. (참조 카운팅 GenC 백엔드는 할당 해제 함수에 대한 포인터를 불투명한 RuntimeTypeInfo로 사용합니다.)
RPython 타입 구현¶
앞서 암시했듯이, RPython 타입(예: ‘list’)은 malloc() 및 관련 함수들의 테스트용 구현이 제공하는 저수준 타입만을 조작하는 일종의 “제한된-제한된 Python(restricted-restricted Python)” 형식으로 구현됩니다. 그러면 정말로 C와 똑같아 보이는, 바로 그 (테스트까지 거친!) 초저수준 Python 코드가 흐름 그래프로 변환되어 사용자 프로그램의 나머지 부분과 통합됩니다. 다시 말해, SomeList로 어노테이션된 두 변수 사이의 add 같은 연산을 이 초저수준 리스트 연결(concatenation)을 호출하는 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...)- 연산의 인자인 고수준 변수(Variable)와 상수(Constant)를 읽고, 필요하면 지정된 표현을 갖도록 변환합니다. 연산이 가진 인자 수만큼 표현을 제공해야 합니다. (새로 변환되었을 수도 있는) 변수와 상수의 리스트를 반환합니다.
hop.inputarg(r, arg=i)- inputargs()와 같지만, i번째 인자만 변환하여 반환합니다.
hop.inputconst(lltype, value)- 저수준 타입과 값을 가진 상수(Constant)를 반환합니다.
HighLevelOp 인스턴스 조작(예를 들어 메서드 호출을 변환할 때 암묵적 인자 ‘self’를 삽입하는 데 사용됩니다):
hop.copy()- 아래의 함수들로 조작할 수 있는 새 복사본을 반환합니다.
hop.r_s_popfirstarg()- 고수준 연산의 첫 번째 인자를 제거합니다. 이 메서드는 원본 SpaceOperation을 실제로 변경하지는 않지만, inputargs() 같은 메서드가 제거된 인자를 더 이상 보지 못하도록 ‘hop’을 수정합니다.
hop.v_s_insertfirstarg(v_newfirstarg, s_newfirstarg)- hop 앞에 인자를 하나 삽입합니다. 이 인자는 (hop.genop() 호출에서와 같이) 변수(Variable)와 그에 대응하는 어노테이션으로 지정해야 합니다.
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 브랜치가 생기며, 이 브랜치는 exception_cannot_occur()가 호출된 경우에만 RTyper가 제거할 수 있습니다.)
LLInterpreter¶
LLInterpreter는 흐름 그래프(flow graph)를 해석할 수 있는 간단한 코드입니다. 이는 테스트 목적으로 매우 유용하며, 특히 RPython 타이퍼 작업을 하는 경우에 그렇습니다. 이를 위한 가장 유용한 인터페이스는 rpython/rtyper/test/test_llinterp.py 파일에 있는 interpret 함수입니다. 이 함수는 함수 하나와, 그 함수를 호출할 때 사용할 인자들의 리스트를 인자로 받습니다. 그런 다음 흐름 그래프를 생성하고, 전달한 인자들의 타입에 따라 그래프에 어노테이션을 붙인 뒤, 그 결과에 대해 LLInterpreter를 실행합니다. 예제:
def test_invert():
def f(x):
return ~x
res = interpret(f, [3])
assert res == ~3
또한 py.test.raises와 매우 비슷하게 동작하는 interpret_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])