[루아2.1] Lua.stx(1)

10 분 소요

Lua.stx (1)

낱말 분석(lexical analysis)하는 Lex.c 파일을 읽었으니, 전편의 순서를 따라 다음은 구문 분석(syntax parsing)하는 yacc 입력 파일인 Lua.stx 차례입니다. 루아 1.1에서 2.1로 넘어가면서 lex의 입력 파일은 아예 소스 코드에서도 빠졌지만 아직 yacc은 사용하고 있습니다.

#define malloc luaI_malloc
#define realloc luaI_realloc
#define free luaI_free

코드 시작 부분에 표준 라이브러리 함수인 malloc(), realloc(), free()를 luaI_malloc(), luaI_realloc(), luaI_free() 함수로 바꾸는 #define 구문이 나옵니다. 표준 라이브러리 함수를 쓰지않고 malloc() 함수 호출을 luaI_malloc()로 대체한다는 뜻입니다. 동적 메모리 관리를 직접하겠다는 것이죠. 그러면 그냥 malloc() 함수가 필요한 위치에 luaI_malloc() 함수를 호출하지 저렇게 표준 함수 이름을 재정의 하진 않거든요. 보통 저런 재정의 코드를 쓰는 경우는 멀티 플랫폼을 지원하기 위해 특정 플랫폼에서 표준 라이브러리를 쓸 수 없을 때입니다. 그런데 지금 루아 2.1은 이 상황은 아니거든요. 왜 저 코드를 작성했는지 궁금했습니다. Lua.stx 파일에서 malloc으로 검색했으나 malloc() 함수를 사용하는 코드는 없었습니다. 그러면 쓰지도 않는 코드를 써 놓은 건가? 그러다가 Lua.stx 파일은 yacc에 입력으로 들어가서 yacc이 C 소스 코드를 만든다는 것이 생각났습니다. 그 파일이 루아 2.1에서는 Parser.c 파일과 Parser.h 파일입니다. 이제서야 이유를 알았습니다.

yacc이 생성하는 Parser.c 파일의 내용을 루아 개발자들이 100% 통제할 순 없습니다. 그런데 루아 2.1로 오면서 루아 내부 구현에서는 메모리 관리를 표준 라이브러리를 쓰는 것이 아니라 자체적으로 만든 함수로 하도록 디자인했습니다. 그래서 yacc 생성하는 Parser.c 파일의 코드도 메모리를 할당할 때 표준 라이브러리를 쓰지 않도록 저렇게 #define으로 동적 메모리 관련 관리 함수를 재정의한 것입니다.

#ifndef LISTING
#define LISTING 0
#endif

#ifndef CODE_BLOCK
#define CODE_BLOCK 256
#endif
static int   maxcode;
static int   maxmain;
static Long   maxcurr;  /* to allow maxcurr *= 2 without overflow */
static Byte  *funcCode = NULL;
static Byte **initcode;
static Byte  *basepc;
static int   maincode;
static int   pc;

LISTING은 구문 분석 완료 후에 결과물로 나온 바이트 코드를 출력할지 설정하는 플래그입니다. 루아 개발 하면서 디버그 용도로 쓰는 플래그로 추정됩니다. CODE_BLOCK은 바이트 코드 블록을 생성할 때 한 번에 생성하는 크기 단위로 보입니다. maxcode, maxmain, maxcurr는 바이트 코드 버퍼의 최대 크기를 체크하는 용도로 사용하는 것으로 보입니다. 루아 1.1을 읽을 때는 이 세 변수를 어떻게 쓰는 것인지 명확하게 이해하지 못했는데 이번에는 이해할 수 있길 바랍니다. funcCode는 함수를 선언했을 때 함수의 바이트 코드가 저장될 메모리 영역을 할당받고 그 시작 주소 포인터를 저장하는데 사용합니다. initcode는 이름에 “init”라는 단어가 들어가 있으니 분명 초기에 동작하는 무엇과 관련이 있을겁니다. 아니면 그냥 단순히 바이트 코드 시작 위치를 가리키고 있는 포인터 변수일 수도 있습니다. basepc와 maincode, pc는 현재 기록 중인 바이트 코드 버퍼 위치에 관련된 변수입니다.

