컴파일러 CH3. Syntax Analysis

Alpha, Orderly·2023년 4월 3일
0

컴파일러

목록 보기
2/5

Context Free Grammar

예시 : 주어 동사 목적어로 이루어진 영어 문장

  • Sentence / Subject / Object
    • 결과로 Non-Terminal Symbol또한 가진다.
  • Noun / Verb
    • 결과로 Terminal Symbol만 위치한다.

생성 예시


Subject / Verb / Object / Noun 순서로 Terminal Symbol로 대체되어 진다.

형태

Left hand side = Non-terminal / Right hand side = Non-Terminal or Terminal

Grammar의 Terminal은 Token이다.
Token의 구조화를 Parser가 진행한다.


Derivation / 유도

  • 문법으로부터 문자열을 유도(Derive) 한다.
  • 각각의 유도 단계(Derivation Step) 에서 Nonterminal Symbol은 우측의 Terminal Symbol 혹은 Non-Terminal Symbol 로 교체된다.
  • 마지막 문자열 형태는 Terminal Symbol / Token 만을 가진다.
    • Non-terminal Symbol을 포함해선 안된다.
  • CFG는 Context-Free-Language의 생성기로, 문법으로 정의되는 모든 언어는 곧 유도될수 있는 모든 문자열과 같다.

CFG는 주어진 프로그래밍 언어의 가능한 문자열을 정의한다.

-> 프로그래밍 언어 문법의 Formal Description

CFG Element

  • Terminal Symbol
    • Token
    • <=, while, if, INTLITERAL
  • Nonterminal Symbol
    • 언어의 특정 Class 문구
    • Program, Command, Expression, ...
  • Start Symbol
    • Nonterminal 중 하나면서 첫번째 유도 과정에 왼쪽으로 들어간다.
    • EX/ Micro English에서 Sentence
    • 컴파일러에서는 프로그램 그 자체를 의미.
  • Productions
    • CFG의 Rule
    • 예시 : sentence ::= noun verb
    • sentence가 noun verb로 치환된다.

BNF

  • CFG를 표현하는 방법
  • ; 의 의미 : Command와 single-Command가 이어짐.

Micro-English의 표현

EBNF

  • 편의상 BNF와 Regex를 합친 EBNF를 사용한다.

    Kleene star clouser를 사용한다

Leftmost Derivation

  • 가장 왼쪽 끝에 있는 Nonterminal 먼저 치환한다.

Rightmost Derivation

  • 가장 오른쪽 끝에 있는 Nonterminal 먼저 치환한다.

CFG 작성 Convention

Start Symbol

  • 첫번째 Production의 왼쪽 방향 값.
  • 어디서든 문자 S로 표기한다.

Nonterminal

  • "sentence"나 "expr"처럼 소문자로 이루어져있다.
  • A, B, C 처럼 대문자로도 이루어진다.

Terminal

  • IDINTLITERAL과 같이 볼드체로 작성한다.
  • 숫자, 연산자, 괄호 등을 포함한다.

파스칼에서의 문법 예시

PROGRAM, integer, VAR 등은 Terminal으로 더이상 Derivation 되지 않는다.


Extended BNF

  • BNF에 Regex가 합쳐진 형태
  • LHS ::= RHS 와 같은 형태로 표현된다.
  • BNF에 아래와 같은 표현을 추가한다
    • (...) - 그룹
    • * - 0번 혹은 추가 반복
    • + - 1번 혹은 추가 반복
    • ? - 0 ~ 1 번 반복

예시

Kleene clouser {}^* 사용법

Positive clouser +{}^+ 사용법

Option ? 사용법


Grammar Transformation

Left Factorization

  • N::=XYXZN ::= XY | XZ == N::=X(YZ)N ::= X(Y|Z)
  • 왼쪽의 공통되는 부분을 묶어준다.

if Expression then single-Command 가 중복되어 묶어 주었다. 뒤에 나오는것은 ? 로 처리해도 될듯

Elimination of Left Recursion

  • 왼쪽 재귀호출이 제거 된다.
  • X로 재귀가 종료된다.
  • N::=XNYN ::= X | NY == N::=XYN ::= XY^*

Substitutuin of non-terminal symbol

  • N::=XN ::= X & M::=aNBM ::= a N B
  • N::=XN ::= X & M::=aXbM ::= a X b
  • Terminal Symbol이 바로 될수 있는 N을 Terminal Symbol인 X로 치환한다.

to-or-dt를 terminal symbol인 (to|downto)로 치환한 모습이다


Parse Tree



Rightmost derivation

