The Abstract Syntax Tree — AST 설계와 구축
Douglas Thain, Introduction to Compilers and Language Design (2nd ed., 2023) · pp.85–98
📌단원 개요
Chapter 5의 단순한 expression AST를 넘어, 실제 언어(B-Minor) 전체를 표현하는 abstract syntax tree(AST)를 5개의 C structure로 정의한다. 이 AST는 semantic analysis의 출발점이다.
- AST는 프로그램의 primary structure를 담는 핵심 internal data structure다.
- "abstract"란 parsing의 세부 사항을 생략한다는 뜻 → prefix / postfix / infix 같은 표기 차이를 신경 쓰지 않는다.
- 여기서 정의하는 AST는 대부분의 procedural language를 표현할 수 있다.
- 마지막에는 Bison parser generator로 이 전체 구조를 자동 생성하는 방법을 본다.
AST를 구성하는 5개의 C structure
| structure | 표현 대상 | 한 줄 요약 |
|---|---|---|
struct decl | declaration | symbol의 name, type, value를 선언 |
struct stmt | statement | program의 state를 바꾸는 action |
struct expr | expression | value와 operation의 조합 → value 산출 |
struct type | type | 변수·함수의 type 인코딩 |
struct param_list | parameter | function parameter의 linked list |
6.1Overview
AST는 프로그램의 구조를 표현하는 internal data structure이며, parsing의 구체적 형식은 버린다.
- declaration: symbol(constant, variable, function 등)의 name, type, value를 명시해 program에서 사용 가능하게 함.
- statement: program의 state를 바꾸는 action을 지시. 예: loop, conditional, function return.
- expression: value와 operation의 조합으로 규칙에 따라 evaluate되어 integer / floating point / string 같은 value를 산출. 일부 언어에서는 side effect로 state를 바꾸기도 함.
AST가 abstract한 이유는 parsing의 particular detail을 생략하기 때문이다. 언어가 prefix / postfix / infix expression 중 무엇을 쓰든 동일한 AST로 표현된다.
각 structure는 다른 structure를 가리키는 pointer를 갖는다(서로 얽혀 있음). 그래서 어떻게 함께 동작하는지 보기 전에 5개를 먼저 전부 훑어봐야 한다.
6.2Declarations
완전한 B-Minor program은 declaration의 sequence다. 각 declaration은 variable 또는 function의 존재를 명시한다.
- variable declaration은 선택적으로 initializing value를 줄 수 있다. 없으면 default value zero가 부여된다.
- function declaration은 선택적으로 body를 줄 수 있다. body가 없으면 그 declaration은 다른 곳에 선언된 함수의 prototype 역할을 한다.
유효한 declaration 예시
b: boolean;
s: string = "hello";
f: function integer ( x: integer ) = { return x*x; }
struct decl
struct decl {
char *name;
struct type *type;
struct expr *value; // expression일 때
struct stmt *code; // function일 때
struct decl *next;
};
factory function
struct decl * decl_create(
char *name,
struct type *type,
struct expr *value,
struct stmt *code,
struct decl *next )
{
struct decl *d = malloc(sizeof(*d));
d->name = name;
d->type = type;
d->value = value;
d->code = code;
d->next = next;
return d;
}
- 이런 structure를 매우 많이 만들기 때문에, 할당과 필드 초기화를 해주는 factory function이 필요하다. (statement, expression 등도 같은 패턴)
nextfield로 declaration들을 linked list로 연결한다. → program은 decl의 sequence.- 가리킬 대상이 없는 field는 null pointer로 둔다(그림에서는 명확성을 위해 생략).
6.3Statements
function의 body는 statement의 sequence다. statement는 지정된 순서대로 특정 action(값 계산, loop, 분기 선택 등)을 수행하라는 지시이며, local variable의 declaration일 수도 있다.
struct stmt 와 stmt_t
struct stmt {
stmt_t kind;
struct decl *decl;
struct expr *init_expr;
struct expr *expr;
struct expr *next_expr;
struct stmt *body;
struct stmt *else_body;
struct stmt *next;
};
typedef enum {
STMT_DECL,
STMT_EXPR,
STMT_IF_ELSE,
STMT_FOR,
STMT_PRINT,
STMT_RETURN,
STMT_BLOCK
} stmt_t;
kind 별로 사용하는 field
| kind | 사용하는 field |
|---|---|
STMT_DECL | decl → (local) declaration을 가리킴 |
STMT_EXPR | expr → expression statement |
STMT_IF_ELSE | expr=control expression, body=true일 때, else_body=false일 때 |
STMT_FOR | init_expr, expr, next_expr=loop header의 세 expression, body=loop 본문 |
STMT_PRINT | expr → 출력할 expression들 |
STMT_RETURN | expr → return할 expression |
STMT_BLOCK | body → 중괄호 안의 statement들 |
factory function
struct stmt * stmt_create( stmt_t kind,
struct decl *decl, struct expr *init_expr,
struct expr *expr, struct expr *next_expr,
struct stmt *body, struct stmt *else_body,
struct stmt *next );
struct stmt는 field가 많지만, 각 kind는 필요한 field만 쓰고 나머지는 null로 둔다.- 예:
if( x<y ) print x; else print y;는STMT_IF_ELSE로, expr, body, else_body만 사용. - 예:
for(i=0;i<10;i++) print i;는STMT_FOR로, 세 expr field(init_expr, expr, next_expr)와 body만 사용.
6.4Expressions
Chapter 5의 단순 expression AST와 거의 같지만, 언어의 모든 operator(arithmetic, logical, comparison, assignment 등)에 대한 binary type과, 모든 leaf value(variable name, constant value 등)에 대한 type이 추가로 필요하다.
struct expr 와 expr_t
struct expr {
expr_t kind;
struct expr *left;
struct expr *right;
const char *name;
int integer_value;
const char *string_literal;
};
typedef enum {
EXPR_ADD,
EXPR_SUB,
EXPR_MUL,
EXPR_DIV,
...
EXPR_NAME,
EXPR_INTEGER_LITERAL,
EXPR_STRING_LITERAL
} expr_t;
namefield는EXPR_NAME에,integer_valuefield는EXPR_INTEGER_LITERAL에 설정된다.- integer, boolean, character literal 값은 모두
integer_valuefield에 저장할 수 있다.
factory functions
// binary operator용
struct expr * expr_create( expr_t kind,
struct expr *L,
struct expr *R );
// 각 leaf type용
struct expr * expr_create_name( const char *name );
struct expr * expr_create_integer_literal( int i );
struct expr * expr_create_boolean_literal( int b );
struct expr * expr_create_char_literal( char c );
struct expr * expr_create_string_literal( const char *str );
특별히 주의할 4가지 case
- Unary operator (logical-not 등): 유일한 argument를
leftpointer에 둔다. 예:!x→EXPR_NOT의 left에EXPR_NAME(x). - Function call:
EXPR_CALLnode. left=function name, right=EXPR_ARGnode들의 unbalanced tree. → tree로 linked list를 표현하며, code generation 때 stack 위 인자 처리를 단순화한다. - Array subscript: binary operator처럼 취급.
EXPR_SUBSCRIPT의 left=array name, right=integer expression. 예:a[b].
!x 의 AST
a[b] 의 AST
f(a,b,c) 의 AST — EXPR_CALL + EXPR_ARG의 unbalanced tree
6.5Types
struct type은 declaration에 등장하는 모든 variable과 function의 type을 인코딩한다. primitive type은 kind만 설정하면 되고, compound type은 여러 type structure를 연결해 만든다.
struct type / param_list / type_t
typedef enum {
TYPE_VOID,
TYPE_BOOLEAN,
TYPE_CHARACTER,
TYPE_INTEGER,
TYPE_STRING,
TYPE_ARRAY,
TYPE_FUNCTION
} type_t;
struct type {
type_t kind;
struct type *subtype;
struct param_list *params;
};
struct param_list {
char *name;
struct type *type;
struct param_list *next;
};
- primitive type(integer, boolean): standalone type structure,
kind만 설정, 나머지 null. - compound type(array, function): 여러 type structure를
subtype으로 연결.
compound type 구성
array [] integer
array [] array [] integer
- function type은
subtype으로 return type을 표현하고,param_listnode의 linked list로 각 parameter의 name과 type을 표현한다. - 예:
function integer (s:string, c:char)→TYPE_FUNCTION(subtype=TYPE_INTEGER) + param_list[ "s":TYPE_STRING → "c":TYPE_CHAR ]. TYPE_ARRAY는subtype을 임의 깊이로 연결 가능.
type structure는 강력한 higher-order 개념도 표현할 수 있다:
a: array [10] function integer ( x: integer ); // function 10개의 array
f: function function integer (x:integer) (y:integer); // function을 반환하는 function
g: function array [10]
function integer (x:integer) (y:integer); // function array를 반환하는 function
B-Minor type system은 이를 표현할 수 있지만, 더 dynamic한 구현이 필요하므로 나중에 typechecking에서 reject된다. (관심 있으면 Scheme, Haskell 같은 functional language 참고)
6.6Putting it All Together
개별 구성요소를 모두 봤으니, 완전한 B-Minor function 하나가 AST로 어떻게 표현되는지 본다.
예제 function
compute: function integer ( x:integer ) = {
i: integer;
total: integer = 0;
for(i=0;i<10;i++) {
total = total + i;
}
return total;
}
- 최상위는
DECL "compute"→type은TYPE_FUNCTION(subtype=TYPE_INTEGER, param="x":TYPE_INTEGER),code는 statement chain. - body는
STMT_DECL("i")→STMT_DECL("total"=0)→STMT_FOR→STMT_RETURN으로nextlinked list. STMT_FOR:init_expr=EXPR_ASSIGN(i,0),expr=EXPR_LT(i,10),next_expr=EXPR_INC(i),body=STMT_BLOCK.STMT_BLOCK안의STMT_EXPR은EXPR_ASSIGN(total, EXPR_ADD(total,i)).
개략 구조 (statement chain)
6.7Building the AST
원칙적으로 nested function call로 AST를 손으로 만들 수도 있지만 현실적이지 않다. 대신 Bison(LR parser generator)이 rule reduce 시마다 creation function을 호출해 tree를 자동으로 엮게 한다.
손으로 만들기 (비현실적인 예) — square(x)=x*x
d = decl_create(
"square",
type_create(TYPE_FUNCTION,
type_create(TYPE_INTEGER,0,0),
param_list_create(
"x",
type_create(TYPE_INTEGER,0,0),
0)),
0,
stmt_create(STMT_RETURN,0,0,
expr_create(EXPR_MUL,
expr_create_name("x"),
expr_create_name("x")),
0,0,0,0),
0);
분명히 이렇게 코드를 쓸 수는 없다 → parser가 reduce될 때마다 creation function을 호출하게 한다.
Bison grammar에 action 심기
최상위: program은 declaration의 sequence
program : decl_list
{ parser_result = $1; }
;
declaration rule
decl : name TOKEN_COLON type TOKEN_SEMI
{ $$ = decl_create($1,$3,0,0,0); }
| name TOKEN_COLON type TOKEN_ASSIGN expr TOKEN_SEMI
{ $$ = decl_create($1,$3,$5,0,0); }
| /* and more cases here */
. . .
;
decl_list: right-recursive로 linked list 구성
decl_list : decl decl_list
{ $$ = $1; $1->next = $2; }
| /* epsilon */
{ $$ = 0; }
;
statement rule
stmt : TOKEN_IF TOKEN_LPAREN expr TOKEN_RPAREN stmt
{ $$ = stmt_create(STMT_IF_ELSE,0,0,$3,0,$5,0,0); }
| TOKEN_LBRACE stmt_list TOKEN_RBRACE
{ $$ = stmt_create(STMT_BLOCK,0,0,0,0,$2,0,0); }
| /* and more cases here */
. . .
;
- 각
declstructure는 따로 생성되므로decl_list로 linked list로 연결해야 한다. - 이를 위해 rule을 right-recursive로 만든다: 왼쪽
decl=하나의 declaration, 오른쪽decl_list=나머지 list. - list의 끝은
decl_list가 ǫ(epsilon)을 생성할 때 null value.
각 rule이 reduce될 때 반환하는 값의 semantic type이 단일 type이 아니다. declaration rule은 struct decl *를, identifier rule은 char *를 반환한다. 그래서 Bison에게 semantic value가 AST 모든 type의 union임을 알려준다.
%union {
struct decl *decl;
struct stmt *stmt;
. . .
char *name;
};
그런 다음 각 rule이 union의 어느 subfield를 쓰는지 %type으로 지정한다:
%type <decl> program decl_list decl . . .
%type <stmt> stmt_list stmt . . .
. . .
%type <name> name
6.8Exercises (연습문제)
- B-Minor의 완전한 LR grammar를 작성하고 Bison으로 test하라. 첫 시도는 shift-reduce, reduce-reduce conflict가 많을 것이므로 Chapter 4의 grammar 지식으로 rewrite해 conflict를 제거하라.
- 이 장에서 설명한 AST structure와 generating function을 작성하고, nested function call로 간단한 AST를 손으로 만들어 보라.
decl_print(),stmt_print()등을 추가해 AST를 다시 출력하라. indentation과 일관된 spacing으로 보기 좋게 format하라.- AST generator function을 Bison grammar의 action rule로 추가해, 완전한 program을 parse하고 다시 출력하라.
decl_translate(),stmt_translate()등을 추가해 B-Minor AST를 Python, Java, Rust 같은 다른 언어로 출력하라.- AST를 graphical form으로 emit하는 function을 추가하라. 한 방법은 Graphviz DOT format: 각 declaration·statement를 node로, 각 pointer를 edge로 표현.
⭐핵심 정리 / 시험 포인트
- AST는 프로그램의 primary structure를 담는 internal data structure이며 semantic analysis의 출발점이다.
- "abstract"=parsing detail 생략 → prefix/postfix/infix 무관. 대부분의 procedural language 표현 가능.
- AST는 5개 structure:
decl,stmt,expr,type,param_list. - declaration=symbol의 name/type/value 선언, statement=state를 바꾸는 action, expression=value+operation → value 산출.
- variable declaration의 default value=zero. body 없는 function declaration=prototype.
struct stmt는 kind field로 종류를 구분하고, kind마다 필요한 field만 쓰고 나머지는 null.- Unary=
left에 argument. Function call=EXPR_CALL+EXPR_ARG의 unbalanced tree. Array subscript=binary처럼EXPR_SUBSCRIPT. - integer/boolean/char literal 값은 모두
integer_valuefield에 저장. - function type:
subtype=return type,param_list=parameter linked list. compound type은subtype으로 연결. - parser가 reduce될 때 creation function 호출.
decl_list는 right-recursive(끝은 epsilon→null). - rule마다 반환 type이 다르므로
%union으로 묶고%type <field>로 subfield 지정.
decl | name, type, value, code, next |
stmt | kind 기반, if/for/block/return 등 |
expr | binary + leaf (name/literal) |
type | kind, subtype, params |
param_list | name, type, next (linked list) |
!x→ unary, argument in leftf(a,b,c)→EXPR_CALL+EXPR_ARGtreea[b]→EXPR_SUBSCRIPT(binary)
- Q. AST가 "abstract"한 이유는? → parsing의 particular detail(prefix/postfix/infix 등)을 생략하기 때문.
- Q. AST를 이루는 5개 structure는? → decl, stmt, expr, type, param_list.
- Q. function call은 AST에서 어떻게 표현되나? →
EXPR_CALL(left=name, right=EXPR_ARG의 unbalanced tree). - Q.
decl_list를 right-recursive로 만드는 이유는? → declaration들을 linked list로 연결하고, 끝(epsilon)에서 null로 종료하기 위해. - Q. 왜 Bison에
%union이 필요한가? → rule마다 반환 type이 달라(struct decl*, char* 등) semantic value를 모든 AST type의 union으로 표현해야 하기 때문.
📋한눈에 보기 — Cheat Sheet
| 키워드 / 요소 | 의미 |
|---|---|
| AST | abstract syntax tree. semantic analysis의 출발점 |
| struct decl | name, type, value, code, next |
| struct stmt | kind + decl/init_expr/expr/next_expr/body/else_body/next |
| struct expr | kind, left, right, name, integer_value, string_literal |
| struct type | kind, subtype, params |
| struct param_list | name, type, next (linked list) |
| STMT_IF_ELSE | expr=control, body=true, else_body=false |
| STMT_FOR | init_expr, expr, next_expr, body |
| STMT_BLOCK | body=중괄호 안 statement들 |
| EXPR_NAME / _INTEGER_LITERAL | leaf: name field / integer_value field |
| EXPR_NOT (unary) | argument는 left pointer에 |
| EXPR_CALL / EXPR_ARG | function call: name + arg의 unbalanced tree |
| EXPR_SUBSCRIPT | a[b] → binary operator처럼 |
| TYPE_FUNCTION | subtype=return type, params=param_list |
| TYPE_ARRAY | subtype으로 임의 깊이 연결 |
| decl_create / stmt_create / expr_create | factory function (malloc + 필드 초기화) |
| kind | structure 종류 enum field |
| decl_list (right-recursive) | declaration linked list, 끝은 epsilon→null |
| %union | 모든 AST type을 묶는 semantic value type |
| %type <field> | 각 rule이 쓰는 union subfield 지정 |
| prototype | body 없는 function declaration |