[루아2.1] Lua.stx(2)

9 분 소요

Lua.stx (2)

루아 2.1 문법을 정의한 Lua.stx 파일을 계속 읽겠습니다. 이번편 글로 다 읽길 바랍니다.

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

%start functionlist

%union 키워드는 yacc가 문법을 해석하고 그 결과를 상위 문법에 전달하는 값을 C언어의 공용체 형태로 정의해 놓은 것입니다. %start 키워드는 문법의 시작을 표시합니다. 루아의 모든 문법은 functionlist 문법에서부터 시작합니다.

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

루아 문법에서 사용하는 토큰입니다. 몇개 안되는 토큰으로 문법을 구현했습니다.

%type <vLong> PrepJump
%type <vInt>  expr, exprlist, exprlist1, varlist1, funcParams, funcvalue
%type <vInt>  fieldlist, localdeclist, decinit
%type <vInt>  ffieldlist1
%type <vInt>  lfieldlist1
%type <vLong> var, singlevar
%type <pByte> body

%type 키워드는 yacc에서 문법을 다 해석하고 나면 최종 결과를 어떤 타입으로 상위 문법으로 전달할지 지정합니다. 예를 들어 expr 문법은 문법 파싱을 다 끝낸 후에 상위 문법으로 결과를 전달하는데, 그 의미가 무엇이건간에 int 타입 값으로 결과를 전달한다는 뜻입니다.

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

%left 연산자 우선순위를 지정하는 키워드입니다. 키워드 이름이 left, right인 이유는 yacc이 파스 트리(parse tree)를 만들 때 왼쪽으로 치우치게 노드를 생성합니다. 그래서 파즈 트리에 노드 생성 위치를 왼쪽으로 지정해주면 먼저 처리하고 오른쪽으로 지정하면 나중에 처리하게 됩니다. 그래서 연산자에 우선순위를 제어할 수 있게 됩니다.

functionlist : /* empty */
	     | functionlist 
	        {
	  	  pc=maincode; basepc=*initcode; maxcurr=maxmain;
		  nlocalvar=0;
	        }
	       stat sc 
		{
		  maincode=pc; *initcode=basepc; maxmain=maxcurr;
		}
	     | functionlist function
	     | functionlist method
	     | functionlist setdebug
	     ;

functionlist 문법입니다. functionlist 문법은 %start 키워드에 지정한 루아의 시작 문법입니다. 그래서 루아의 모든 문법은 functionlist 문법부터 처리를 시작해서 문법 정의를 찾아가야 합니다. functionlist 문법은 stat, function, method, setdebug 문법을 계속 반복하는 문법입니다. 그러면 이후에 나오는 문법 정의는 stat, function, method, setdebug 문법에 대한 정의여야 합니다.

아직도 정확하게 이해는 못하겠지만, functionlist 문법을 해석하는 C 언어 코드는 stat가 바로 나올 때만 작성되 있습니다. 그리고 이 코드에 등장하는 변수 이름에 initcode, maincode, maxmain 등 main 관련 이름이 등장하는 것으로 보아, 루아 문법에서 함수로 만들지 않고 바로 stat로 해석되는 문장을 main으로 간주하는 것으로 보입니다.

function     : FUNCTION NAME 
               {
		init_function($2);
	       }
               body 	 
	       { 
		Word func = luaI_findsymbol($2);
	        s_tag(func) = LUA_T_FUNCTION;
	        s_bvalue(func) = $4;
	       }
	       ;

예상 대로 function 문법에 대한 정의가 등장했습니다. 함수를 정의하는 문법입니다. 루아 함수 정의는 FUNCTION 토큰 다음에 NAME 토큰이 나오고 body 문법으로 넘어갑니다. body 문법은 잠시후에 읽겠습니다. 함수를 선언할 때 마다 루아 인터프리터는 init_function() 함수를 호출합니다. $2는 NAME 토큰에 대한 값입니다. Lex.c 파일을 보면 NAME 토큰을 리턴할 때 yylval에는 lua_constcreate() 함수의 리턴값을 넣었습니다. 이 값이 yacc 문법 파일에는 $2로 처리됩니다.

