[루아1.1] Lua.lex 읽기

7 분 소요

루아 다운받기

루아 소스 코드는 인터넷에서 쉽게 찾을 수 있습니다. 구글님이 도와주시죠. 아래 링크에서 다운 받을 수 있습니다. 위 페이지에 보면 루아 1.0도 있습니다.

https://www.lua.org/ftp/

시작을 1.1부터 하는 이유는 1.0 소스 코드를 보면 lex와 yacc 소스 파일이 없습니다. 소스 코드 안에 설명에도 써 있습니다.

This is Lua 1.0. It was never publicly released. This code is a snapshot of the status of Lua on 28 Jul 1993. It is distributed for historical curiosity to celebrate 10 years of Lua and is hereby placed in the public domain.

The source files for the lexer and parser have been lost: all that is left is the output of lex and yacc. A grammar can be found inside y_tab.c in yyreds.

대충 해석해보면 이렇습니다. 루아 1.0은 정식 릴리즈가 아니라 루아 탄생 10주년 기념으로 공개한 스냅샷이라고 합니다. 그리고 소스 파일 중에 lex와 yacc 파일이 없어서 문법은 y_tab.c를 봐야 알 수 있다고 하네요. y_tab.c는 yacc가 gcc 보라고 만든 파일이지 사람이 읽으라고 만든 파일이 아니기 때문에 안보겠습니다. 물론 yyreds라는 변수에 yacc 파일에 적은 코드가 그대로 적혀 있긴 하지만 1.1과 딱히 다른점이 없으니, 그거 본다고 시작부터 진 빼기 싫거든요. 그래서 1.1부터 보기로 결정한 것입니다.

루아 1.1 소스 코드를 보면 include 디렉터리와 src 디렉터리가 있습니다. 읽어야 할 파일들은 다 여기 있군요.

