CPython

CPython/컴파일러 (1)

25G 2023. 1. 8. 12:10

저번 글에서 택스트 형태의 소스 코드를 컴파일 가능한 논리 구조인

리더 (텍스트)->랙서 > CST-> 파서 > AST-> 컴파일러에서 랙서와 파서 부분을 정리 했고 이번엔 컴파일러에 대해서 정리를 하려합니다.

 

컴파일 작업은 두 부분으로 구성된다.

1. 컴파일러 AST를 순회하며 논리적 실행순서를 나타내는 제어 흐름 그래프를 생성한다.

2.어셈블러: CFG의 노드들을 실행 가능한 명령을 순차적으로 나열한 바이트 코드 형태로 변환한다.

 

 컴파일 과정 

파서 > AST -> 컴파일러 > CFG -> 어셈블러 >  바이트코드 -> 실행

 

AST 모듈을 코드 객체로 컴파일하는 과정

PyAST_CompileObject()

compile.c 경로에 정의돼 있다.

  • 컴파일러 상태는 심벌 테이블을 담는 컨테이너 타입이다.
  • 심벌 테이블은 변수 이름을 포함하고 추가로 하위 심벌 테이블을 포함할 수 도 있다.
  • 컴파일러 타입에는 컴파일러 유닛도 포함된다.
  • 각 컴파일러 유닛은 이름,변수 이름, 상수, 셀 변수들을 포함한다.
  • 컴파일러 유닛은 기본 프레임 블록을 포함한다.
  • 기폰 프레임 블록은 바이트코드 명령을 포함한다.

위 함수에서 컴파일러 상태 구조체인 compiler 타입은 컴파일러 플래그나 스택, PyArena등 컴파일러를 위한 다양한 프로퍼티를 포함한다.

컴파일러 상태 필드들

  • c_arena : 메모리할당 아레나에 대한 포인터
  • c_const_cache : names 튜플을 포함한 모든 상수를 담는 파이선 dict
  • c_do_noet_emit_bytecode : 바이트 코드 컴파일 비활성화 플래그
  • c_filename: 컴파일에 필요한 파일명
  • c_flags: 상속받은 컴파일러 플래그들
  • c_future: 대화형 모드를 위한 모듈의 __future__플래그에 대한 포인터
  • c_interactive: 대화형 모드 활성화 플래그
  • c_nestlevler : 현재 깊이 레벨
  • c__optimize: 최적화 레벨
  • c_st: 컴파일러의심벌 테이블
  • c_stack: compiler_unit에 대한 포인터를 담는 파이썬 리스트
  • u: 현재 블록의 컴파일러 상태

PyAST_CompileObject()가 다음과 같이 컴파일러 상태를 초기화한다.

  • 모듈에 문서화 문자열__doc__이 포함되어 있지 않다면 빈 문서화 문자열을 생성한다. __annotations__프로퍼티에 대해서도 같은 작업을 수행한다.
  • 스텍트레이스 및 예외 처리에 필요한 파일 이름을 컴파일러 상태에 설정한다.
  • 인터프리터가 사용한 메모리 할당 아레나를 컴파일러의 메모리 할당 아레나로 설정한다. 
  • 코드 컴파일 전에 퓨처 플래그들을 설정한다.

 

퓨처플래그와 컴파일러 플래그

퓨처 플래그와 컴파일러 플래그는 컴파일러 기능을 설정한다.

  1. 환경 변수와 명령줄 플래그를 담는 구성 상태
  2. 모듈 소스 코드의__future__문

퓨처플래그

퓨처 플래그는 특정 모듈에서 문법이나 언어 기능을 활성화 하기 위해 사용된다. annotations 퓨처 플래그로 타입힌트의 평가를 지연시킬수 있다.

from __future__ import annotations

이 퓨처 문 아래 부터 결정되지 않은 타입힌트를 사용할 수 있다. 만약 퓨처문 없이 결정되지 않은 타입힌트를 사용하면 이 모듈을 임포트 할 수 없다.

 

대부분의 __future__플래그는 파이선 2와 3간 이식 지원을 위해 사용되었다고 한다. 파이썬 4.0이 나온다면 더많은퓨처 플래그가 추가 될 수 있다.

 

컴파일러 플래그

컴파일러 플래그는 실행 환경에 의존적이기 때문에 코드나컴파일러의 실행방식을 변경할 수 있다. __future__문과 달리 코드로 활성화 되지는 않는다.

컴팡릴러 플래그중 -0플래그는 디버그 용도로 추가된 모든 assert 문을 비활성화 하는 최적화를 실행한다. 이 플래그는 PYTHONPTIMIZE=1 환경변수로도 비활성화 할 수 있다.

 

심벌 테이블

