[루아1.1] lua.stx 읽기 (2)

4 분 소요

Lua.stx 파일은 내용이 깁니다. 아마 이 파일 다 읽으면 루아 1.1 소스 코드는 거의 다 읽은 것이나 다름 없지 않을까 합니다. 전 글에서 정적 전역 변수 선언 부분 코드를 읽었습니다. 이번 글에서는 그 아래에 나오는 인터널 함수 구현 코드를 읽겠습니다.

함수가 총 몇개냐면,

  • code_byte()
  • code_word()
  • code_float()
  • code_word_at()
  • push_field()
  • flush_record()
  • flush_list()
  • incr_ntemp()
  • add_nlocalvar()
  • incr_nvarbuffer
  • code_number()

11개 입니다. 별로 많지 않으니 하나씩 읽어 보겠습니다. 분석을 해야 한다면 각 함수를 볼 때마다 함수가 어디에서 어떻게 호출되는지 추적해야 하지만 저는 그냥 읽고 있으므로 일단은 먼저 끝까지 읽는데만 집중하겠습니다.

static void code_byte (Byte c)
{
 if (pc>maxcurr-2)  /* 1 byte free to code HALT of main code */
 {
  maxcurr += GAPCODE;
  basepc = (Byte *)realloc(basepc, maxcurr*sizeof(Byte));
  if (basepc == NULL)
  {
   lua_error ("not enough memory");
   err = 1;
  }
 }
 basepc[pc++] = c;
}

maxcurr는 바이트 코드 스택 최대값입니다. 이 최대값에서 두 개 전 위치까지 pc가 증가하면 maxcurr를 GAPCODE만큼 증가합니다. GAPCODE는 50인데요. 왜 50인지 알 수는 없으나, 느낌에 그냥 50으로 정한 것 같습니다. 한 번에 50씩 바이트 코드 스택를 증가하는 거지요. 그런 다음 basepc라는 이름에 바이트 코드 스택에 바이트 코드를 저장합니다. 마지막 바이트 코드 위치는 pc 변수로 기록해 둡니다.

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

Word 타입은 2바이트입니다. CodeWord라는 새로운 타입이 등장했습니다. Opcode.h에 정의가 있습니다.

typedef union
{
 struct {char c1; char c2;} m;
 Word w;
} CodeWord;

별건 없습니다. 그냥 2바이트 워드를 1바이트씩 끊어서 접근할 수 있도록 공용체 하나 만들어 둔 것입니다. 그래서 code_word() 함수 코드를 보면 파라메터로 넘어온 Word 타입 값을 CodeWord 타입 공용체에 복사한 다음 바이트로 나눠서 code_byte() 함수를 두번 호출합니다.

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);
}

float은 4바이트입니다. CodeFloat은 안봐도 어떻게 생긴 타입인지 알것 같습니다. 코드 패턴은 code_word()와 동일하니 빨리 넘어 가죠.

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

code_word_at() 함수는 p 포인터 위치에 Word 타입 값을 복사하는 함수입니다. 삽입(insert) 동작을 하는 것이 아니라 그냥 p 위치에 값을 덮어쓰기(overwrite)하는 군요.

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

이건 전편 글에서 봤던 함수 입니다.

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]);
  ntemp -= n;
}

레코드가 뭔지 모르겠네요. 아무튼 함수 구현만 그대로 읽으면 바이트 코드 스택에 STORERECORD 명령어를 넣습니다. 그리고 n을 넣는데, 이어지는 코드를 보면 어떤 것의 갯수 같습니다. 그리고 n 만큼 반복하면서 field 배열의 값을 코드 버퍼에 넣습니다.

static void flush_list (int m, int n)
{
  if (n == 0) return;
  if (m == 0)
    code_byte(STORELIST0); 
  else
  {
    code_byte(STORELIST);
    code_byte(m);
  }
  code_byte(n);
  ntemp-=n;
}