|-- include
|   |-- lua.h
|   |-- lualib.h
|   `-- mm.h
|-- src
|   |-- hash.c
|   |-- hash.h
|   |-- inout.c
|   |-- inout.h
|   |-- lex.c
|   |-- Makefile
|   |-- opcode.c
|   |-- opcode.h
|   |-- table.c
|   |-- table.h
|   |-- y.tab.c
|   |-- y.tab.h
|   `-- yacc
|       |-- exscript
|       |-- lua.lex
|       |-- lua.stx
|       `-- Makefile

다 합쳐서 파일이 20개도 안됩니다. 왠지 비벼볼만하다라는 생각이 듭니다. 저는 파일 이름만 보고서 가장 먼저 든 생각이, ‘오, lex랑 yacc을 썼네?’ 였습니다. 제가 왜 그랬냐면, 저는 lex와 yacc은 정식으로 릴리즈하는 프로그래밍 언어에서는 안쓰는 줄 알았습니다. 실제로 루아 소스 코드를 보기 전까지 찾아 봤던 프로그래밍 언어들도 lex과 yacc은 쓰지 않았습니다. 심지어 루아도 최신인 5.3은 lex와 yacc을 쓰지 않습니다. 중간 어디쯤에서 걷어 냈겠지요. 저도 몇 년전 만들었던 프로그래밍 언어에서 lex와 yacc을 쓰지 않았습니다. lex와 yacc을 쓰면 성능 문제가 있다고 하는데 제가 이들을 쓰지 않았던 이유는 lex와 yacc을 쓰면 컴파일러 구현 코드가 .c 파일이 아니라 .yacc 파일에 많이 들어가게 되거든요. 그게 싫었던 건데 루아는 첫 번째 정식 릴리즈에서 그냥 lex과 yacc을 썼네요. 다르게 생각해보면 lex와 yacc은 태생이 컴파일러 만들으라고 있는 툴인데 그걸 굳이 안쓰는것도 웃긴일입니다. 일단 lex와 yacc으로 충분히 잘 돌아가는 컴파일러와 프로그래밍 언어 문법을 완성하고 나면 그때에 lex와 yacc을 걷어내도 되거든요.

시작도 하기전에 깨달음을 얻습니다.

Lua.lex

먼저 lex 파일입니다. lex 파일에는 프로그래밍 언어 문법을 기술할 때 쓰는 토큰에 대한 정규표현식을 기술하는 것이 일반적입니다. 그리고 그 작업에 필요한 C 언어 코드 약간하고요. 루아 1.1의 lex 파일도 크게 다르지 않을 것이라 기대하고 파일을 열겠습니다.

일단 짧군요. 마음에 듭니다. 루아 컨셉이 쉽고 간단한 언어여서 그런지 처리하는 토큰이 종류가 많지 않은가 봅니다.

%{

char *rcs_lualex = "$Id: lua.lex,v 1.1 1993/12/17 18:53:41 celes Exp $";

#include <stdlib.h>
#include <string.h>

#include "opcode.h"
#include "hash.h"
#include "inout.h"
#include "table.h"
#include "y.tab.h"

#undef input
#undef unput

static Input input;
static Unput unput;

void lua_setinput (Input fn)
{
 input = fn;
}

void lua_setunput (Unput fn)
{
 unput = fn;
}

char *lua_lasttext (void)
{
 return yytext;
}

%}

시작 부분입니다. lex 문법은 따로 찾아 보시기 바랍니다. 저는 읽기에 집중하겠습니다. stdlib.h와 string.h는 잘 알고 있는 표준 라이브러의 헤더들입니다. 일단 지나갑니다. 이어서 루아 1.1 소스 코드에 있는 opcode.h, hash.h, inout.h, table.h가 보입니다. y.tab.h는 나중에 yacc이 생성하는 헤더입니다. 그러면 lex 파일을 읽기 전에 opcode.h, hash.h, inout.h, table.h 이렇게 파일 네 개를 빠르게 훑어 보겠습니다.

빠르게 훑어 봤는데 아직은 뭘 의도하는 코드가 각 헤더에 작성된건지 모르겠군요. 나중에 알겠지요. 일단 어떻게 생겼는지만 보고 계속 lex 파일을 읽겠습니다. 분석이 아니라 읽기가 목적이니까요.

이어서 input과 unput이라는 이름을 #undef합니다. 바로 아랫줄에서 input과 unput으로 정적 변수를 선언하기 때문인데요. 굳이 #undef 디렉티브를 쓴 이유가 있는가 봅니다. 이름이 충돌하나.. 찾아 봤는데 충돌하는 이름은 없어 보입니다. 굳이 안써도 될 코드를 썼네요.

input과 unput이라는 이름으로 정적 변수를 선언합니다. 타입은 각각 Input 타입과 Unput 타입인데요. 얘네들은 opcode.h에 있습니다.

typedef void (*Cfunction) (void);
typedef int  (*Input) (void);

Input 타입은 int를 리턴하는 파라메터없는 함수 포인터입니다. 웃긴건 Unput 타입은 소스 코드에서 찾을 수 없습니다. 제 생각에는 소스 코드 1.1도 빌드 되는 코드를 올린 것이 아니라 그 시점의 스냅샷을 올린것 같습니다. 그래서 혹시나 하고 빌드 해 봤는데 빌드도 안되더라구요.. 그래서 심볼이 딱 안맞나 봅니다. 뭐 어떻습니까. 읽는데는 지장없습니다. 지금 강하게 의심하는 것은 1.1 소스 코드에 들어 있는 lex, yacc 파일이 1.0 버전용 아닌가? 하는 것입니다. 뭐 어느쪽이든 상관없습니다.

이어서 함수 세 개가 나옵니다. lua_setinput(), lua_setunput(), lua_lasttext() 입니다. 내용은 단순하네요. lua_setinput(), lua_setunput() 함수의 파라메터 이름이 fn인 이유는 앞에서 봤듯 Input 타입과 Unput 타입이 함수 포인터라 그렇습니다. lua_lasttext()는 마지막 토큰의 값을 리턴합니다. 토큰과 값은 다릅니다. 예를 들어 숫자 3이면 lexer는 NUMBER라는 토큰을 리턴하고 lua_lasttext() 함수는 3을 리턴합니다.

%%
[ \t]*					;
^"$debug"				{yylval.vInt = 1; return DEBUG;}
^"$nodebug"				{yylval.vInt = 0; return DEBUG;}
\n					lua_linenumber++;
"--".*					;
"local"					return LOCAL;
"if"					return IF;
"then"					return THEN;
"else"					return ELSE;
"elseif"				return ELSEIF;
"while"					return WHILE;
"do"					return DO;
"repeat"				return REPEAT;
"until"					return UNTIL;
"function"				{
                                         yylval.vWord = lua_nfile-1;
                                         return FUNCTION;
					}
"end"					return END;
"return" 				return RETURN;
"local" 				return LOCAL;
"nil"					return NIL;
"and"					return AND;
"or"					return OR;
"not"					return NOT;
"~="					return NE;
"<="					return LE;
">="					return GE;
".."					return CONC;
\"[^\"]*\" 			| 
\'[^\']*\'			      {
				       yylval.vWord = lua_findenclosedconstant (yytext);
				       return STRING;
				      }
[0-9]+("."[0-9]*)?	|
([0-9]+)?"."[0-9]+	|
[0-9]+("."[0-9]*)?[dDeEgG][+-]?[0-9]+ |
([0-9]+)?"."[0-9]+[dDeEgG][+-]?[0-9]+ {
				        yylval.vFloat = atof(yytext);
				        return NUMBER;
				       }
[a-zA-Z_][a-zA-Z0-9_]*  	       {
					yylval.vWord = lua_findsymbol (yytext);
					return NAME;
				       }
.					return  *yytext;

C 언어 코드 이후는 토큰 규칙 정의 부분입니다. 정규 표현식이죠. 쭉쭉 읽어 나가겠습니다. 공백이나 탭이 반복되는건 무시한다. $debug로 시작하는 줄이 나오면 yylval.vInt에 1 넣고 DEBUG 토큰 리턴. $nodebug로 시작하는 줄이면 yylval.vInt에 0 넣고 DEBUG 토큰 리턴. 줄바꿈이 나오면 lua_linenumber를 하나씩 증가. “–“가 나오고 나면 그 뒤에 문자는 무시. 이게 주석입니다. local, if, then, elseif, while, do, repeat, until 얘네들은 루아 예약어입니다. 그래서 각각 예약어에 할당한 토큰을 바로 리턴합니다. function도 예약어인데, FUNCTION 토큰을 리턴하기 전에 lua_nfile 이라는 변값에서 1을 빼서 값으로 yylval.vWord에 입력하네요. 다시 예약어가 쭉 나옵니다. end, return, local, nil, and, or, not 입니다. 그 다음에는 비교 연산자입니다. ~=가 토큰이 NE니까 이게 ‘같지 않다’입니다. 작거나 같다, 크거나 같다는 C언어랑 똑같이 <=와 >=입니다. “..”은 CONC가 토큰이름인데 미리 공부한 루아 문법에 따르면 점 두개 (..)는 루아에서 문자열 연결 연산자입니다. 아마도 concatenate의미로 CONC를 토큰 이름으로 정했나 봅니다. 그 뒤로 조금 복잡해 보이는 정규식이 따라 옵니다. 일단 여기까지 읽고 lua_linenumber와 lua_nfile 변수를 찾아 보겠습니다.

일단 Lua.lex 파일에 #include한 파일을 먼저 찾아보면 Inout.h 파일에 extern int lua_linenumber로 lua_linenumber가 익스턴 선언되 있습니다. 본체는 어디 있을까요. 아마 Inout.c에 있을 것 같습니다. 빙고! 거기 있네요. 그럼 어떻게 쓰이는지 찾아 볼까요.

int lua_openfile (char *fn)
{
 lua_linenumber = 1;
 lua_setinput (fileinput);
 fp = fopen (fn, "r");
 if (fp == NULL) return 1;
 if (lua_addfile (fn)) return 1;
 return 0;
}

함수 이름을 보면 루아 소스 파일을 인터프리터가 열 때 호출하는 함수 인것 같습니다. 아마 맞겠죠. 파일을 열 때 lua_linenumber를 1로 초기화합니다. 그리고 lexer에서 줄바꿈을 인식할 때마다 1씩 늘리는 거죠. 정말 말 그대로 줄번호를 저장하고 있는 변수네요.

lua_nfile 변수는 Table.h에 익스턴 선언되 있고 Table.c에 본체가 있습니다.

/*
** Add a file name at file table, checking overflow. This function also set
** the external variable "lua_filename" with the function filename set.
** Return 0 on success or 1 on error.
*/
int lua_addfile (char *fn)
{
 if (lua_nfile >= MAXFILE-1)
 {
  lua_error ("too many files");
  return 1;
 }
 if ((lua_file[lua_nfile++] = strdup (fn)) == NULL)
 {
  lua_error ("not enough memory");
  return 1;
 }
 return 0;
}

Table.c에 보면 lua_nfile 변수를 쓰는 코드가 쭉 있습니다. lua_addfile(), lua_delfile(), lua_filename() 입니다. 이중 중요해 보이는 함수는 lua_addfile() 파일입니다. 함수 이름만 보면 무슨 파일을 추가하는 것 같은데 아까 나온 openfile() 말고 뭘 또 하는건가…? 먼저 주석을 읽어 봅니다. 파일 이름을 파일 테이블에 추가하면서 오버 플로우를 체크. lua_filename이라는 익스터널 변수에 함수 파일 이름을 추가. 이게 주석 내용을 대충 해석한건데 주석이랑 코드랑 다르네요. 역시 십년 후에 전세계적으로 퍼지는 프로그램 소스 코드도 초기 버전엔 주석 관리가 안되는 군요. 익스터널 변수는 lua_filename가 아니라 lua_file같습니다. 아마 주석을 작성한 후에 변수 이름을 바꾼 것 같은데 주석은 같이 안바꿨나 보네요. 이런 실수 많이 하지요. lexer가 인식하는 토큰은 function이기 때문에 함수 정의와 연관있는 것 같은데 여전히 함수 이름과 변수 이름에 파일이 들어가니 헷갈리네요. lua_addfile() 함수가 어디서 사용되는지 찾아보겠습니다. 앗! 이런, 위에 인용한 lua_openfile()에서 쓰네요..:) 그럼 정말 파일 오픈하고 관련있는가 봅니다. 그런데 왜 FUNCTION 토큰에서 값을 넘기는지는 아직 모르겠습니다. 나중에 알게 되겠지요.

그럼 다시 이어서 쭉쭉 읽겠습니다. 따옴표가 복잡하게 나온 정규 표현식은 쉽게 말해 문자열입니다. 문자열을 lexer가 인식하면 lua_findenclosedconstant() 함수를 호출한다는데, 마찬가지로 lua_findenclosedconstant() 함수는 없네요. lex 파일이 1.1 용이 아닐거라는 의심이 더욱 강해집니다. 일단 그냥 넘어가겠습니다. 지금 모르면 다음 릴리즈 소스 코드 볼 때 알겠지요.

그 다음에 나오는 0, 9가 복잡하게 나오는 정규 표현식은 숫자 표시 입니다. 표준 함수 atof()를 호출해서 모든 숫자를 double로 처리합니다. 그래서 루아는 숫자 타입이 double 하나 뿐입니다. 최신 버전에서 int를 추가했다가 메인 개발자가 개발 포기 선언했느니 어쩌느니 했단 글을 읽은 기억이 납니다. 토큰은 NUMBER입니다.

그 아래 a-z, A-Z 이렇게 나온건 뭔지 아시겠죠? 심볼 정의 입니다. 프로그래밍 언어론에서는 identifier라고도 하지요. lua_findsymbol() 함수를 호출해서 리턴값을 yylval.vWord에 넣습니다. 나중에 자세히 읽고 지금은 lua_findsymbol() 함수를 빠르게 훑어 보기만 합니다. Table.c 파일에 있습니다. 쭉 보니 심볼 테이블에서 심볼을 찾아보고 심볼 인덱스를 리턴하는 것 같습니다. 토큰은 NAME입니다.

이렇게 Lua.lex 파일을 다 읽었습니다. 다행히 하루에 파일 하나를 다 읽었습니다. 다른 파일도 하루에 다 읽을 수 있다면 약 한 달이면 루아 1.1 소스 코드를 다 읽을 수 있겠네요. 그랬으면 좋겠습니다.

댓글남기기