Derivation과 Parse Tree

  • Parse Tree의 내부 노드는 Nonterminal이다.
  • 노드의 Child는 Production에서의 우측 Non-terminal 혹은 Terminal이다.
  • Parse tree의 Leaves는 전부 Terminal/Token 이다.
  • 왼쪽에서 오른쪽으로 leaves들을 읽을시 Derivated Sentence가 만들어진다.

Ambiguous Grammar

  • 두가지 이상의 방법으로 문자열이 Derive 될수 있으면, 문법이 모호(Ambiguous) 한 것이다.
  • 위 경우는 연산자 우선순위로 인해 모호함이 생겨났다.

해결

  • 우선순위가 다른 두 경우에 따라 경우를 나눈다.
  • 우선순위가 낮은것이 파싱트리의 상단에 위치하도록 한다.


에 대해서

는 모호한 구문이다.

왜?
  • ( IF a THEN ) ( IF b THEN x=false; ) ( ELSE x=true);
  • ( IF a THEN ) ( IF b THEN x=false; ELSE x=true );
  • 맨 끝부분의 ELSE가 IF a에 소속되는가, IF b에 소속되는가에 대한 모호함이 생긴다.

해결

  • 중괄호와 같은 코드 블럭을 적용 혹은 indent를 통해 코드블럭을 적용한다.
  • 명시적으로 END IF 라는 키워드를 도입한다.

Terminology

Recognition

  • 입력이 언어의 문법에 맞아 떨어지는가?
  • Recognizer가 CFG를 이용해 프로그램의 문법을 확인한다.

Parsing

  1. 입력 프로그램 Recognize
  2. 입력 프로그램의 구문 구조를 확인한다.
    • 파서는 CFG를 통해 프로그램의 문장을 파싱한다.
      • 프로그램이 문법에 대해 유효한가, 유효하지 않은가를 판단한다.
    • AST를 만든다.
      • AST : 추상구문트리
      • 프로그래밍 언어로 작성된 소스 코드의 추상 구문 구조의 트리이다.

Context Free Grammar Classes

n : 프로그램의 사이즈

  • 임의의 CFG에 대해 파싱은 O(n3)O(n^3) 의 시간이 소요될수 있다, 이는 너무 느리다.
  • 몇몇 문법의 클래스에 따라 O(n)O(n)에 파싱할수 있다.
    • Top Down - LL grammar - Leftmost derivation
    • Bottom up - LR grammar - Rightmost derivation

Top - Down Approach

  • 위에서부터 파싱 시작해 나감

Bottom - Up Approach

  • Terminal Token으로부터 파싱을 시작해 나감

Grammar Classes

  • LL 문법에 대해 LL 파싱 알고리즘을 통해 파싱
  • LR 문법에 대해 LR 파싱 알고리즘을 통해 파싱
  • LL 문법은 LR 문법의 부분집합이다.

LL 문법과 Top-Down parser

  • 예측성 파서 / Predictive parser
  • 왼쪽에 재귀하는 Production을 포함할수 없다, Left recursive production을 포함할수 없다.
  • 두개 이상의 production이 동일한 prefix를 가져선 안된다 ( Common Prefix )
  • 위 2개는 Grammar Transformation을 통해 제거 할수 있다.
  • Parse Tree가 Root로부터 만들어진다.
  • 토큰을 하나씩 보기 때문에 LL(1) Parser이라고도 부른다.


Recursive Descent Parsing

직접 LL/Top-down을 파싱하는 방법

  • EBNF로 부터 Recursive Descent Parser를 만들어 내기

예시

  1. EBNF 구조가 주어진다

1. 각각의 Left Hand Side의 Non-Terminal N에 대해 parseN 함수 만들기

  • parseSentence(); 를 먼저 실행해야 할 것이다.
  1. 클래스

Auxiliary method : 보조 메소드

  • Terminal / Token 을 받아들이는 함수.
  • 현재 토큰과 받아온 기대 토큰을 비교한다.

Parsing method : 파싱 메소드

  • Sentence를 파싱할때 Subject > Verb > Object 순으로 파싱 하고 "." 을 받아들인다.

- 파싱 메소드 예시

  • 각각의 예시에 따라 Terminal은 accpet로 받고, Non-Terminal의 경우 다시 parseXXX(); 재귀적으로 호출한다.
  • 모든 경우에 해당하지 않을경우 문법 에러를 리턴한다.

- 명사/ Noun 파싱 예시

  • 전체가 Terminal/Token 으로 끝나기에 parseXXX()는 호출되지 않고 전부 accept로 받아진다.

