CPython

CPython/컴파일러 (2) '거의 같음' 연산자 구현하기(2)

25G 2023. 1. 8. 13:15

컴파일러 C API

AST 모듈 컴파일의 진입점인 compiler_mod()는 모듈타입에 따라 다른컴파일러함수를 사용한다. mod가 Module일 경우 모듈은 컴파일러 유닛으로 컴파일되어 c_stack 프로퍼티에 저장된다. 이후 컴파일러 유닛 스택에서 PyCodeObject를 생성하기 위해 assemble()을 실행한다.

compile.c파일

compiler_body()는 모듈의 각 문을 순회하며 방문한다.

AST노드 타입을 확인하는 asdl_seq_GET() 호출을 통해 이 문의 타입이 결정된다. VISIT 매크로는 각 문 타입에 해당하는 compile.c의 함수를 호출한다.

모든 문을  포괄하는 stmt타입의 경우 컴파일러는 compiler_bisit_stmt()를 호출해 Python/Python.asdl에 정의된 하위 문 타입들로 전환한다.

 

명령

컴파일러는 일련의 명령을 담고 있는 블록을 컴파일러 상태로 내보낸다. 명령 구조체는 명령 코드와 인자, 문장이 위치한 줄 번호를 포함한다. 점프 명령일 경우 점프할 블록에 대한 포인터도 포함한다.

 

명령타입

다음 명령 타입 instr의 필드 들이다.

 

  • i_jabs : 절대 주소 점프 명령 플래그
  • i_jrel: 상대 주소 점프 명령 플래그
  • i_linen : 이 명령이 생성된 줄 번호
  • i_opcode : 이 명령의 명령 코드 번호
  • i_oparg: 명령 코드 인자
  • i_target: i_jrel이 참일 경우 basicblock을 가리키는 포인터

 

점프 명령

점프 명려은 한 명령에서 다른 명령으로의 점플를 실행한다. 절대 점프 방식과 상대 점프 방식의 점프 명령을 사용할 수 있다.

절대 점프 명령은 컴파일된 코드 객체상에서 정확한 명령의 위치를 점프대상으로 사용하고, 상대 점프 명령은 다른 명령을 기준으로 점프대상을 지정한다. 각 명령어는 각기 다른 인자를 요구한다.

 

어셈블리

컴파일 단계가 끝나면 프레임 블록 리스트가 완성된다. 각 프레임 블록은 일련의 명령리스트와 다음 블록을 가리키는 포인터를 가진다. 어셈블러는 기본 프레임 블록들에 대해 깊이 우선탐색을 실행하고 명령들을 단일한 바이트 코드 시퀀스로 병합한다.

 

어셈블러의 깊이 우선 탐색 알고리즘

어셈블러는 기본 프레임 블록 그래프를 DFS로 탐색한다. DFS알고리즘은 그래프 탐색에 흔히 사용되는 알고리즘 이다.

트리 구조인 CST와 AST와는 달리 컴파일러 상태는 노드가 명령을 담는 기본 프레임 블록인 그래프다.

기본 프레임 블록은 두 그래프에 연결된다. 첫번째 그래프는 각 블록의 b_list 프로퍼티를 기준으로 생성되며, 블록이 생성된 순서와 반대로 정렬된다. b_list에서 생성된 그래프를 통해 컴파일러 유닛의 모든 블록을 순차적으로 방문할 수 있다.

두번째 그래프는 각 블록의 b_next 프로퍼티를 기준으로 생성된다. 이그래프는 제어 흐름을 나타낸다. 그래프의 정점들은 compiler_use_next_block(c,next) 호출로 생성된다. next는 현재 블록과 이어질 정점이 될 다음 블록이다.

 

어셈블러 C API

compile.c 파일참조

 

어셈블러 api의 진입점 assemble()은 다음 역할을 수행한다. 

  • 메모리 할당을 위해 블록 개수를 계산한다.
  • 마지막 블록이 None을 반환하는지 확인한다.
  • 모든 상대 주소 점프 명령의 오프셋을 계산한다.
  • dfs() 호출로 블록에 대한 DFS를 실행한다.
  • 컴파일러로 모든 명령을 전달한다.
  • 컴파일러 상태를 인자로 해서 makecode()를 호출해 PyCodeObject를 생성한다.