여담으로 제가 루아 1.1을 읽을 때는 구문 분석 결과로 생성하는 바이트 코드가 저장되는 메모리 영역을 바이트 코드 스택이라고 불렀는데요. 생각해보니 스택이라고 부르면 안될 것 같습니다. 바이트 코드가 쌓이는 과정이 스택처럼 차곡차곡 쌓이길래 스택인줄 알았더니 값을 읽을 때는 제어문이나 루프문에 따라서 왔다 갔다 하며 읽었습니다. 스택보다는 인스트럭션 메모리에 더 가까운 동작입니다. 그래서 이제부터 바이트 코드 버퍼라고 고쳐서 부르고 있습니다. 지난 문서를 고치면 되겠지만 그냥 두는 것 자체가 공부 과정의 기록이므로 기록으로서 가치를 더 중히 여겨 고치지 않기로 결정했습니다.

#define MAXVAR 32
static Long    varbuffer[MAXVAR];    /* variables in an assignment list;
				it's long to store negative Word values */
static int     nvarbuffer=0;	     /* number of variables at a list */

static Word    localvar[STACKGAP];   /* store local variable names */
static int     nlocalvar=0;	     /* number of local variables */

#define MAXFIELDS FIELDS_PER_FLUSH*2
static Word    fields[MAXFIELDS];     /* fieldnames to be flushed */
static int     nfields=0;

위에서부터 순서대로 전역 변수 심볼 테이블 인덱스 버퍼(varbuffer), 로컬 변수 심볼 테이블 인덱스 버퍼(localvar), 필드 이름 테이블 인덱스 버퍼(fields)입니다.

static void code_byte (Byte c)
{
 if (pc>maxcurr-2)  /* 1 byte free to code HALT of main code */
 {
  if (maxcurr >= MAX_INT)
    lua_error("code size overflow");
  maxcurr *= 2;
  if (maxcurr >= MAX_INT)
    maxcurr = MAX_INT;
  basepc = growvector(basepc, maxcurr, Byte);
 }
 basepc[pc++] = c;
}

code_byte() 함수는 바이트 코드 버퍼에 바이트 코드 혹은 바이트 코드의 파라메터 데이터를 넣는 함수입니다. 구문 분석하면서 파서가 가장 많이 호출하는 함수입니다. maxcurr 변수가 바이트 코드 버퍼의 최대값과 관계있을 거라 의심하는 이유가 이 함수에 있습니다. pc가 maxcurr보다 클 때 growvector() 함수를 호출해서 바이트 코드 버퍼 크기를 늘립니다. basepc는 바이트 코드 버퍼의 시작 주소 포인터로 보입니다.

static void code_word (Word n)
{
 CodeWord code;
 code.w = n;
 code_byte(code.m.c1);
 code_byte(code.m.c2);
}

static void code_float (float n)
{
 CodeFloat code;
 code.f = n;
 code_byte(code.m.c1);
 code_byte(code.m.c2);
 code_byte(code.m.c3);
 code_byte(code.m.c4);
}

static void code_code (Byte *b)
{
 CodeCode code;
 code.b = b;
 code_byte(code.m.c1);
 code_byte(code.m.c2);
 code_byte(code.m.c3);
 code_byte(code.m.c4);
}

바이트 코드 버퍼에 각각 word, float, code 타입 데이터를 넣는 함수입니다. 각각 자료형의 크기만 다를 뿐 code_byte() 함수를 자료형 크기에 맞춰 여러번 호출한다는 것 외엔 같은 패턴으로 구현한 함수입니다.

static void code_word_at (Byte *p, Word n)
{
 CodeWord code;
 code.w = n;
 *p++ = code.m.c1;
 *p++ = code.m.c2;
}

바이트 코드 버퍼 특정 위치에 word 타입 데이터를 덮어 쓰는 함수입니다. if-else 문법을 파싱할 때 이 함수를 호출합니다. 덮어 쓴다는 표현은 기존 데이터를 지우고 그 위치에 새 데이터를 쓴다는 건조한 표현입니다. 그래서 기존에 의미있는 바이트 코드를 덮어 쓴다고 해석할 수도 있고 비어있는 위치에 바이트 코드를 미리 써 둘 수도 있습니다. 과연 어느쪽일까요. 나중에 보면 알겠죠.