method         : FUNCTION NAME ':' NAME
	       {
		init_function($4);
	        localvar[nlocalvar]=luaI_findsymbolbyname("self");
	        add_nlocalvar(1);
	       }
	       body
	       {
	        /* assign function to table field */
	        pc=maincode; basepc=*initcode; maxcurr=maxmain;
	        nlocalvar=0;
		lua_pushvar(luaI_findsymbol($2)+1);
                code_byte(PUSHSTRING);
		code_word(luaI_findconstant($4));
                code_byte(PUSHFUNCTION);
                code_code($6);
                code_byte(STOREINDEXED0);
		maincode=pc; *initcode=basepc; maxmain=maxcurr;
	       }
	       ;

루아 2.1에서 새로 생긴 문법입니다. 새로 추가한 기능이 객체 지향이라더니 메소드 문법을 새로 만들었나봅니다. 문법 형태는 함수 정의와 거의 같고 함수 이름 뒤에 콜론(:)을 붙이고 NAME 토큰을 하나 더 쓰는데 이게 뭘 표시하는 NAME 토큰일까요? 문법 처리 코드를 읽어보겠습니다. init_function()에 파라메터로 넘기는 토큰은 콜론 뒤에 있는 두 번째 NAME 토큰입니다. body 문법을 처리한 후에 마무리 하는 코드를 보니 첫 번째 NAME 토큰이 클래스 이름인것 같군요. 메소드 선언 형태 자체는 꽤나 전통적인 형태입니다. 그도 그럴것이 지금 읽고 있는 코드가 95년도에 릴리즈된 코드입니다. 그때 최신 스타일 문법이라해도 지금 보면 전통적일 수 밖에 없죠. :)

body :  '(' parlist ')' block END
	{
          codereturn();
	  $$ = newvector(pc, Byte);
	  memcpy($$, basepc, pc*sizeof(Byte));
	  funcCode = basepc; maxcode=maxcurr;
#if LISTING
                PrintCode(funcCode,funcCode+pc);
#endif
	}
		;

body 문법은 함수나 메소드의 본체를 정의하는 문법입니다. 괄호 사이에 파라메터 리스트가 있고, 괄호 다음에 block 문법이 나옵니다. 그리고 END 토큰으로 끝납니다. 아마 block 문법에 stat가 다시 나오겠군요.

statlist : /* empty */
	 | statlist stat sc
	 ;

sc	 : /* empty */ | ';' ;

stat  : { codedebugline(); } stat1 ;

statlist는 stat를 반복하는 문법입니다. sc는 stat 문법의 끝을 표시하는 것입니다. 세미콜론을 쓰거나 아무것도 안써도 되는 군요. 이 때나 지금이나 C언어의 세미콜론 사용은 호불호가 갈리는가 봅니다. stat는 다시 stat1로 문법을 넘깁니다.

stat1  : IF expr1 THEN PrepJump block PrepJump elsepart END
	{ codeIf($4, $6); }

       | WHILE {$<vLong>$=pc;} expr1 DO PrepJump block PrepJump END
       {
        basepc[$5] = IFFJMP;
	code_word_at(basepc+$5+1, pc - ($5 + sizeof(Word)+1));
        basepc[$7] = UPJMP;
	code_word_at(basepc+$7+1, pc - ($<vLong>2));
       }
     
       | REPEAT {$<vLong>$=pc;} block UNTIL cond PrepJump
       {
        basepc[$6] = IFFUPJMP;
	code_word_at(basepc+$6+1, pc - ($<vLong>2));
       }

       | varlist1 '=' exprlist1
       {
        {
         int i;
         adjust_mult_assign(nvarbuffer, $3, $1 * 2 + nvarbuffer);
	 for (i=nvarbuffer-1; i>=0; i--)
	  lua_codestore (i);
	 if ($1 > 1 || ($1 == 1 && varbuffer[0] != 0))
	  lua_codeadjust (0);
	}
       } 
       | functioncall	{ code_byte(0); }
       | LOCAL localdeclist decinit
	{ add_nlocalvar($2);
	  adjust_mult_assign($2, $3, 0);
	 }
       ;

