CHAPTER 6

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 decldeclarationsymbol의 name, type, value를 선언
struct stmtstatementprogram의 state를 바꾸는 action
struct exprexpressionvalue와 operation의 조합 → value 산출
struct typetype변수·함수의 type 인코딩
struct param_listparameterfunction parameter의 linked list

6.1Overview

AST는 프로그램의 구조를 표현하는 internal data structure이며, parsing의 구체적 형식은 버린다.

💡 declaration / statement / expression 정확히 구분
  • 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를 바꾸기도 함.
⭐ 시험 포인트 — "abstract"의 의미

AST가 abstract한 이유는 parsing의 particular detail을 생략하기 때문이다. 언어가 prefix / postfix / infix expression 중 무엇을 쓰든 동일한 AST로 표현된다.

⚠️ 미리 보기가 필요한 이유

각 structure는 다른 structure를 가리키는 pointer를 갖는다(서로 얽혀 있음). 그래서 어떻게 함께 동작하는지 보기 전에 5개를 먼저 전부 훑어봐야 한다.

6.2Declarations

완전한 B-Minor program은 declaration의 sequence다. 각 declaration은 variable 또는 function의 존재를 명시한다.

💡 declaration의 규칙
  • 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;
}
⭐ 시험 포인트 — factory function & linked list
  • 이런 structure를 매우 많이 만들기 때문에, 할당과 필드 초기화를 해주는 factory function이 필요하다. (statement, expression 등도 같은 패턴)
  • next field로 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_DECLdecl → (local) declaration을 가리킴
STMT_EXPRexpr → expression statement
STMT_IF_ELSEexpr=control expression, body=true일 때, else_body=false일 때
STMT_FORinit_expr, expr, next_expr=loop header의 세 expression, body=loop 본문
STMT_PRINTexpr → 출력할 expression들
STMT_RETURNexpr → return할 expression
STMT_BLOCKbody → 중괄호 안의 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 );
⭐ 시험 포인트 — field가 많지만 kind마다 일부만 쓴다
  • 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;
  • name field는 EXPR_NAME에, integer_value field는 EXPR_INTEGER_LITERAL에 설정된다.
  • integer, boolean, character literal 값은 모두 integer_value field에 저장할 수 있다.

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

⭐ 시험 포인트 — 특수 expression 구성
  • Unary operator (logical-not 등): 유일한 argument를 left pointer에 둔다. 예: !xEXPR_NOT의 left에 EXPR_NAME(x).
  • Function call: EXPR_CALL node. left=function name, right=EXPR_ARG node들의 unbalanced tree. → tree로 linked list를 표현하며, code generation 때 stack 위 인자 처리를 단순화한다.
  • Array subscript: binary operator처럼 취급. EXPR_SUBSCRIPT의 left=array name, right=integer expression. 예: a[b].

!x 의 AST

EXPR_NOT / EXPR_NAME x

a[b] 의 AST

EXPR_SUBSCRIPT / \ EXPR_NAME EXPR_NAME a b

f(a,b,c) 의 AST — EXPR_CALL + EXPR_ARG의 unbalanced tree

EXPR_CALL / \ EXPR_NAME EXPR_ARG f / \ EXPR_NAME EXPR_ARG a / \ EXPR_NAME EXPR_ARG b / EXPR_NAME c

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

TYPE_ARRAY |subtype TYPE_INTEGER

array [] array [] integer

TYPE_ARRAY |subtype TYPE_ARRAY |subtype TYPE_INTEGER
⭐ 시험 포인트 — function type
  • function type은 subtype으로 return type을 표현하고, param_list node의 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_ARRAYsubtype을 임의 깊이로 연결 가능.
⚠️ higher-order type은 typecheck에서 거부됨

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;
}
💡 전체 AST가 어떻게 엮이는가
  • 최상위는 DECL "compute"typeTYPE_FUNCTION(subtype=TYPE_INTEGER, param="x":TYPE_INTEGER), code는 statement chain.
  • body는 STMT_DECL("i")STMT_DECL("total"=0)STMT_FORSTMT_RETURN으로 next linked 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_EXPREXPR_ASSIGN(total, EXPR_ADD(total,i)).

