충남대학교 컴퓨터공학과 조은선 교수님의 "컴파일러 개론" 강의를 필기한 내용입니다.

다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.

Semantic analysis 이전에 할거

  • Lexical analysis와 Syntax analysis를 끝내면 그 결과로 나온 Parse tree로 Semantic analysis를 하는데
  • Semantic analysis로 들어가기 전에 Parse tree에 좀 살을 붙이는 작업을 한다
    • SDD(Syntax-Directed Definition) 혹은 SDT(Translation) 이나
    • AST(Abstract Syntax Tree) 의 도구를 이용하더라

Syntax Directed Definition / Translation (SDD / SDT)

  • 얜 뭐냐
  • 과제에서 했던게 사실 SDD였던거임 - 어떤 생성규칙을 방문했을때, 그리고 빠져나왔을때의 Action을 설정해서 파싱 과정에서 다양한 정보를 생산할 수 있게 하는 것
  • 따라서 생성규칙마다 Action을 적어놓으면 파싱 과정 혹은 파싱된 이후에 노드를 방문하며 Action들을 수행하게 된다
    • 보통은 트리가 생성된 다음에 순회하지만 생성되면서 순회하는 방법도 있댄다
    • LR은 Bottom-up으로 생성되면서 하고 LL같은 경우에는 Recursive descent parser구현에서 심볼마다 할당된 함수가 return될때 방문하게 됨
  • 그럼 이 Action은 어떻게 적느냐
  • Non-terminal마다 ‘값’이 있고 그 ‘값’들로 코드를 짜면 그게 Action이 됨
  • 이렇게 말하면 뭔 소린가 싶은데

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image1.png

  • 이게 Yacc / Bison에서의 예시임
  • 보면 일단 생성규칙이 있고
  • {} 에 Action을 적어주게 되는데
  • Yacc에서는 RHS의 값은 저 $$ 라는 변수에 저장되게 되고 LHS는 앞에서부터 차례대로 $n 의 변수에 저장되게 된다

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image2.png

  • 위 그림처럼 된다는거임
  • 따라서 위의 예시에서는 RHS의 값인 $$ 으로 첫번째 expr의 값인 $1 과 두번째 expr 값인 $3 을 더하게 되는 것
  • 여기서 주의할점은 여기에서 등장하는 변수들은 scope가 해당 생성규칙에 한정된다는 것
    • 뭔소리냐면 만약에 $1 이 10이고 $3 이 30이었다면 $$ 의 값은 40이 되것지
    • 근데 이때의 root는 또 다른 서브트리의 leaf가 될 수 있것지
    • 그럼 그때 40이라는 값은 $$ 에 저장되는게 아니라 $n 에 저장된다는 거다
    • 즉, 하나의 서브트리에 한정해서 루트의 값이 $$ 이고 리프의 값이 $n 이 된다는 소리임

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image3.png

  • 위 그림 보면 뭔소린지 감은 잡힐거임
  • 뭐 Terminal에 대해서는 값이 없다고 생각할 수 있는데 Lex에서 던져주는 식으로 값을 가져올 수도 있댄다 - 따라서 Terminal에 대해서도 별도의 변수가 할당되어 있는 것

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image4.png

  • ANTLR에서는 위처럼 표현된다는거임
  • ANTLR는 파싱트리가 생성되고 난 후에 Post order traversal로 순회하며 Action을 취한다
  • 따라서 BaseListener라는 클래스 원형과 Listener 라는 인터페이스를 제공하여 얘네들을 상속 / 구현해 Action들을 명시하게 되어 있음
  • 생성규칙 서브트리로 진입했을 때와 나갔을때 호출되는 enter뭐시기, exit뭐시기 메소드들이 있고 얘네들 안에 코드를 적어주면 그 코드가 실행되는 것
  • 자세한건 과제 참고해라

Attribute

  • 위의 예제에서는 $$, $n 의 값이 정수인것으로 가정했는데
  • $$, $n 을 정수가 아닌 구조체로 명시할 수도 있다 - 이때의 구조체 Property를 Attribute 라고 부름

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image5.png

  • val이라는 Attribute가 있다고 어딘가에서 명시했다는 가정 하에 위처럼 사용할 수 있는 것

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image6.png

  • Attribute를 명시하는 방법은 위처럼 하면 된다
  • 일단 Lex에서도 yylval에 해당 Attribute가 있다고 생각하고 코드를 짠 후에
  • yacc의 선언부(맨 위)에 %union {} 으로 Attribute 목록을 적어주고 각각의 심벌들에 대해서 어떤 Attribute를 가지는지 명시해줄 수 있는 것
  • 근데 중요한점은 위에서는 구조체라고 했지만 사실은 Union type인 것 - Attribute마다 각각의 메모리가 잡히는게 아닌 메모리를 한덩이 잡아놓고 Attribute 각각에 따라 자료형만 바뀌는거다
  • 즉, 하나의 심벌이 여러개의 Attribute를 가질 수는 없는 셈
  • 따라서 위처럼 명시하게 된다 - $$, $n 를 union type으로 명시하고 각각의 심벌마다 specific type을 지정해 주는 셈임
  • 심벌 각각에 Attribute를 지정해 줄 때는 Non-terminal일때는 %type <attr> N 이런식으로 명시하고
  • Terminal에 대해서는 %token <attr> T 이런식으로 명시한다