static void push_field (Word name)
{
  if (nfields < STACKGAP-1)
    fields[nfields++] = name;
  else
   lua_error ("too many fields in a constructor");
}

루아의 테이블을 선언할 때, 이름있는 아이템을 선언하면 이름은 루아 구현 내부에 fields 배열 변수에 저장됩니다. 저장되어 있다가 flush_record() 함수가 호출되면 하나씩 삭제됩니다.

static void flush_record (int n)
{
  int i;
  if (n == 0) return;
  code_byte(STORERECORD);
  code_byte(n);
  for (i=0; i<n; i++)
    code_word(fields[--nfields]);
}

필드 이름은 field 배열 변수에 저장되어 있다가 flush_record() 함수가 호출되면 바이트 코드 버퍼에 들어가면서 field 배열 변수에서 삭제됩니다. 구조를 보면 아마 필드 이름을 바이트 코드 버퍼에 넣는 작업을 문법적으로 한 번에 처리할 수가 없어서 field 배열 변수를 임시 영역으로 사용하는 것같습니다. 실제 코드를 봐도 push_field() 함수는 ffield 문법에서 호출합니다. 그리고 fieldlist 문법에서 호출합니다. 그래서 제 짐작이 맞다면 ffield 문법에서 필드 이름을 파싱해서 field 배열 변수에 넣고 fieldlist 문법에서 field 배열 변수의 값을 꺼내서 바이트 코드 버퍼에 넝는 것입니다.

static void flush_list (int m, int n)
{
  if (n == 0) return;
  if (m == 0)
    code_byte(STORELIST0); 
  else
  if (m < 255)
  {
    code_byte(STORELIST);
    code_byte(m);
  }
  else
   lua_error ("list constructor too long");
  code_byte(n);
}

바이트 코드 버퍼에 넣는 명령어 이름을 봐도 flush_list() 함수는 리스트 자료형의 선언과 관련된 구문 분석을 처리하는 함수로 보입니다. 파라메터가 m과 n 두 개인 이유는 단순히 속도를 빠르게 하기 위함으로 보입니다. 잠시후에 문법 읽을 때 나올 것입니다만, 미리 잠깐 언급하면, 루아는 리스트 자료형을 선언해서 바이트 코드 버퍼에 파라메터를 넣을 때 리스트 크기 40이 기본 단위입니다. 그래서 만약 리스트 크기를 40 이하로 선언하면 m은 항상 1이고 n은 항상 0입니다. 만약 리스트 크기를 83으로 선언했다면 m은 2이고 n은 3입니다.

static void add_nlocalvar (int n)
{
 if (MAX_TEMPS+nlocalvar+MAXVAR+n < STACKGAP)
  nlocalvar += n;
 else
  lua_error ("too many local variables");
}

static void incr_nvarbuffer (void)
{
 if (nvarbuffer < MAXVAR-1)
  nvarbuffer++;
 else
  lua_error ("variable buffer overflow");
}

로컬 변수를 선언하면 localvar 배열 변수에 로컬 변수 심볼 테이블 인덱스 값을 저장합니다. 저장하는 작업은 함수를 따로 호출하지 않고 yacc 문법 코드에서 바로 합니다. 그런데 nlocalvar를 하나 증가하는 작업은 add_nlocalvar() 함수를 호출해서 합니다. 아마 에러 체크를 하려고 함수 호출로 처리하는 것으로 보입니다. 전역 변수에 대해서도 마찬가지 절차로 작업합니다.

static void code_number (float f)
{ 
  Word i = (Word)f;
  if (f == (float)i)  /* f has an (short) integer value */
  {
   if (i <= 2) code_byte(PUSH0 + i);
   else if (i <= 255)
   {
    code_byte(PUSHBYTE);
    code_byte(i);
   }
   else
   {
    code_byte(PUSHWORD);
    code_word(i);
   }
  }
  else
  {
   code_byte(PUSHFLOAT);
   code_float(f);
  }
}

바이트 코드 버퍼에 숫자를 넣는 함수입니다. 일단 2바이트 word 타입 변수로 캐스팅해보고 1바이트일 때, 2바이트일 때, 4바이트일 때에 맞춰 각각 다른 명령어를 바이트 코드 버퍼에 넣고 값을 넣습니다.

