이론없이 컴파일러 만들기 꿈파일러 2
토큰화하는 방법
이전 장에서 토큰화가 무엇인지 알아봤다.
우리는 산업역군의 프로그래머이기 때문에 구현가능한 형태를 원한다. 예제: flex 를 이용해서 간단히 토큰화를 어떻게 하는지 살펴보겠다.
flex 가 무엇인지 살펴보면
flex는 《fast lexical analyzer generator》의 줄임말로 lex의 기능을 개선한 자유 소프트웨어이다. 주로 bison과 쌍을 이루어 구문 분석기를 만드는 데 사용된다. flex를 이용하면 C로 구문 문석 코드를 만들 수 있다. 한편 C++ 코드를 만들어 주는 비슷한 기능을 하는 프로그램으로 flex++가 있으며 flex와 함께 배포된다. 작성자는 “Vern Paxson”씨로 1987년도에 처음 만들어졌다.
앞 장에서 봤듯 입력된 문법이 적법한지 상관없이 어떤 종류의 문자열인가를 판단하는 1차 분류로 생각하면 된다.
%{
Prologue
%}
Bison declarations
%%
Grammar rules
%%
Epilogue
구조는 위와 같은데, 우리는 마지막 Epilogue 는 쓰지 않을 것이다. 처음 %{ … %} 사이에는 include 문들이 오고, Bison declarations 에는 flex 가 해당 문법파일을 어떻게 컴파일할것인지 옵션이 담긴다. 다음으로 Grammer Rules 에 우리가 분류해야 할 토큰 규칙들이 위치한다. 자세한 것은 인터넷 검색을 통해 해결하도록 하자. flex 에 대한 것들을 자세히 설명하기보다 바로 예제를 통해 이해하도록 한다.
예를 들어 보자.
%{
#include <iostream>
#include <cstdlib>
#include "scanner.h"
using namespace std;
#define yyterminate() 0;
%}
%option nodefault
%option noyywrap
%option c++
%option yyclass="Scanner"
%option yylineno
%%
[a-z][a-z0-9]* {
std::cout << yytext << std::endl;
return 11;
}
[0-9][0-9]* {
std::cout << yytext << std::endl;
return 12;
}
= {
std::cout << yytext << std::endl;
return 13;
}
[\n\t ] {
}
. {
}
<<EOF>> {
std::cout << "EOF" << std::endl;
return yyterminate();
}
%%
%% 사이의 문법만 주목해서 보자. 정규식 { … } 형태로 기술되는데, 블록{} 사이에는 C/C++ 코드가 들어갈 수 있다.
이것만 기억하면 된다. 좌측에 정규식이 평가될 때 {} 사이의 코드가 실행된다.
// scanner.h
class Scanner : public yyFlexLexer {
public:
Scanner(std::istream *in) : yyFlexLexer(in) {}
virtual int yylex();
};
// lex.yy.cc
#define YY_DECL int Scanner::yylex()
...
#include "scanner.h"
...
YY_DECL
{
register yy_state_type yy_current_state;
register char *yy_cp, *yy_bp;
register int yy_act;
...
}
case 1:
YY_RULE_SETUP
#line 14 "scanner.l"
{
std::cout << yytext << std::endl;
return 11;
}
YY_BREAK
case 2:
YY_RULE_SETUP
#line 18 "scanner.l"
{
std::cout << yytext << std::endl;
return 12;
}
YY_BREAK
case 3:
YY_RULE_SETUP
#line 22 "scanner.l"
{
std::cout << yytext << std::endl;
return 13;
}
YY_BREAK
case 4:
/* rule 4 can match eol */
YY_RULE_SETUP
#line 26 "scanner.l"
{
}
YY_BREAK
case 5:
YY_RULE_SETUP
#line 28 "scanner.l"
{
}
YY_BREAK
case YY_STATE_EOF(INITIAL):
#line 30 "scanner.l"
{
std::cout << "EOF" << std::endl;
return yyterminate();
}
YY_BREAK
위 코드를 보면 scanner.l 이 flex를 이용하여 생성된 lex.yy.cc 파일에 블록 {} 사이의 코드가 그대로 들어간 모습을 볼 수 있다.
▾ tutorial1/
main.cpp
scanner.h
scanner.l
$ cd tutorial1
$ flex scanner.l && g++ lex.yy.cc main.cpp -std=c++11 && ./a.out
일부러 흐름을 잘 보여주기 위해 당분간은 CMake 를 쓰지 않겠다. 컴파일 되는 흐름을 잘 이해해야 한다. 먼저 flex 명령어를 통해 lex.yy.cc 가 생긴다. lex.yy.cc 안에는 %option yyclass="Scanner"
에 의해 YY_DECL
매크로가 정의된다.
또한, Scanner::yylex 가 overriding 으로 구현된다.
렉서파일에 scanner.h 를 쓰고 또 Scanner 클래스가 flex 를 쓰고, 생성된 파일(lex.yy.cc) 에서 scanner.h 를 쓰는 이상한 기분이지만 이 상호작용을 잘 이해하고 넘어가기 바란다.
// main.cpp
std::istringstream is(R"(
apple 5543 hello world =
)");
Scanner scanner(&is);
while(1) {
int t = scanner.yylex();
if(t == 0) {
break;
}
std::cout << t << std::endl;
}
위 프로그램에 의해 flex 를 실행할 수 있다.
apple 5543 hello world =
입력된 문자열이 이와 같을 때 토큰 분류기는 11 12 11 11 13
과 같이 반환된다. 이렇게 토큰화를 통해 숫자형 배열에 담은 후 구문분석(Parsing)을 하게 된다.
동작하는 예제는 git clone https://github.com/hsnks100/dreampiler.git 의 tutorial1