Passion/Python

PLY - Lex에 대한 이해

sunshout 2007. 6. 27. 14:54
3. Lex

Lex는 lexical analyzer의 줄임말입니다.

우리가 생각을 글로 표현하면 문장이 나오죠.
예를 들어 "나는 일을 한다" 라는 문장이 있을 때
"", "", "", "", "한다" 가 각각의 의미를 가지는 단어가 되죠.
이렇게 하나하나의 단어를 Token이라고 합니다.

Lex의 역할을 긴 문장을 입력받아서 의미있는 단어들을 하나하나씩 추출하는 것이라고 보면 됩니다.

위의 예를 좀더 보면 우리는 명사, 조사, 동사 등으로 구분을 할 수 있습니다.
"나","일" 등은 명사로, "는","을" 은 조사로 "한다" 는 동사로...

이와 같이 Token은 Type과 Value로 정의할 수 있습니다.
즉 "나" 라는 Token은 (type, value)의 쌍이 되는거죠. 예를 들어 (type=명사, value="나") 이렇게

이렇게 모든 입력 받은 문장을 (type, value)의 쌍으로 구분하는 것이 lex의 역할이 됩니다.

3.1 Token 정의하기
가장 먼저 해야 하는 것이 우리가 정의할 언어가 어떤 Token들을 가지고 있는냐를 선언해야 합니다.
여기서는 우선 토큰의 이름(Type)을  정의하는 것입니다.

예를 들어 수식을 정의하는 언어를 설계한다고 하면

(Language : python)
# List of token names
tokens = (
    'NUMBER',
    'PLUS',
    'MINUS',
    'TIMES',
    'DIVIDE',
    'LPAREN',
    'RPAREN',
)

토큰의 이름은 마음대로 정의하면 된다.
여기서는 숫자 + - * / ( ) 만 존재한다고 보고 이를 각각의 이름으로 정의하였다.

3.2 Token 설명하기
위에서는 어떤 토큰들이 존재하는지를 나열하였다.
그러면 실제 이러한 토큰들이 어떻게 생겼는지를 설명하여야 한다.
이는 Regular expression을 이용하여 표현한다.
중요한 것은 각 토큰을 설명하는 부분은 토큰의 이름 앞에 t_를 붙인다.


(Language : python)
# Regular expression
t_PLUS  = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE= r'/'
t_LPAREN= r'\('
t_RPAREN= r'\)'

위에서 7개의 토큰을 정의하였는데 그 중에서 6개의 토큰을 설명하였다.
그 이유의 위의 6개의 토큰은 단순한 한글자들의 기호로 이러한 토큰들이 존재하고 있다고만 이해하면 되기 때문이다.
여기서 특별하게 \ 이 붙어있는 것들은 regular expression에서 특별한 용도로 사용되기 때문에
이 용도로 사용하지 않고 단순한 character로 인식되기 위해서 \를 붙여야 한다.

예를 들어 r'+' 는 어떤 단어가 한개 이상 존재한다는 말이 되지만,
r'\+'는 +라는 character를 가리키게 된다.

regular expression에 사용되는 특수 문자들은
. ^ $ * + ? { } ( ) [ ] 가 있다. 이러한 문자들을 character로 사용하기 싶으면 앞에 \를 붙이면 된다.

하지만 숫자의 경우에는 그 값들이 너무나 다양하기 때문에 좀더 복잡한 과정이 필요하다.
Lex에서 숫자나 문자와 같이 토큰으로 분류한 후 어떤 작업이 필요한 경우에는 함수로 만들면 된다.

위에서 토큰을 숫자로 인식하면 Yacc에 숫자로 넘기기 위해서는 아래와 같이 하면 된다.

(Language : python)
def t_NUMBER(t):
    r'\d+'
    try:
         t.value = int(t.value)
    except ValueError:
         print "Number %s is too large!" % t.value
     t.value = 0
    return t

파라메터는 token 오브젝트를 받아서 처리를 하고 리턴을 하면 된다.

Token class는 다음과 같이 네 개의 내부 변수를 가지고 있다고 보면 된다.

Token class (Language : python)
# Token class                                                                                       
class LexToken(object):                                                                             
    def __str__(self):                                                                               
        return "LexToken(%s,%r,%d,%d)" % (self.type,self.value,self.lineno,self.lexpos)             
    def __repr__(self):                                                                             
        return str(self)                                                                             
    def skip(self,n):                                                                               
        self.lexer.skip(n)

{type, value, lineno, lexpos} 이다.

다음으로 Reserved word에 대해서 알아보면
reserved word는

(Language : python)
reserved = {
    'if' : 'IF',
    'then' : 'THEN',
    'else' : 'ELSE',
}

이렇게 정의해 놓으면 된다. 그러면 if, then, else 등의 단어를 만나면 IF, THEN, ELSE 등의 토큰으로 리턴하게 된다.
이는 t_IF, t_THEN, t_ELSE 를 만들 필요가 없다.

아래와 같이 설명하면 된다.

(Language : python)
def t_ID(t):
    r'[a-zA-Z_][a-zA-Z)0-9]*'
    t.type = reserved.get(t.value,'ID') # Check for reserved words
    return t

3.5 Discarded tokens
스페이스, 텝 등은 문장에서 단지 사람들이 편리하게 이해하기 위해서 넣는 부분이고,
실제적으로 파서까지 전달할 필요는 없다.

이런 경우는 다음과 같이 처리한다.

(Language : python)
def t_COMMENT(t):
    r'\#.*'          #으로 시작하고 어떤 문자던 올 수 있다.
    pass
    # No return value. Token discarded

또는
t_ignore_COMMENT = r'\#.*'


3.6 Line Number 와 Position 정보
사실 lex 자체가 라인번호에 대해서 알고 있는 것은 아니다.
그래서 일반적으로 newline기호인  \n 이 나오면 한 줄이 증가했다고 기억하면 된다.

Line number (Language : python)
# Define a rule so we can track line numbers
def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)

에러 핸들링을 위해서는 위의 라인번호 뿐만 아니라 column 정보도 필요하다.
이는 아래와 같이 처리하면 된다.

3.7 Ignored characters
탭이나 스페이스 같은 것은 Lex에서 이해하고 버리면 된다.
이럴 경우에는

(Language : python)
t_ignore = ' \t'




Column 정보 (Language : python)
# Compute column.
#     input is the input text string
#     token is a token instance
def find_column(input,token):
    i = token.lexpos
    while i > 0:
        if input[i] == '\n': break
        i -= 1
    column = (token.lexpos - i)+1
    return column

3.8 Literal Characters
위에서는 + - * / 등에 토큰 이름을 만들어서 정의하였다.
이 방법외에도

(Language : python)
literals = [ '+','-','*','/' ]

또는

literals = "+-*/"

 이렇게 정의할 수도 있다.
이럴 경우 + 의 토큰이름은 +가 된다.

3.9 Error handling
토큰으로 구분을 못했을 경우

Error 처리 (Language : python)
# Error handling rule
def t_error(t):
    print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)

3.10 Main function
이제 Lex를 돌릴 차례이다.

(Language : python)
lexer = lex.lex()
lexer.input(sometext)
while 1:
    tok = lexer.token()
    if not tok: break
    print tok