코드를 컴파일하기 전에 PyAST_CompileObject() API 는 심벌 테이블을 생성한다.

심벌 테이블은 전역과 지역 등 이름 공간의 목록들을 컴파일러에 제공한다. 컴파일러는 심벌 테이블에서 얻은 이름 공간에서 스코프를 결정하고 참조를 실행한다.

 

심벌 테이블과 관련된 소스파일들

  • symtable.c : 심펄테이블 구현
  • symtable.h : 심펄테이블 api와 타입 정의
  • symtable.py: 표준 라이브러리 symtable 모듈

 

컴파일러는 컴파일러당 하나의 symtable 인스턴스만 사용하기 때문에 이름 공간 관리가 중요하다. 예를 들어 두 클래스가 동일한 이름의 메서드를 가지고 있을 경우 모듈에서 어떤 메서드를 호출할지 정해주는 것이 symtable의 역할이다.

하위 스코프의 변수를 상위 스코프에서 사용하지 못하게 하는것도 symtable의 역할이다.

 

symtable 모듈

PyPI에서 받을 수 있는 tabulate 모듈을 사용해 심벌 테이블을 출력해 볼 수 도 있다.

위 스크립트를 실행해 보면 다음과 같은 심벌테이블이 출력된다.

 

심벌테이블 구현

심벌테이블 구현은 symtable.c 에서 찾을 수 있고 주 인터페이스는 PySymtable_buildObject() 다.

PySymtable_BuildObject()는  PyAST_FromNodeObject() 와 비슷하게 mod_ty타입에 따라 모듈 내의 문장들을순회한다.

심벌테이블은 mod_ty 타입인 AST의 노드와 분기를 재귀적으로 탐색하며 symtable의 엔트리로 추가한다.

PySymtable_BuildObject()는 모듈의 각 문을 순회하며 symtable_visit_stmt()를 호출한다. symtable_visit_stmt()는 Python/Python.asdl에서 정의한 모든문 타입에 대한 case를 가지고 있는 거대한 switch 문이다.

각 문 타입 마다 심벌을 처리하는 함수가 존재한다. 함수 정의문 타입을 처리하는 함수에는 다음을 처리하기 위한 로직들이 들어있다.

 

  • 현재 재귀 깊이가 재귀 제한을 넘지 않았는지 검사한다.
  • 함수가 함수 객체로 넘겨지거나 호출될 수 있도록 함수 이름을 심벌 테이블에 추가한다.
  • 기본 인자 중 리터럴이 아닌 인자를 심벌테이블에서 찾는다.
  • 타입 힌트를  처리한다.
  • 함수 데코레이터를 처리한다.

마지막으로 symtable_enter_block()이 함수 블록을 방문해 인자와 함수 본문을 차례대로 처리한다.

다음은 symtable_visit_stmt() 중 함수에 대한 symtable을 구축하는 C코드의 일부입니다.

이렇게 생성된 심벌테이블은 컴파일러로 넘겨진다.

 

핵심 컴파일 과정

PyAST_CompileObject() 에 검파일러 상태와 symtable,AST로 파싱된 모듈이 준비되면 컴파일이 시작된다.

코어 컴파일러는 다음과 같은 두가지 작업을 수행한다.

  1. 컴파일러 상태와 심벌 테이블, AST를 제어흐름 그래프로 변환한다.
  2. 논리 오류나 코드 오류를 탐지해 실행 단계를 런터임 예외로 부터 보호한다.

파이썬에서 컴파일러 사용하기

내장 함수인 compile()로 컴파일러를 직접 호출 할 수 있다. compile()은 code object를 반환한다.

 



symtable() API 처럼 단순 표현식의 경우에는 eval 모드를, 모듈이나 함수, 클래스일 경우에는 exec 모드를 사용하면 된다.

컴파일된 코드는 코드 객체의 co_code 프로퍼티에 담긴다.

표준 라이브러리 바이트코드 역어셈블러 모듈 dis로 화면에 바이트코드를 출력하거나 Instruction 인스턴스 리스트를 얻을 수 있다.

dis를 임포트하고 코드 객체의 co_code 프로퍼티에 대해 dis()를 실행하면 역 어셈블러가 컴파일된 코드를 역어셈블한 후 REPL에 출력한다.

위에 명시된 LOAD_NAME, LOAD_CONST, BINARY_ADD, RETURN_VALUE는 모두 바이트 코드 명령이다. 바이너리 형태에서 명령을 1바이트로 표현한다는 뜻으로 바이트 코드란 이름이 붙었지만 사실 파이썬3.6부터 명령은 2바이트, 즉 워드로 표현되므로 지금은 바이트코드보다는 워드코드가 올바른 표현이라고 합니다.