코드 객체 생성

makecode()는 컴파일러 상태와 어셈블러의 프로퍼티들을 확인하고 PyCode_New()를 호출해 PyCodeObject에 컴파일러 상태와 어셈블러 프로퍼티를 추가한다. 

PyCode_NewWithPosOnlyArgs()로 전송되기 전에 PyCode_Optimize()로 전송되는 바이트 코드를 확인할 수 있다. 이 함수는 Python/peephole.c가 제공하는 바이트 코드 최적화 과정의 일부다. 해당 코드는 바이트 코드 명령을 확인하고 특정 시나리오에 해당될 경우 해당 명령을 다른 명령으로 교체한다. 예를 들어 return 문 뒤의 도달할 수 없는 명령을 제거한다.

 

'거의 같음' 연산자 구현하기

저번에 문법에만 추가해봤었던 '거의같음' 연산자를 지원하도록 CPython을 수정해 봅시다.

먼저 PyObject의 비교 함수에서 참조할 수 있도록 Py_AlE연산자에 대한 #define정의를 추가해야합니다.

1. Include/object.h 값이 6인 PyAlE 정의 추가

2. 바로 아래에 있는 Py_RETURN_RICHCOMPARE 메크로에 Py_AlE에 대한 case문을 추가하자

3. Objects/object.c 에는 연산자가 0에서 5사이의 값인지 확인하는 부분이 있다. 다음 assert가 Py_AlE의 값 6도 허용하도록 수정해야한다. (Py_GE -> Py_AlE로 수정)

다음으로 Py_AlE를 연산자 타입의 값으로 사용할 수 있도록 COMPARE_OP명령 코드를 수정한다.

4. Objects/object.c의 _Py_SwappedOp리스트에 Py_AlE를 추가하자. 예를 들어 어떠한 클래스가 매직 메서드로 == 비교 연산자를 재정의 했지만 그 반대인 != 연산자를 위한 매직 메서드를 정의핮 ㅣ않았을때 이 리스트를 사용해 역을 구한다

예를 들어 Coordinate 클래스를 정의하고 __eq__매직 메서드를 구현하면 동등 연산자를 직접 정의할 수 있다.

위와같이 ~= 와 Py_AlE를 추가해 주자.

5. Lib/opcode.py 의 비교 연산자 리스트도 수정한다.

opstrings 리스트는 비교 연산자가 ㅋㄹ래스에 구현되지 않았을때 에러 메시지를 표시하는데 사용된다.

6. 이제 연산자가 PyCmp_AlE인 BinOp노드를 처리할 수 있도록 컴파일러를 수정하자.

Python/compile.c 에서 compiler_addcompare()를 찾는다

위 사진과 같이 comp_op AST 열겨형의 AlE와 PyCmp_AlE 명령 코드 비교 열거형을 짝짓는 case 를 추가한다.

아재 거의 같음 연산자의 동작이 다음 시나리오와 일치하도록 프로그래밍한다.

  • 1 ~= 2 는 false
  • 부동 소수점 반올림을 통해 1 ~= 1.01은 ture다

7. Objects/floatobject.c 에서 float_richcompare() 를 찾아서 goto용 레이블 정의 Compare: 에 다음 case를 추가한다.

8. Python/ceval.c에 있는 평가 루프의 보호 장치도 수정해야한다. 

 

이제 CPython을 다시 컴파일하고 REPL을 열어 구현한 연산자 테스트를 해보자.

다음명령어로 다시 컴파일을 한다.

make -j2 -s

 

그리고 REPL을 열어 테스트를 해본다

 

'CPython' 카테고리의 다른 글

CPython 메모리 관리  (2) 2023.01.25
CPython/ 평가루프  (0) 2023.01.24
CPython/컴파일러 (1)  (0) 2023.01.08
CPython/렉싱과 파싱/ '거의 같음'연산자 구현(1)  (2) 2022.12.10
CPython 구성과 입력  (2) 2022.12.10