이 함수는 뭐 하는 함수인지 더 모르겠네요. 나중에 STORELIST 명령어가 뭔지 봐야 알것 같습니다. 함수 내용 자체는 간단합니다. STORELIST0 혹은 STORELIST 명령어를 바이트 코드 스택에 넣고 그 다음에 m, n 값을 바이트 코드 스택에 넣습니다. m, n 역시 어떤 것의 갯수 같습니다.

static void incr_ntemp (void)
{
 if (ntemp+nlocalvar+MAXVAR+1 < STACKGAP)
  ntemp++;
 else
 {
  lua_error ("stack overflow");
  err = 1;
 }
}

이름이 기능을 설명하는 함수입니다. STACKGAP은 Opcode.h에 128로 정의된 상수입니다. 루아 스택 VM의 최대값이 128인가? 128의 의미도 아직은 모르겠습니다. 뭐 아무튼 이 값이 128이 안되면 ntemp를 증가합니다.

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

이것도 아주 쉬운 구현입니다. MAXVAR 최대값이 넘지 않으면 nvarbuffer를 증가합니다.

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);
  }
  incr_ntemp();
}

조금 긴 함수입니다. 이름을 봐서는 바이트 코드 스택에 루아 number 타입 값을 넣는 함수로 보입니다. 내용을 읽어보죠. 루아 number 타입은 C 언어로 치면 무조건 float입니다. 4바이트 크기죠. 그런데 루아 VM은 1바이트 단위 스택 머신이므로 4바이트를 그대로 넣으면 앞에 0 매번 채워지니 낭비입니다. 그래서 저는 코드가 저렇게 생긴 것으로 해석합니다. 파라메터로 넘어오 f가 2바이트인지 확인하려고 2바이트 Word 타입으로 캐스팅해서 변수 i에 넣고 그 값을 f랑 비교합니다. 같다면 f는 2바이트 값인 것이지요. 코드를 보아하니 루아 바이트 코드 명령어는 0, 1, 2 값이 스택에 푸시됐다는 것을 의미하는 명령을 따로 만들었나 봅니다. 똑똑하네요. 가장 많이 쓰는 값이 0, 1, 2 일 테고 매번 이것을 PUSHBYTE 다음에 0, 1, 2를 넣는 식으로 스택에서 2바이트씩 차지하게 하느니 PUSH0, PUSH1, PUSH2 단일 명령으로 만들어서 성능을 높이고 스택 크기를 줄이는 전략으로 보입니다.

여기까지 인터널 함수 구현입니다. 그 다음에는 yacc 토큰 섹션입니다.

%union 
{
 int   vInt;
 long  vLong;
 float vFloat;
 char *pChar;
 Word  vWord;
 Byte *pByte;
}

%start functionlist

%token WRONGTOKEN
%token NIL
%token IF THEN ELSE ELSEIF WHILE DO REPEAT UNTIL END
%token RETURN
%token LOCAL
%token <vFloat> NUMBER
%token <vWord>  FUNCTION STRING
%token <pChar>   NAME 
%token <vInt>   DEBUG

%type <vWord> PrepJump
%type <vInt>  expr, exprlist, exprlist1, varlist1, typeconstructor
%type <vInt>  fieldlist, localdeclist
%type <vInt>  ffieldlist, ffieldlist1
%type <vInt>  lfieldlist, lfieldlist1
%type <vLong> var, objectname


%left AND OR
%left '=' NE '>' '<' LE GE
%left CONC
%left '+' '-'
%left '*' '/'
%left UNARY NOT

%union은 C 공용체 같은 겁니다. 선언한 타입으로 토큰과 규칙의 최종값을 캐스팅해서 쓰겠다는 겁니다. %token으로 사용할 토큰 종류를 정합니다. 이 토큰이 y.tab.h에 선언으로 변환됩니다. %type은 해당 문법 규칙을 전개하면 어떤 타입으로 처리해야 하는지를 정한 것입니다. %left는 연산자 우선순위를 결정한 것입니다. 자세한 내용은 yacc 문서를 보고 공부해야 합니다. 저는 yacc 문법을 이미 알기 때문에 눈으로 보는 것으로 읽기를 마쳤습니다.

댓글남기기