Attribute의 종류

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image7.png

  • 이런 생성규칙에 대해

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image8.png

  • 위처럼 children의 attribute를 이용해 parent의 attribute를 채우는 경우 Synthesized Attribute라고 부르고

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image9.png

  • parent, sibling의 attribute를 이용해 child의 attribute를 채우는 경우 Inherited Attribute 라고 부른다
  • Synthesized Attribute 의 경우에는 Bottom-up방향으로 attribute값이 propagation되고 Inherited Attribute 의 경우에는 Top-down방향으로 attribute값이 propagation되게 된다
  • 당연히 Terminal의 경우에는 Synthesized Attribute밖에 가지지 않는다

SDD를 이용한 자료형 선언 예시

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image10.png

  • 일단 위의 예시에서 intType과 floatType은 상수로 선언되어있다고 가정한다
  • 그리고 AddType(id, type)이라는 함수를 이용해 해당 id(변수)의 자료형으로 type을 가지게 한다고 가정한다
  • 마지막으로 D1저거는 그냥 D랑 똑같다고 생각하셈 - recursive declaration 이어서 RHS랑 이름이 겹치니까 걍 좀 다르게 적었다는 느낌
  • 그럼 이 예시에서는 Bottom-up방식으로 propagation되며 변수의 자료형이 선언된다는 것을 알 수 있을것이다
    • RHS의 attribute를 채울 때 LHS의 attribute값을 이용하면 Bottom-up방식으로 attribute가 채워지며 propagation된다
  • 위의 예시에서 D의 타입도 지정해주는 이유는 두번째 생성규칙 보면 D가 재귀적으로 나오고 그 뒤에 id가 뒤이어 오기 때문에 D뒤에 나오는 id는 D의 타입을 따르게 해주기 위함

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image11.png

  • 또 다른 예시임
  • 이번에는 자료형 명시부분 T와 변수 명시 부분 L로 나누어 TL로 D가 표현되는 방식인데
  • 보면 위의 예시에서는 Bottom-up 방식 뿐 아니라 Top-down방식으로도 propagation되며 자료형이 선언된다는 것을 알 수 있다
    • LHS의 특정 attribute를 채우는데 RHS가 사용된다면 이때는 Top-down 방식으로 attribute가 채워지면 propagation되는 것

AST(Abstract Syntax Tree)

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image12.png

  • 뭔지는 프언개때 배워서 알거임
  • Parse tree에서 문법적인 부분은 모두 제거하고 의미적인 부분만 남겨서 트리구조를 단순화시킨 것
  • 보면 괄호같은 경우에는 우선순위를 잡아주는 역할을 하는데 파스트리에서는 트리의 깊이도 우선순위가 녹아있기 때문에 다 제거가 되고
  • Non-terminal의 경우에도 Terminal로 바로 치환해서 불필요한 것들을 다 제거하게 된다

구현 예시

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image13.png

  • 일단 자바로 짠 자료구조이다
  • 객체지향 언어에서는 Polymorphism을 이용해 저래 표현할 수 있다더라

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image14.png

  • 이건 C로 짠 코드인데
  • 객체지향 언어가 아닌 경우에는 임의 자료형 자식 n개를 갖는 것을 표현하기 어렵댄다
    • 자바같은경우에는 뭐 임의 자료형을 일반화해서 하나로 묶는게 되지만 C언어에서는 안되니까
  • 임의 갯수의 자식을 가지는 것을 표현하기 위해 저렇게 N-ary tree를 binary tree로 바꾸는 방법을 이용한다
  • 자식 포인터와 형제 포인터 두개만으로 n번째 자식에 도달하기 위해서는 첫번째 자식으로 간 다음에 링크드 리스트마냥 쭉 옆으로 따라가는 것

AST 만들기

LL파싱 하는 와중에 만들기

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image15.png

  • 왼쪽 위에가 옛날에 배운 Recursive descent parser이고 오른쪽 아래가 AST생성하는 버전
  • 자식이 준 AST 서브트리를 모아다가 합쳐서 반환하는 방식으로 구현

LR 파싱 하는 와중에 만들기

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image16.png

  • AST이기 때문에 당연히 의미있는 것에 대해서만 Node를 생성함
  • 그래서 위처럼 Shift-reduce할때
  • 의미있는 Terminal을 Shift할때 단말 노드를 만들고
  • 의미있는 생성규칙을 Reduce할때 단말 노드를 합쳐 Subtree를 만든다

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image17.png

  • 그래서 좀 더 자세히 보면 위처럼 됨
  • 일단 심벌들마다 추가적인 공간이 있어 여기에 서브트리가 저장이 되고
  • Reduce되고 난 뒤에 트리가 저렇게 합쳐지는 것을 볼 수 있음

SDD로 만들기

%E1%84%8B%E1%85%B5%E1%84%85%E1%85%A9%E1%86%AB11%20-%20SDD,%20AST%20c7fd5fdf1e184d4cbdc23c1767c52b94/image18.png

  • 별거 없다
  • node라는 attribute가 있어서 각각 expr에 대한 서브트리를 저장 하게 되고
  • Action에서 자식의 노드를 이용해 나 자신의 노드를 채우는 연산을 수행 하게 된다

Evaluation

  • Parse Tree혹은 AST에서 각각 노드를 방문하며 명시된 Action을 작동시키는 것을 Evaluation 이라고 하는데
  • 그 방법중 하나로
  • On-the-fly Evaluation 은 AST Node 방문 순서대로 Evaluation하는 것인데
  • 가장 효율적이긴 하나 제약조건이 있다
    • S-attributed SDD : Synthesized Attribute만 가지고 있는 경우
    • L-attributed SDD : Synthesized Attributed와 더불어 parent가 아닌 sibling attribute 만 이용해 attribute를 계산하는 경우
    • 위 두가지 경우에만 Evaluation이 가능하다