[루아2.1] Lex.c

6 분 소요

Lua 2.1

루아 릴리즈를 보면 루아 1.1 다음에 바로 루아 2.1입니다. 루아 개발자들이 저처럼 그냥 아무 이유없이 메이저 버전을 올린 것이 아니라면 변경점이 많기 때문에 메이저 버전이 올라간 것이겠죠. 게다가 루아 2.1이 발표된 90년대 중반(정확히는 95년이군요)에는 버전 넘버링을 지금보다 더 엄밀한 규칙으로 하는 프로젝트가 많았습니다.

루아 2.1 소스 코드에 있는 README 파일에서 변경 사항을 기록한 부분을 가져오겠습니다.

* Changes since version 1.1 (current version is 2.1)
  + object-oriented support;
  + fallbacks;
  + simplified syntax for tables;
  + many internal improvements.

객체 지향 개념을 추가했다고 하네요. 이거 때문에 메이저 버전을 올렸는지도 모릅니다. fallbacks는 뭔지 모르겠습니다. 루아 테이블 자료 구조를 사용하는 문법을 단순하게 바꿨다고 하네요. 그리고 내부적으로 뭘 많이 개선했다고 합니다. 메이저 버전이 올라간 만큼, 변경 지점만 찾아서 읽는 것이 아니라 루아 1.1을 읽을 때처럼 그냥 전체 코드를 다시 쭉 읽어 보겠습니다.

쓰고나니 규칙을 정할 수 있겠네요. 마이너 버전이 올라갔을 때는 변경점 중심으로 읽고 메이저 버전이 올라갔을 때는 전체 코드를 읽는 식으로 진행하는 것도 좋아보입니다. 그럼 바로 코드를 읽겠습니다.

Lex.c

#define lua_strcmp(a,b)	(a[0]<b[0]?(-1):(a[0]>b[0]?(1):strcmp(a,b)))

#define next() { current = input(); }
#define save(x) { *yytextLast++ = (x); }
#define save_and_next()  { save(current); next(); }

루아 개발자들은 문자열 비교 함수인 strcmp()를 그대로 쓰는걸 참 싫어하는 것 같습니다. 매번 딱히 이득도 없어 보이는데 매번 재구현하네요. next(), save(), save_and_next()는 입력 문자를 하나 받고, 결과를 yytextLast에 저장하는 함수입니다.

static int current;
static char yytext[256];
static char *yytextLast;

static Input input;

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

char *lua_lasttext (void)
{
  *yytextLast = 0;
  return yytext;
}

current 변수는 현재 낱말 분석 대상이되는 글자가 저장되는 변수입니다. yytext는 낱말 분석이 완료된 토큰의 값에 해당하는 문자열이 저장되는 문자열 변수입니다. yytextLast는 yytext에 글자를 한 글자씩 쉽게 추가하려고 만든 포인터입니다. lua_setinput() 함수는 루아 코드 원본이 파일이냐, 문자열이냐에 따라 다르게 동작하는 input 함수 포인터를 설정하는 함수입니다. lua_lasttext() 함수는 yytext를 리턴합니다.

/* The reserved words must be listed in lexicographic order */
static struct
  {
    char *name;
    int token;
  } reserved [] = {
      {"and", AND},
      {"do", DO},
      {"else", ELSE},
      {"elseif", ELSEIF},
      {"end", END},
      {"function", FUNCTION},
      {"if", IF},
      {"local", LOCAL},
      {"nil", NIL},
      {"not", NOT},
      {"or", OR},
      {"repeat", REPEAT},
      {"return", RETURN},
      {"then", THEN},
      {"until", UNTIL},
      {"while", WHILE} };


#define RESERVEDSIZE (sizeof(reserved)/sizeof(reserved[0]))


static int findReserved (char *name)
{
  int l = 0;
  int h = RESERVEDSIZE - 1;
  while (l <= h)
  {
    int m = (l+h)/2;
    int comp = lua_strcmp(name, reserved[m].name);
    if (comp < 0)
      h = m-1;
    else if (comp == 0)
      return reserved[m].token;
    else
      l = m+1;
  }
  return 0;
}