/*
** Search a local name and if find return its index. If do not find return -1
*/
static int lua_localname (Word n)
{
 int i;
 for (i=nlocalvar-1; i >= 0; i--)
  if (n == localvar[i]) return i;	/* local var */
 return -1;		        /* global var */
}

루아 2.1로 버전이 올라가면서 로컬 변수를 관리하는 기능이 강화되었습니다. lua_localname는 심볼 테이블 인덱스를 파라메터로 받아서 그 심볼이 로컬 변수인지 전역 변수인지 판단해서 리턴합니다. 전역 변수면 -1을 리턴하고 로컬 변수면 localvar 배열 변수의 인덱스를 리턴합니다.

/*
** Push a variable given a number. If number is positive, push global variable
** indexed by (number -1). If negative, push local indexed by ABS(number)-1.
** Otherwise, if zero, push indexed variable (record).
*/
static void lua_pushvar (Long number)
{ 
 if (number > 0)	/* global var */
 {
  code_byte(PUSHGLOBAL);
  code_word(number-1);
 }
 else if (number < 0)	/* local var */
 {
  number = (-number) - 1;
  if (number < 10) code_byte(PUSHLOCAL0 + number);
  else
  {
   code_byte(PUSHLOCAL);
   code_byte(number);
  }
 }
 else
 {
  code_byte(PUSHINDEXED);
 }
}

변수 정보를 바이트 코드 버퍼에 넣는 함수입니다. 로컬 변수일 때는 PUSHLOCAL 명령어와 변수 개수를 바이트 코드 버퍼에 넣습니다. 전역 변수일 때는 PUSHGLOBAL 명령어와 변수 개수를 바이트 코드 버퍼에 넣습니다.

static void lua_codeadjust (int n)
{
 if (n+nlocalvar == 0)
   code_byte(ADJUST0);
 else
 {
   code_byte(ADJUST);
   code_byte(n+nlocalvar);
 }
}

VM에서 스택 포인터 위치를 조정하는 ADJUST 명령어를 바이트 코드 버퍼에 넣는 함수입니다.

static void init_function (TreeNode *func)
{
 if (funcCode == NULL)	/* first function */
 {
  funcCode = newvector(CODE_BLOCK, Byte);
  maxcode = CODE_BLOCK;
 }
 pc=0; basepc=funcCode; maxcurr=maxcode; 
 nlocalvar=0;
  if (lua_debug)
  {
    code_byte(SETFUNCTION); 
    code_code((Byte *)luaI_strdup(lua_file[lua_nfile-1]));
    code_word(luaI_findconstant(func));
  }
}

init_function() 함수 이름만 보고는 초기화 함수에 대한 코드인가?하는 생각이 들었습니다. C 언어의 main() 함수같은 엔트리 포인트 처리 같은 것으로 생각했죠. 그런데 실제 init_function() 함수가 사용되는 문법은 함수 선언 문법입니다. 그래서 이 함수 이름은 함수를 초기화하는 함수로 해석해야 맞습니다. 루아는 함수의 바이트 코드 버퍼를 따로 할당받습니다. 그래서 funcCode 포인터로 함수의 바이트 코드 위치를 따로 관리합니다. 그래서 함수 선언을 할 때마다 바이트 코드 버퍼를 관리하는 pc, basepc, maxcurr 변수값을 함수 전용 바이트 코드 영역으로 재설정합니다.

static void codereturn (void)
{
  if (lua_debug) code_byte(RESET); 
  if (nlocalvar == 0)
    code_byte(RETCODE0);
  else
  {
    code_byte(RETCODE);
    code_byte(nlocalvar);
  }
}

함수 리턴을 할 때, RETCODE 명령어와 함수 안에서 선언한 로컬 변수 개수를 바이트 코드 버퍼에 넣는 함수입니다. 당연히 문법에서 호출 위치도 함수를 끝낼 때입니다.

static void codedebugline (void)
{
  if (lua_debug)
  {
    code_byte(SETLINE);
    code_word(lua_linenumber);
  }
}

루아 디버깅 정보를 출력하기 위해 바이트 코드 버퍼에 루아 코드의 라인 번호 정보를 넣는 함수입니다.