제어문, 반복문, 선언문, 호출문 등 기본 문법을 모두 포함한 것이 stat1 문법입니다. 제어문과 반복문 문법에 PrepJump라는 문법 이름이 많이 보입니다. 추측컨데, 제어문과 반복문의 점프 위치를 yacc 수준에서 처리하려고 만든 것일겁니다. 실제 문법상 보이지는 않지만 문법처리를 위해서 이름을 만들어 놓고 해당 문법에서는 인터프리터가 필요한 정보만 만들어 주는 것이지요. 이따가 맞는지 확인하겠습니다.

elsepart : /* empty */
	 | ELSE block
         | ELSEIF cond THEN PrepJump block PrepJump elsepart
	{ codeIf($4, $6); }
         ;

elsepart 문법은 ELSE 토큰과 ELSEIF를 사용합니다. ELSEIF 문법에서 elsepart 문법을 또 사용하므로 elseif 구문이 여러개 나오는 문법을 표현할 수 있습니다.

block    : {$<vInt>$ = nlocalvar;} statlist ret 
         {
	  if (nlocalvar != $<vInt>1)
	  {
           nlocalvar = $<vInt>1;
	   lua_codeadjust (0);
	  }
         }
         ;

block 문법은 함수, 메소드, 제어문, 반복문 등의 구현 본체입니다. C 언어로 치면 중괄호({}) 사이의 코드이고, 파스칼 문법이라면 begin-end 사이의 코드입니다. 당연히 stat 문법이 여러개 나오는 것으로 구현되지요. 로컬 변수는 그 스코프가 블럭에 한정되므로 블럭 문법을 시작할 때와 끝날 때마다 로컬 변수에 대한 처리를 해야합니다. 그래서 처리 코드를 보면 nlocalvar 변수를 사용합니다.

ret	: /* empty */
        | RETURN { codedebugline(); } exprlist sc
          {
           if ($3 < 0) code_byte(MULT_RET);
           codereturn();
          }
	;

리턴 문법을 처리하는 문법입니다. 루아 리턴은 리턴값을 콤마로 구분해서 여러개를 리턴할 수 있습니다.

PrepJump : /* empty */
	 { 
	  $$ = pc;
	  code_byte(0);		/* open space */
	  code_word (0);
         }	   

PrepJump는 제어문과 반복문에서 pc 위치를 점프하는 바이트 코드를 생성하려고 만든 임시 문법입니다. 실제 어떤 코드를 파싱하는 문법은 아닙니다.

expr1	 : expr { if ($1 == 0) code_byte(1); }
	 ;
				
expr :  '(' expr ')'  { $$ = $2; }
     |  expr1 EQ  expr1	{ code_byte(EQOP);   $$ = 1; }
     |	expr1 '<' expr1	{ code_byte(LTOP);   $$ = 1; }
     |	expr1 '>' expr1	{ code_byte(GTOP);   $$ = 1; }
     |	expr1 NE  expr1	{ code_byte(EQOP); code_byte(NOTOP); $$ = 1; }
     |	expr1 LE  expr1	{ code_byte(LEOP);   $$ = 1; }
     |	expr1 GE  expr1	{ code_byte(GEOP);   $$ = 1; }
     |	expr1 '+' expr1 { code_byte(ADDOP);  $$ = 1; }
     |	expr1 '-' expr1 { code_byte(SUBOP);  $$ = 1; }
     |	expr1 '*' expr1 { code_byte(MULTOP); $$ = 1; }
     |	expr1 '/' expr1 { code_byte(DIVOP);  $$ = 1; }
     |	expr1 '^' expr1 { code_byte(POWOP);  $$ = 1; }
     |	expr1 CONC expr1 { code_byte(CONCOP);  $$ = 1; }
     |	'-' expr1 %prec UNARY	{ code_byte(MINUSOP); $$ = 1;}
     | table { $$ = 1; }
     |  varexp          { $$ = 1;}
     |  NUMBER          { code_number($1); $$ = 1; }
     |  STRING
     {
      code_byte(PUSHSTRING);
      code_word($1);
      $$ = 1;
     }
     |	NIL		{code_byte(PUSHNIL); $$ = 1; }
     |  functioncall    { $$ = 0; }
     |	NOT expr1	{ code_byte(NOTOP);  $$ = 1;}
     |	expr1 AND PrepJump {code_byte(POP); } expr1
     { 
      basepc[$3] = ONFJMP;
      code_word_at(basepc+$3+1, pc - ($3 + sizeof(Word)+1));
      $$ = 1;
     }
     |	expr1 OR PrepJump {code_byte(POP); } expr1	
     { 
      basepc[$3] = ONTJMP;
      code_word_at(basepc+$3+1, pc - ($3 + sizeof(Word)+1));
      $$ = 1;
     }
     ;