개략 구조 (statement chain)

DECL "compute" ├ type: TYPE_FUNCTION → TYPE_INTEGER, param "x":TYPE_INTEGER └ code: STMT_DECL "i" (TYPE_INTEGER) → STMT_DECL "total" = EXPR_INTEGER 0 → STMT_FOR init_expr : EXPR_ASSIGN(EXPR_NAME i, EXPR_INTEGER 0) expr : EXPR_LT(EXPR_NAME i, EXPR_INTEGER 10) next_expr : EXPR_INC(EXPR_NAME i) body : STMT_BLOCK STMT_EXPR EXPR_ASSIGN(EXPR_NAME total, EXPR_ADD(EXPR_NAME total, EXPR_NAME i)) → STMT_RETURN: EXPR_NAME total

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 */
       . . .
     ;
⭐ 시험 포인트 — right-recursive linked list
  • decl structure는 따로 생성되므로 decl_listlinked list로 연결해야 한다.
  • 이를 위해 rule을 right-recursive로 만든다: 왼쪽 decl=하나의 declaration, 오른쪽 decl_list=나머지 list.
  • list의 끝은 decl_listǫ(epsilon)을 생성할 때 null value.
⚠️ 마지막 복잡한 문제 — semantic value의 type

각 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 (연습문제)

  1. B-Minor의 완전한 LR grammar를 작성하고 Bison으로 test하라. 첫 시도는 shift-reduce, reduce-reduce conflict가 많을 것이므로 Chapter 4의 grammar 지식으로 rewrite해 conflict를 제거하라.
  2. 이 장에서 설명한 AST structure와 generating function을 작성하고, nested function call로 간단한 AST를 손으로 만들어 보라.
  3. decl_print(), stmt_print() 등을 추가해 AST를 다시 출력하라. indentation과 일관된 spacing으로 보기 좋게 format하라.
  4. AST generator function을 Bison grammar의 action rule로 추가해, 완전한 program을 parse하고 다시 출력하라.
  5. decl_translate(), stmt_translate() 등을 추가해 B-Minor AST를 Python, Java, Rust 같은 다른 언어로 출력하라.
  6. 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 stmtkind 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_value field에 저장.
  • function type: subtype=return type, param_list=parameter linked list. compound type은 subtype으로 연결.
  • parser가 reduce될 때 creation function 호출. decl_listright-recursive(끝은 epsilon→null).
  • rule마다 반환 type이 다르므로 %union으로 묶고 %type <field>로 subfield 지정.
💡 5개 structure 한 줄 요약
declname, type, value, code, next
stmtkind 기반, if/for/block/return 등
exprbinary + leaf (name/literal)
typekind, subtype, params
param_listname, type, next (linked list)
💡 특수 expression 패턴
  • !x → unary, argument in left
  • f(a,b,c)EXPR_CALL + EXPR_ARG tree
  • a[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

키워드 / 요소의미
ASTabstract syntax tree. semantic analysis의 출발점
struct declname, type, value, code, next
struct stmtkind + decl/init_expr/expr/next_expr/body/else_body/next
struct exprkind, left, right, name, integer_value, string_literal
struct typekind, subtype, params
struct param_listname, type, next (linked list)
STMT_IF_ELSEexpr=control, body=true, else_body=false
STMT_FORinit_expr, expr, next_expr, body
STMT_BLOCKbody=중괄호 안 statement들
EXPR_NAME / _INTEGER_LITERALleaf: name field / integer_value field
EXPR_NOT (unary)argument는 left pointer에
EXPR_CALL / EXPR_ARGfunction call: name + arg의 unbalanced tree
EXPR_SUBSCRIPTa[b] → binary operator처럼
TYPE_FUNCTIONsubtype=return type, params=param_list
TYPE_ARRAYsubtype으로 임의 깊이 연결
decl_create / stmt_create / expr_createfactory function (malloc + 필드 초기화)
kindstructure 종류 enum field
decl_list (right-recursive)declaration linked list, 끝은 epsilon→null
%union모든 AST type을 묶는 semantic value type
%type <field>각 rule이 쓰는 union subfield 지정
prototypebody 없는 function declaration