프로그래밍 언어 (컴파일러/인터프리터 언어)

출처: 컴퓨터 사이언스 부트캠프 with 파이썬 (양태환, 길벗)

컴파일러 언어와 인터프리터 언어는 컴파일 타임이 있느냐 없느냐 즉, 소스 코드를 분석하는 시점과 입력 데이터를 받는 시점이 언제인지에 따라 나뉜다.

1. C: 컴파일러 언어 분석

  • 소스 코드를 컴파일
  • 목적 코드(object code)인 기계어로 된 인스트럭션 생성
  • 링커(linker)는 필요한 라이브러리를 가져오고 여러 개의 목적 파일을 함께 묶어 실행 파일(executable file)을 생성
  • 소스 코드를 분석하는 컴파일 타임(compile time)과 실제 데이터를 받아 출력하는 런타임(run time)일 분리되어 있음

2. Python: 인터프리터 언어 분석

  • 소스 코드를 컴파일해 바이트 코드(byte code)를 생성
  • 바이트 코드가 생성된 후에는 PVM(Python Virtual Machine)에서 바이트 코드를 해석하여 프로그램을 실행
  • 소스 코드를 분석하는 컴파일 타임이 따로 없고 실행과 동시에 분석을 시작 (소스 코드와 입력 데이터가 같은 시점에 삽입됨)

3. Python: 소스 코드부터 실행까지

3-1. 컴파일러

  • 일반적인 컴파일러는 렉서(lexer)와 파서(parser)로 구성
  • 소스 코드는 렉서를 거치며 여러 개의 토큰으로 변경됨
  • 파서는 토큰을 분석해 분석 트리(parse tree)를 구성
  • 코드 생성(code generation): 분석 트리가 만들어지만 이를 이용해 목적 코드가 생성

3.2. Python의 바이트 코드 생성 과정

  • 소스 코드 $\rightarrow$ 분석 트리
  • 분석 트리 $\rightarrow$ 추상 구문 트리
  • 심벌 테이블 생성
  • 추상 구문 트리 $\rightarrow$ 바이트 코드
1
2
3
4
5
6
7
8
9
10
11
12
# 예제 코드 test.py
# 두 인자를 더해 리턴하는 함수 func()와 두 전역변수 a, b
# a와 b를 받아 func()를 호출하고, 그 값을 c에 저장한 뒤 print() 함수로 출력

def func(a, b):
    return a + b

a = 10
b = 20

c = func(a, b)
print(c)
1
2
3
4
5
6
7
from tokenize import tokenize
from io import BytesIO

s = open('test.py').read()
g = tokenize(BytesIO(s.encode("utf-8")).readline)
for token in g:
    print(token)