expr은 수식을 표현하는 문법입니다. stat 문법이 표현의 최소 단위 문법이고 expr 문법은 계산의 최소 단위 문법입니다. expr 문법만 따로 떼서 VM을 돌리면 계산기죠.

table :
     {
      code_byte(CREATEARRAY);
      $<vLong>$ = pc; code_word(0);
     }
      '{' fieldlist '}'
     {
      code_word_at(basepc+$<vLong>1, $3);
     }
         ;

문법이름은 table인데 생성하는 바이트 코드 명령어는 CREATEARRAY입니다. 둘 사이는 어떤 관계가 있을까요?

functioncall : funcvalue funcParams
	{ code_byte(CALLFUNC); code_byte($1+$2); }

함수 호출 문법입니다.

funcvalue    : varexp { $$ = 0; }
	     | varexp ':' NAME 
	     { 
               code_byte(PUSHSELF); 
	       code_word(luaI_findconstant($3));
               $$ = 1;
	     }
	     ;

funcvalue는 함수 호출할 때 쓰는 함수 이름에 해당하는 문법입니다. C언어에서도 함수 이름은 단지 identifier가 아니라 함수 포인터 등을 고려해야 하는등 단순한 작업은 아닙니다. 콜론 뒤에 NAME 토큰 써서 함수 호출하는 것은 아마 클래스 메소드를 호출하는 것이 아닐까하고 생각합니다.

funcParams :	'(' exprlist ')'
	{ if ($2<0) { code_byte(1); $$ = -$2; } else $$ = $2; }
	|	table  { $$ = 1; }
	;

괄호 사이에 exprlist 문법이 있는 형태입니다. 함수 호출할 때 파라메터를 넘기는 문법을 정의한 것입니다.

exprlist  :	/* empty */		{ $$ = 0; }
	  |	exprlist1		{ $$ = $1; }
	  ;
		
exprlist1 :	expr	{ if ($1 == 0) $$ = -1; else $$ = 1; }
	  |	exprlist1 ',' { if ($1 < 0) code_byte(1); } expr 
	{
          int r = $1 < 0 ? -$1 : $1;
          $$ = ($4 == 0) ? -(r+1) : r+1;
	}
	  ;

exprlist 문법은 exprlist1 문법으로 바로 넘어갑니다. 문법 처리 코드를 보면 1과 -1만으로 어떤 작업을 합니다. 아마 로컬 변수, 전역 변수에 관련된 작업을 처리하는 것으로 보입니다.

parlist  :	/* empty */ { lua_codeadjust(0); }
	  |	parlist1    { lua_codeadjust(0); }
	  ;
		
parlist1 :	NAME		  
		{
		 localvar[nlocalvar]=luaI_findsymbol($1); 
		 add_nlocalvar(1);
		}
	  |	parlist1 ',' NAME 
		{
		 localvar[nlocalvar]=luaI_findsymbol($3); 
		 add_nlocalvar(1);
		}
	  ;

parlist는 함수 선언할 때, 파라메터 목록을 어떻게 쓰면 되는지에 대한 문법입니다. NAME 토큰을 연달아 쓰면 됩니다. 그리고 파라메터는 해당 함수의 로컬 변수로 취급됩니다. 이것은 C 언어와 같네요.

fieldlist  : /* empty */ { $$ =  0; }
	   | lfieldlist1 lastcomma
  	     { $$ = $1; flush_list($1/FIELDS_PER_FLUSH, $1%FIELDS_PER_FLUSH); }
	   | ffieldlist1 lastcomma
	     { $$ = $1; flush_record($1%FIELDS_PER_FLUSH); }
	   | lfieldlist1 ';' 
	     { flush_list($1/FIELDS_PER_FLUSH, $1%FIELDS_PER_FLUSH); }
	    ffieldlist1 lastcomma
	     { $$ = $1+$4; flush_record($4%FIELDS_PER_FLUSH); }
	   ;