RD 파서의 체계적 개발(Systemetic Development)

  1. EBNF로 문법을 표기하기

  2. Left Factorization / Left Recursion Elimination 를 통해 정리하기 - Grammar transformation

  3. 파서 클래스 만들기

    • private variable : currentToken
    • 스캐너 호출 메소드 : accept and acceptIt
  4. EBNF를 RD 파서로 변환하기

    • Nonterminal N에 대해 parseN(); 함수 만들기
    • parseS(); 실행하기 - Starting symbol의 Production function
    • parseS(); 의 리턴 결과 확인하기.


    • FIRST[x] : x Nonterminal이 RHS에서 처음으로 가질수 있는 Terminal들의 집합.
    • 복합적 예시
    • acceptIt() : 토큰 확인 하지 않고 받아오기
    • accept(TokenType) : 토큰 타입을 확인하고 받아오기

    FIRST 함수

    • 주어진 Non-Terminal Symbol에 대해 처음으로 올수있는 Terminal Symbol 들의 집합
    • 만약 문자열이 사라지면 { ϵ\epsilon } 을 추가해준다.
    • FIRST 집합이 겹친다 == 두 집합의 교집합이 공집합이 아니다. == Common prefix가 존재한다 == LL(1) 문법이 아니다.

    FOLLOW 함수

    FOLLOW(A)

    • 주어진 Non-Terminal Symbol A에 대해 다음에 올 Terminay Symbol들의 집합.

LL(1) Grammar

  • 앞에서 소개한 EBNF를 파서로 변환하는 알고리즘은 모든 문법에 가능하진 않고, LL(1) 문법에서만 사용이 가능하다.

LL(1) 문법이란

  • Left-to-right parsing of input stream : 왼쪽에서 오른쪽으로 받아오기
  • Leftmost derivation : 왼쪽부터 유도
  • 1 token look-ahead in the input stream : 토큰을 1개씩 받아와서 결정

LL(1) 문법의 정의

  • Left-recursion을 포함하는 문법은 LL(1) 문법이 이나다.
  • common prefix를 포함하는 문법은 LL(1) 문법이 아니다.
  • 모호한 문법은 LL(1) 문법이 아니다.
  • Grammar Transformation을 통해 위 정의를 맞추게 된다.

기말고사내용


파싱 테이블 만들기

  1. FIRST와 FOLLOW set을 구한다
  • FIRST에는 ϵ\epsilon 포함 가능
  • X::=Y1Y2...X ::= Y_1Y_2... 에서 FIRST(X)에 FIRST(Y1Y_1) - {ϵ\epsilon} 이 들어간다.
  • Y의 FIRST에 epsilon이 있을 경우 이를 반복한다.
  • 모든 Y의 FIRST에 epsilon이 있을시 < 이들이 모두 epsilon이 될시 > FIRST(X)에도 epsilon이 들어간다.
  1. x축에는 토큰, y축은 nonterminal을 넣어 테이블을 만든다
  • 여기서 column index의 토큰은 이 토큰이 FIRST로 왔을때 y축의 NONTERMINAL이 어떤 식으로 파싱될수 있는지를 담는다.
  • id는 S, E, T, F의 FIRST에 포함되기 때문에 작성된 것이다.
  • 만약 파싱 도중 빈칸에 도달할 시, 이는 올바르지 못한 파싱을 의미한다.

모호성

  • 모호성은 대부분 연산자 우선순위에 의해 기인한다.
  • 더 높은 우선순위의 연산자가 나중에 들어가게 하면 된다.

AST / Abstract Syntax Tree

  • 파싱 트리의 간단한 형태
  • 타입체크, 코드 최적화, 코드 생성을 위한 입력 프로그램의 중간 표현


AST 클래스 디자인하기

  • AST.java를 최상위 추상 클래스로 둔다.
  • Non-terminal 들에 대하여 Parser 함수를 만들어둔것을 바탕으로 하여 AST 클래스를 생성한다.
  • 괄호, 타입 표시자, for/while/if 등의 키워드에 대해 terminal node 클래스를 둘 필요는 없다.
완성된 AST를 시각화 한것

AST 클래스 생성자

  • 모든 AST 클래스는 생성자를 필요로 한다.
  • 본 생성자는 While Statement의 조건과 본문을 가져온다.
  • 기존에 만들어 두었던 파싱 함수를 통해 구현하고, 생성자를 호출한다.
  • while / for / 괄호 는 non-terminal이 아닌 accept로 받아오는것을 볼수 있다.
  • 구문이 들어가나 들어가지 않나로 판단할수도 있다.
  • 비어있는 구문을 확인해야 할수도 있다.
  • 재귀적 호출을 사용할수도 있다.
  • 이들에 맞지 않으면, 문법 에러를 뱉어낼수 있다.
profile
만능 컴덕후 겸 번지 팬

0개의 댓글