static void adjust_mult_assign (int vars, int exps, int temps)
{
  if (exps < 0) 
  {
    int r = vars - (-exps-1);
    if (r >= 0)
      code_byte(r);
    else
    {
      code_byte(0);
      lua_codeadjust(temps);
    }
  }
  else if (vars != exps)
    lua_codeadjust(temps);
}

lua_codeadjust() 함수를 호출하는 것을 보면 변수 선언과 관계 있는 것같은데 아직 용도 파악이 안되었습니다. 나중에 다시 보겠습니다.

static void lua_codestore (int i)
{
 if (varbuffer[i] > 0)		/* global var */
 {
  code_byte(STOREGLOBAL);
  code_word(varbuffer[i]-1);
 }
 else if (varbuffer[i] < 0)      /* local var */
 {
  int number = (-varbuffer[i]) - 1;
  if (number < 10) code_byte(STORELOCAL0 + number);
  else
  {
   code_byte(STORELOCAL);
   code_byte(number);
  }
 }
 else				  /* indexed var */
 {
  int j;
  int upper=0;     	/* number of indexed variables upper */
  int param;		/* number of itens until indexed expression */
  for (j=i+1; j <nvarbuffer; j++)
   if (varbuffer[j] == 0) upper++;
  param = upper*2 + i;
  if (param == 0)
   code_byte(STOREINDEXED0);
  else
  {
   code_byte(STOREINDEXED);
   code_byte(param);
  }
 }

루아 1.1 읽기 글에서 lua_codestore() 함수 구현 절반이 lua_pushvar()와 같다고 썻던것 같은데 큰 실수를 했군요. 다른 구현입니다. lua_codestore() 함수는 바이트 코드 버퍼에 STORE 계열 명령어를 넣습니다. 완전히 다르죠. 제가 조건문과 코드 패턴만 보고 너무 빨리 넘어갔나 봅니다. 눈으로 읽기만 하는 것의 약점이죠. 그래도 여러번 읽으니 지난번의 문제점도 파악해 낼 수 있어 약점은 어느정도 보완할 것이라고 생각합니다.

static void codeIf (Long thenAdd, Long elseAdd)
{
  Long elseinit = elseAdd+sizeof(Word)+1;
  if (pc == elseinit)		/* no else */
  {
    pc -= sizeof(Word)+1;
    elseinit = pc;
  }
  else
  {
    basepc[elseAdd] = JMP;
    code_word_at(basepc+elseAdd+1, pc-elseinit);
  }
  basepc[thenAdd] = IFFJMP;
  code_word_at(basepc+thenAdd+1,elseinit-(thenAdd+sizeof(Word)+1));
}

if-else if-else 문법을 구문 분석할 때 호출하는 함수입니다. 조건문의 true/false 여부에 따라 점프해야할 바이트 코드 버퍼 주소가 다르므로 해당 버퍼 주소를 계산합니다.

static void yyerror (char *s)
{
 static char msg[256];
 sprintf (msg,"%s near \"%s\" at line %d in file \"%s\"",
          s, lua_lasttext (), lua_linenumber, lua_filename());
 lua_error (msg);
}


/*
** Parse LUA code.
*/
void lua_parse (Byte **code)
{
 initcode = code;
 *initcode = newvector(CODE_BLOCK, Byte);
 maincode = 0; 
 maxmain = CODE_BLOCK;
 if (yyparse ()) lua_error("parse error");
 (*initcode)[maincode++] = RETCODE0;
#if LISTING
{ static void PrintCode (Byte *c, Byte *end);
 PrintCode(*initcode,*initcode+maincode); }
#endif
}

yyerror() 함수는 yacc 에러 처리를 대행하는 함수입니다. lua_parse() 함수는 Lua.stx 파일의 엔트리 포인트 역할을 하는 함수입니다.

여기까지 읽어서 Lua.stx 파일을 반 조금 안되게 읽었습니다. 루아 1.1 코드를 읽을 때는 Lua.stx 파일만 다섯 번에 나눠서 읽었는 이번에는 두 번째 읽는 것이라 그런지 그 보단 짧을 것 같은 예감입니다.

댓글남기기