lastcomma  : /* empty */ 
	   | ','
	   ;

ffieldlist1 : ffield			{$$=1;}
	   | ffieldlist1 ',' ffield	
		{
		  $$=$1+1;
		  if ($$%FIELDS_PER_FLUSH == 0) flush_record(FIELDS_PER_FLUSH);
		}
	   ; 

ffield      : NAME '=' expr1 
	      { 
	       push_field(luaI_findconstant($1));
	      }
           ;

lfieldlist1 : expr1  {$$=1;}
	    | lfieldlist1 ',' expr1
		{
		  $$=$1+1;
		  if ($$%FIELDS_PER_FLUSH == 0) 
		    flush_list($$/FIELDS_PER_FLUSH - 1, FIELDS_PER_FLUSH);
		}
            ;

루아의 테이블 타입 변수를 선언할 때 사용하는 문법을 정의합니다. 세부 문법 여러개로 나뉘어 있습니다. 필드 문법은 key-value 쌍으로 선언할 수도 있고, 그냥 value만 나열해서 선언할 수도 있습니다. 각각 필드는 콤마로 구분합니다. 맨 마지막 필드 다음에 콤마를 써도 되고 안써도 됩니다.

varlist1  :	var			
	  {
	   nvarbuffer = 0; 
           varbuffer[nvarbuffer] = $1; incr_nvarbuffer();
	   $$ = ($1 == 0) ? 1 : 0;
	  }
	  |	varlist1 ',' var	
	  { 
           varbuffer[nvarbuffer] = $3; incr_nvarbuffer();
	   $$ = ($3 == 0) ? $1 + 1 : $1;
	  }
	  ;
		
var	  :	singlevar { $$ = $1; }
	  |	varexp '[' expr1 ']' 
	  {
	   $$ = 0;		/* indexed variable */
	  }
	  |	varexp '.' NAME
	  {
	   code_byte(PUSHSTRING);
	   code_word(luaI_findconstant($3));
	   $$ = 0;		/* indexed variable */
	  }
	  ;
		
singlevar :	NAME
	  {
	   Word s = luaI_findsymbol($1);
	   int local = lua_localname (s);
	   if (local == -1)	/* global var */
	    $$ = s + 1;		/* return positive value */
           else
	    $$ = -(local+1);		/* return negative value */
	  }
	  ;

varexp	: var { lua_pushvar($1); }
	;

변수 선언 및 변수 참조 관련 문법입니다. 변수 이름만 쓰거나, 변수 다음에 점을 찍고 인덱스를 쓰거나, 변수 다음에 대괄호 사이에 인덱스를 쓰는 형태로 사용 가능합니다. 변수 여러 개를 이어서 쓰는 것도 가능합니다. 여러 개를 쓸 때는 콤마로 구분해서 나열합니다.

localdeclist  : NAME {localvar[nlocalvar]=luaI_findsymbol($1); $$ = 1;}
     	  | localdeclist ',' NAME 
	    {
	     localvar[nlocalvar+$1]=luaI_findsymbol($3); 
	     $$ = $1+1;
	    }
	  ;
		
decinit	  : /* empty */  { $$ = 0; }
	  | '=' exprlist1 { $$ = $2; }
	  ;
	  
setdebug  : DEBUG {lua_debug = $1;}

로컬 변수 선언할 때 로컬 변수 이름을 나열하는 문법이 localdeclist입니다. 변수를 선언할 때 초기 값을 동시에 지정하려면 decinit 문법을 사용합니다.

루아 1.1 코드를 읽을 때처럼 문법 구현 처리 코드를 이해하면서 읽으려고 했더니 너무 시간이 오래걸리고 어차피 잘 모르는것 코드 구현은 거의 건너 뛰고 문법 자체에 집중해서 읽었습니다. 다음에는 구현에 집중해 봐야겠습니다.

댓글남기기