TokenInfo(type=59 (BACKQUOTE), string='utf-8', start=(0, 0), end=(0, 0), line='')
TokenInfo(type=1 (NAME), string='def', start=(1, 0), end=(1, 3), line='def func(a, b):\n')
TokenInfo(type=1 (NAME), string='func', start=(1, 4), end=(1, 8), line='def func(a, b):\n')
----------- 중략 -----------
TokenInfo(type=53 (OP), string=')', start=(8, 7), end=(8, 8), line='print(c)\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(8, 8), end=(8, 9), line='print(c)\n')
TokenInfo(type=0 (ENDMARKER), string='', start=(9, 0), end=(9, 0), line='')

이렇게 얻어진 토큰으로 분석 트리를 만든 다음, 추상 구문 트리로 변형

1
2
3
4
# 추가: 아래 명령을 사용해서 Python에서 사용하는 토큰 종류를 확인할 수 있다.

import token
token.tok_name

3.2.1. 추상 구문 트리

  • 추상 구문 트리(Abstract Syntax Tree, AST): 소스 코드의 구조를 나타내는 자료 구조
  • 추상 구문 트리를 바탕으로 심벌 테이블, 바이트 코드를 생성할 수 있음
1
2
3
4
5
6
import ast
node = ast.parse(s, "test.py", "exec") # 노드를 생성하고
g = ast.walk(node) # walk 함수를 이용하면 트리의 모든 노드를 순회할 수 있는 제너레이터를 얻을 수 있음
print(next(g)) # 제너레이터를 만든 다음 next를 통해 노드를 하나씩 획득
print(next(g))
print(next(g))
<_ast.Module object at 0x1a0fede828>
<_ast.FunctionDef object at 0x1a0fede898>
<_ast.Assign object at 0x1a0fedea58>

3.2.2 심벌 테이블

  • 심벌 테이블(symbol table): 변수나 함수의 이름과 그 속성에 대해 기술해 놓은 테이블
1
2
3
4
import symtable
sym = symtable.symtable(s, "test.py", "exec") # symtable로 테이블을 받아오고
print(sym.get_name()) # 이름을 확인 ('top': 이 테이블이 글로벌 테이블이라는 의미)
print(sym.get_symbols()) # 현재 영역에 있는 심벌을 확인
1
2
top
[<symbol 'func'>, <symbol 'a'>, <symbol 'b'>, <symbol 'c'>, <symbol 'print'>]
1
2
3
4
5
# 함수 func의 심벌테이블
print(sym.get_children()) # 글로벌 심벌 테이블 내에 다른 심벌 테이블이 있는지 확인
func_sym = sym.get_children()[0] # 테이블을 받아옴
print(func_sym.get_name()) # 심벌테이블의 이름은 func
print(func_sym.get_symbols()) # 심벌을 얻어오면 인자 a와 b를 볼 수 있음
1
2
3
[<Function SymbolTable for func in test.py>]
func
[<symbol 'a'>, <symbol 'b'>]

3.2.3. 바이트 코드와 PVM

1
2
3
4
5
6
7
# 바이트 코드를 생성

import dis
g = dis.get_instructions(s) # 바이트 코드를 제공하는 제너레이터 생성
for inst in g:
    print(inst.opname.ljust(20), end= " ") # 바이트 코드 이름
    print(inst.argval) # 인자 값 출력, 바이트 코드 인스트럭션 확인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LOAD_CONST           <code object func at 0x1a0fb57030, file "<disassembly>", line 1>
LOAD_CONST           func
MAKE_FUNCTION        0
STORE_NAME           func
LOAD_CONST           10
STORE_NAME           a
LOAD_CONST           20
STORE_NAME           b
LOAD_NAME            func
LOAD_NAME            a
LOAD_NAME            b
CALL_FUNCTION        2
STORE_NAME           c
LOAD_NAME            print
LOAD_NAME            c
CALL_FUNCTION        1
POP_TOP              None
LOAD_CONST           None
RETURN_VALUE         None

추가: PVM - CPython 소스 코드 중 ceval.c에 있는 PVM 일부 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{

--------------- 중략 ---------------

main_loop:
    for (;;) { /* 무한 루프 (PVM)
        assert(stack_pointer >= f->f_valuestack); /* else underflow */
        assert(STACK_LEVEL() <= co->co_stacksize);  /* else overflow */
        assert(!PyErr_Occurred());

--------------- 중략 ---------------

switch (opcode) { /* 실제 바이트 코드를 분석해서 실행

        /* BEWARE!
           It is essential that any operation that fails must goto error
           and that all operation that succeed call [FAST_]DISPATCH() ! */

        TARGET(NOP)
            FAST_DISPATCH();

        TARGET(LOAD_FAST) {
            PyObject *value = GETLOCAL(oparg);
            if (value == NULL) {
                format_exc_check_arg(PyExc_UnboundLocalError,
                                     UNBOUNDLOCAL_ERROR_MSG,
                                     PyTuple_GetItem(co->co_varnames, oparg));
                goto error;
            }
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }

        PREDICTED(LOAD_CONST);
        TARGET(LOAD_CONST) {
            PyObject *value = GETITEM(consts, oparg);
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }

        PREDICTED(STORE_FAST);
        TARGET(STORE_FAST) {
            PyObject *value = POP();
            SETLOCAL(oparg, value);
            FAST_DISPATCH();
        }
        
--------------- 후략 ---------------