reserved 배열에 예약어와 토큰을 미리 설정해 둡니다. 낱말 분석에 예약어는 분리해야 하니까요. findReserved() 함수는 예약어를 검색합니다. 이제보니 바이너리 서치네요. 루아 코드를 낱말 분석할 때 알파벳이 나오면 일단 findReserved() 함수는 호출하고 보니, 바이너리 서치로 만들어놔야 속도에서 이득을 볼 수 있을겁니다.

이제 yylex() 함수를 조각조각 잘라서 읽어보겠습니다.

int yylex (void)
{
  float a;
  while (1)
  {
    yytextLast = yytext;
#if 0
    fprintf(stderr,"'%c' %d\n",current,current);
#endif
    switch (current)
    {
      case EOF:
      case 0:
       return 0;
      case '\n': lua_linenumber++;
      case ' ':
      case '\t':
        next();
        continue;

시작 부분입니다. while 루프를 돌면서 한 바퀴 돌 때마다 yytextLast 포인터를 yytext 시작 위치로 돌립니다. 그래서 while 루프 한 번 돌 때마다 토큰을 하나씩 분리하는 것입니다. 파일 끝이거나 문자열 끝이면 낱말 분석을 종료합니다. 개행이 나오면 lua_linenumber 변수값을 하나씩 올립니다. 공백이나 탭이면 다음 토큰을 찾으러 낱말 분석을 다시합니다.

      case '$':
	next();
	while (isalnum(current) || current == '_')
          save_and_next();
        *yytextLast = 0;
	if (lua_strcmp(yytext, "debug") == 0)
	{
	  yylval.vInt = 1;
	  return DEBUG;
        }
	else if (lua_strcmp(yytext, "nodebug") == 0)
	{
	  yylval.vInt = 0;
	  return DEBUG;
        }
	return WRONGTOKEN;

루아 코드에서 디버그 플래그를 켜는데 쓰는 토큰입니다. 딱히 중요하지 않으니 패쓰~.

      case '-':
        save_and_next();
        if (current != '-') return '-';
        do { next(); } while (current != '\n' && current != 0);
        continue;

입력 문자열이 ‘-‘로 시작하면 주석이거나 마이너스 둘 중 하나입니다. 그래서 주석이 아니면 그냥 마이너스 문자 리턴, 주석이면 개행이나 문자열이 끝날 때까지 분석을 안하고 넘어갑니다.

      case '=':
        save_and_next();
        if (current != '=') return '=';
        else { save_and_next(); return EQ; }

      case '<':
        save_and_next();
        if (current != '=') return '<';
        else { save_and_next(); return LE; }

      case '>':
        save_and_next();
        if (current != '=') return '>';
        else { save_and_next(); return GE; }

      case '~':
        save_and_next();
        if (current != '=') return '~';
        else { save_and_next(); return NE; }

입력 문자열이 ‘=’로 시작하면 비교 연산자 ‘같다’ (‘==’) 혹은 대입 연산자입니다. 그래서 ‘같다’ 토큰이면 EQ를 리턴하고 아니면 ‘=’ 문자를 그대로 리턴합니다. 입력 문자열이 ‘<’로 시작하면 ‘작거나 같다’ (‘<=’) 혹은 ‘작다’ (‘<’)입니다. 그래서 다음 글자가 ‘=’이 아니면 ‘<’문자를 그대로 리턴하고 맞으면 LE 토큰을 리턴합니다. GE 토큰 관련 처리도 같은 패턴입니다. 루아는 ‘같지 않다’ 연산자가 ‘~=’입니다. 그걸 처리하는 낱말 분석 코드가 이어서 나옵니다.

      case '"':
      case '\'':
      {
        int del = current;
        next();  /* skip the delimiter */
        while (current != del)
        {
          switch (current)
          {
            case EOF:
            case 0:
            case '\n':
              return WRONGTOKEN;
            case '\\':
              next();  /* do not save the '\' */
              switch (current)
              {
                case 'n': save('\n'); next(); break;
                case 't': save('\t'); next(); break;
                case 'r': save('\r'); next(); break;
                default : save(current); next(); break;
              }
              break;
            default:
              save_and_next();
          }
        }
        next();  /* skip the delimiter */
        *yytextLast = 0;
        yylval.vWord = luaI_findconstant(lua_constcreate(yytext));
        return STRING;
      }

문자열 토큰을 분석하는 코드입니다. 겹따옴표 혹은 홑따옴표로 시작하고 끝나는 문자열이 모두 yytext에 저장됩니다. 중간에 null이나 eof 혹은 개행이 있으면 WRONGTOKEN입니다. 자체적으로 이스케이프처리도 합니다. 개행, 탭 등은 별도로 처리하고요.

      case 'a': case 'b': case 'c': case 'd': case 'e':
      case 'f': case 'g': case 'h': case 'i': case 'j':
      case 'k': case 'l': case 'm': case 'n': case 'o':
      case 'p': case 'q': case 'r': case 's': case 't':
      case 'u': case 'v': case 'w': case 'x': case 'y':
      case 'z':
      case 'A': case 'B': case 'C': case 'D': case 'E':
      case 'F': case 'G': case 'H': case 'I': case 'J':
      case 'K': case 'L': case 'M': case 'N': case 'O':
      case 'P': case 'Q': case 'R': case 'S': case 'T':
      case 'U': case 'V': case 'W': case 'X': case 'Y':
      case 'Z':
      case '_':
      {
        Word res;
        do { save_and_next(); } while (isalnum(current) || current == '_');
        *yytextLast = 0;
        res = findReserved(yytext);
        if (res) return res;
        yylval.pNode = lua_constcreate(yytext);
        return NAME;
      }

그냥 알파벳이 나오면 일단 예약어인지 검사해보고 예약어면 예약어 토큰을 리턴합니다. 예약어가 아니면 yytext를 루아 구현 내부 어딘가에 저장하고 포인터를 넘겨받아 yylval에 저장합니다. lua_constcreate() 함수는 루아 2.1에서 새로 생긴 함수입니다. 나중에 Tree.c 코드 읽을 때 등장할 것입니다.

      case '.':
        save_and_next();
        if (current == '.')
        {
          save_and_next();
          return CONC;
        }
        else if (!isdigit(current)) return '.';
        /* current is a digit: goes through to number */
	a=0.0;
        goto fraction;

      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
	a=0.0;
        do { a=10*a+current-'0'; save_and_next(); } while (isdigit(current));
        if (current == '.') save_and_next();
fraction:
	{ float da=0.1;
	  while (isdigit(current))
	  {a+=(current-'0')*da; da/=10.0; save_and_next()};
          if (current == 'e' || current == 'E')
          {
	    int e=0;
	    int neg;
	    float ea;
            save_and_next();
	    neg=(current=='-');
            if (current == '+' || current == '-') save_and_next();
            if (!isdigit(current)) return WRONGTOKEN;
            do { e=10*e+current-'0'; save_and_next(); } while (isdigit(current));
	    for (ea=neg?0.1:10.0; e>0; e>>=1) 
	    {
	      if (e & 1) a*=ea;
	      ea*=ea;
	    }
          }
          yylval.vFloat = a;
          return NUMBER;
        }

입력 문자열이 점(‘.’)으로 시작하면 문자열 연결 연산자(‘..’)이거나 그냥 점 자체 혹은 앞에 0이 없는 소숫점 숫자(.323)입니다. 저는 명시적으로 앞에 0을 꼭 쓰는 것을 선호하지만 루아는 0을 안 써도 소수점 입력을 허용합니다. 루아 1.1은 atof() 함수로 문자열로 입력 받은 숫자를 float 타입 값으로 변환했는데, 루아 2.1은 변수 a를 이용해서 계산해서 float 타입 값으로 변환합니다. 이 방식이 더 빠른가봅니다.

      case U_and: case U_do: case U_else: case U_elseif: case U_end:
      case U_function: case U_if: case U_local: case U_nil: case U_not:
      case U_or: case U_repeat: case U_return: case U_then:
      case U_until: case U_while:
      {
        int old = current;
        next();
        return reserved[old-U_and].token;
      }

      case U_eq:	next(); return EQ;
      case U_le:	next(); return LE;
      case U_ge:	next(); return GE;
      case U_ne:	next(); return NE;
      case U_sc:	next(); return CONC;

      default: 		/* also end of file */
      {
        save_and_next();
        return yytext[0];
      }
    }
  }
}

위 코드가 왜 있는지 모르겠군요. 왜냐하면 위 코드는 앞의 case 구문에서 다 걸러내고 제대로 동작한다면 이 코드는 동작하지 않는 코드거든요. 속도상에 이득이라든가 이런걸 노린 것이라면 위 코드에 해당하는 case 구문이 맨 위로 올라가야 합니다.

댓글남기기