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

7 분 소요

Lua.stx (3)

아직 Lua.stx 파일의 1/3 정도 밖에 못 읽었습니다. 아직도 갈 길이 멀군요. 그래도 계속 가 볼랍니다. 어차피 아직 1.1도 다 못 읽었고 다 읽는다 해도 릴리즈가 21개 더 남았습니다. 일주일에 릴리즈 한 개씩 읽는다 해도 20주가 넘게 걸리네요. 연말에 이 읽기 작업이 끝날 것 같습니다. 연말까지 제가 흥미를 잃지 않는 다면요.

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

루아 문법은 기본 단위가 함수인가 봅니다. 이해가 되지 않는 yacc 문법 정의네요. 이런식으로 작성하는 것도 허용되는가 봅니다. 저렇게 규칙 중간에 C 코드가 들어가면 어떻게 되는 건지 모르겠네요. 모르면 일단 넘어 가겠습니다. 저는 분석하는게 아니라 읽고 있으니까요.

function     : FUNCTION NAME 
	       {
		if (code == NULL)	/* first function */
		{
		 code = (Byte *) calloc(GAPCODE, sizeof(Byte));
		 if (code == NULL)
		 {
		  lua_error("not enough memory");
		  err = 1;
		 }
		 maxcode = GAPCODE;
		}
		pc=0; basepc=code; maxcurr=maxcode; 
		nlocalvar=0;
		$<vWord>$ = lua_findsymbol($2); 
	       }
	       '(' parlist ')' 
	       {
	        if (lua_debug)
		{
	         code_byte(SETFUNCTION); 
                 code_word(lua_nfile-1);
		 code_word($<vWord>3);
		}
	        lua_codeadjust (0);
	       }
               block
               END 	 
	       { 
                if (lua_debug) code_byte(RESET); 
	        code_byte(RETCODE); code_byte(nlocalvar);
	        s_tag($<vWord>3) = T_FUNCTION;
	        s_bvalue($<vWord>3) = calloc (pc, sizeof(Byte));
		if (s_bvalue($<vWord>3) == NULL)
		{
		 lua_error("not enough memory");
		 err = 1;
		}
	        memcpy (s_bvalue($<vWord>3), basepc, pc*sizeof(Byte));
		code = basepc; maxcode=maxcurr;
#if LISTING
PrintCode(code,code+pc);
#endif
	       }
	       ;

함수 문법 정의입니다. 꽤 길군요. 함수 이름을 파싱하면 바이트 코드 스택에 메모리를 할당합니다. 최초 한 번만 동작하는 것으로 보입니다. 그리고 파라메터 리스트를 파싱하면 lua_codeadjust() 함수에 0을 넘기는데요. 뭐하는 함수인지 보겠습니다.

static void lua_codeadjust (int n)
{
 code_byte(ADJUST);
 code_byte(n + nlocalvar);
}

ADJUST라는 루아 명령어를 알아야 의미를 해석할 수 있겠네요.

계속 읽어 나가겠습니다. 함수 본체는 block 규칙으로 처리합니다. 나중에 block 규칙을 보죠. 그리고 END 토큰으로 함수 끝을 표시합니다. 함수 끝을 파싱하고 나면 RETCODE 명령어를 바이트 코드 스택에 넣습니다. 그리고 로컬 변수 개수를 바이트 코드 스택에 넣습니다. $3은 $3을 vWord 타입으로 캐스팅한다는 뜻입니다. $3은 위 문법 코드 대로라면 parlist입니다. 그린 s_tag() 매크로를 따라가면 Object 타입이 나오는데, 아직 그 부분을 읽을 차례가 아니라서 그냥 이렇구나 하고 바로 지나갔습니다. 잠깐 본 코드 대로라면 나중에 나올 parlist 규칙은 최종적으로 Object 타입으로 파싱이 끝나야 합니다. 진짜 그런지 이따가 확인해보죠. 그리고 마지막으로 지금까지 바이트 코드 스택에 있는 내용을 아직 정체를 모르는 Object 타입 변수에 할당한 메모리에 복사하고 code 변수를 다시 앞으로 당깁니다. 그러면 해석해 보면 함수는 Object 타입 변수로 관리하고 함수 본체 바이트 코드는 함수 Object 변수에 저장됩니다.

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

stat	 : {
            ntemp = 0; 
            if (lua_debug)
            {
             code_byte(SETLINE); code_word(lua_linenumber);
            }
	   }
	   stat1
		
sc	 : /* empty */ | ';' ;

statlist는 아무것도 없거나 stat와 sc가 반복되는 규칙입니다. 그러면 stat 규칙을 보죠. stat 규칙은 그대로 stat1 규칙으로 넘어갑니다. 아마 stat1 규칙을 전개하기 전에 처리할 C 언어 코드를 넣으려고 만든 규칙 같습니다. ntemp를 0으로 초기화합니다. ntemp라는 변수는 stat 규칙 하나에만 유효한 변수인가 보네요. sc 규칙은 별거 없고 C 언어에서 ;이 하는 역할을 하는가 봅니다. 규칙을 보건데 루아는 ;이 있는 것과 없는 것 둘다 허용합니다.

stat1  : IF expr1 THEN PrepJump block PrepJump elsepart END
       {
        {
	 Word elseinit = $6+sizeof(Word)+1;
	 if (pc - elseinit == 0)		/* no else */
	 {
	  pc -= sizeof(Word)+1;
	  elseinit = pc;
	 }
	 else
	 {
	  basepc[$6] = JMP;
	  code_word_at(basepc+$6+1, pc - elseinit);
	 }
	 basepc[$4] = IFFJMP;
	 code_word_at(basepc+$4+1,elseinit-($4+sizeof(Word)+1));
	}
       }
     
       | WHILE {$<vWord>$=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 - ($<vWord>2));
       }
     
       | REPEAT {$<vWord>$=pc;} block UNTIL expr1 PrepJump
     	
       {
        basepc[$6] = IFFUPJMP;
	code_word_at(basepc+$6+1, pc - ($<vWord>2));
       }


       | varlist1 '=' exprlist1
       {
        {
         int i;
         if ($3 == 0 || nvarbuffer != ntemp - $1 * 2)
	  lua_codeadjust ($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			{ lua_codeadjust (0); }
       | typeconstructor                { lua_codeadjust (0); }
       | LOCAL localdeclist decinit   { add_nlocalvar($2); lua_codeadjust (0); }
       ;

이름과 내용으로 보건데 stat1은 프로그래밍 언어 이론에서 말하는 statement에 해당하는 문법 규칙입니다. 중요 문법을 다 여기에 정의합니다. 흔히 구문이라는 용어로 번역하는 바로 그것입니다.

첫 번째 statement 문법은 조건문입니다. 루아는 조건문의 조건절에서 expression을 허용하네요. 제가 싫어하는 문법이네요. 이게 나온 것이 95년이니, 옛날 느낌이 나는 문법입니다. 이후 나오는 메모리 주소 계산식이 즉각적으로 머릿속에서 계산되지는 않는데 무슨 내용인지는 알것 같습니다. else 구문이 있으면 JMP 명령을 추가해서 else 구문이 있는 코드 주소로 점프하는 코드를 삽입합니다. 그리고 IFFJMP 명령을 바이트 코드 스택에 넣습니다. 문법을 파싱하면서 컴파일을 바로 바로 하네요. 보면 볼 수록 파싱과 컴파일이 분리되지 않은 코드라 정돈이 안된 느낌입니다. 릴리즈가 계속되면 나아지리라 믿습니다.

두 번째 statement 문법은 while 반복문입니다. 마찬가지로 메모리 주소 계산식은 이해가 안되고 IFFJMP와 UPJMP가 왜 바이트 코드 스택에 들어가는지는 알 것 같습니다. 전형적인 while 반복문 구현의 형태겠지요.

세 번째 statement 문법은 repeat 반복문입니다. C 언어의 do-while 문법하고 같은 동작을 하는 문법입니다. IFFUPJMP라는 명령어를 썼습니다. 스택값이나 변수값을 확인해서 어디론가 점프하는 뭐 그런류의 명령어일겁니다.

제가 파이썬이나 루아에서 좋아하는 문법이 네 번째 statement 문법인데 변수 assignement 문법입니다. C 언어는 변수 한 개에 값 한 개 식으로 한 줄에 코딩하는데 파이썬이나 루아는 변수 여러개에 값 여러개를 한 줄에 한 번에 코딩할 수 있습니다.

이어지는 함수 호출 문법, 타입 생성자, 로컬 변수 선언 모두 lua_codeadjust() 함수를 호출해서 파싱, 컴파일을 하는데요. 여전히 lua_codeadjust() 함수가 뭘 하는지 모르기 때문에 의미를 파악할 수 없습니다. 빠른 시일 내에 알게 되길 바랍니다.

elsepart : /* empty */
	 | ELSE block
         | ELSEIF expr1 THEN PrepJump block PrepJump elsepart
         {
          {
  	   Word elseinit = $6+sizeof(Word)+1;
  	   if (pc - elseinit == 0)		/* no else */
  	   {
  	    pc -= sizeof(Word)+1;
	    elseinit = pc;
	   }
	   else
	   {
	    basepc[$6] = JMP;
	    code_word_at(basepc+$6+1, pc - elseinit);
	   }
	   basepc[$4] = IFFJMP;
	   code_word_at(basepc+$4+1, elseinit - ($4 + sizeof(Word)+1));
	  }  
         }   
         ;

조건문에서 elseif-else 문법입니다. else 문법을 쓸 때마다 조건에 따라 점프해야 할 주소를 계산하고 JMP, IFFJMP 명령어를 바이트 코드 스택에 넣습니다. 문법 파일을 다 읽고 나면 명령어 동작을 처리하는 파일을 읽어야 할 것 같군요. 명령어가 뭘 하는 녀석인지 모르니까 답답합니다.

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

block은 C 언어로 치면 {와 }사이 코드를 뜻합니다. 그 사이에는 당연히 statements가 있지요. 루아 문법도 같습니다. block 문법 자체는 nlocalvar 변수값으로 전개 결과를 돌려보냅니다. 아직 의미는 모르겠군요. 왜냐면 statlist 문법에 로컬 변수 선언 문법이 포함되 있기 때문에 statlist 문법을 전개하기 전에 nlocalvar 변수값을 읽는 것은 직전 블록의 nlocalvar 변수값이거든요. 코드 흐름상의 이유로 이렇게 만들었나 봅니다. 루프 돌다 보면 이렇게 (A)가 end of (A-1)의 값을 가지고 있어야 하는 경우가 생기거든요. 이 코드도 그런 이유 아닐까 생각합니다.

ret	: /* empty */
        | { if (lua_debug){code_byte(SETLINE);code_word(lua_linenumber);}}
          RETURN  exprlist sc 	
          { 
           if (lua_debug) code_byte(RESET); 
           code_byte(RETCODE); code_byte(nlocalvar);
          }
	;

루아는 함수 리턴에서 변수 여러개를 리턴할 수 있습니다. 그래서 exprlist가 리턴 문법에 나옵니다. 바이트 코드는 현재 해석 불가입니다. 다음 릴리즈 코드를 읽을 때는 해석할 수 있겠지요.

PrepJump : /* empty */
	 { 
	  $$ = pc;
	  code_byte(0);		/* open space */
	  code_word (0);
         }
	   
expr1	 : expr { if ($1 == 0) {lua_codeadjust (ntemp+1); incr_ntemp();}}

PrepJump 문법은 C 언어 코드를 삽입하려는 목적으로 만든 문법입니다. 현재 PC 주소값을 규칙 전개 결과로 돌려보냅니다.

expr1 문법도 expr 문법으로 바로 넘어갑니다. expr 문법을 전개한 다음에 lua_codeadjust() 함수와 incr_ntemp() 함수를 호출하려고 만든 문법 규칙입니다.

expr :	'(' expr ')'    { $$ = $2; }
     |	expr1 '=' expr1	{ code_byte(EQOP);   $$ = 1; ntemp--;}
     |	expr1 '<' expr1	{ code_byte(LTOP);   $$ = 1; ntemp--;}
     |	expr1 '>' expr1	{ code_byte(LEOP); code_byte(NOTOP); $$ = 1; ntemp--;}
     |	expr1 NE  expr1	{ code_byte(EQOP); code_byte(NOTOP); $$ = 1; ntemp--;}
     |	expr1 LE  expr1	{ code_byte(LEOP);   $$ = 1; ntemp--;}
     |	expr1 GE  expr1	{ code_byte(LTOP); code_byte(NOTOP); $$ = 1; ntemp--;}
     |	expr1 '+' expr1 { code_byte(ADDOP);  $$ = 1; ntemp--;}
     |	expr1 '-' expr1 { code_byte(SUBOP);  $$ = 1; ntemp--;}
     |	expr1 '*' expr1 { code_byte(MULTOP); $$ = 1; ntemp--;}
     |	expr1 '/' expr1 { code_byte(DIVOP);  $$ = 1; ntemp--;}
     |	expr1 CONC expr1 { code_byte(CONCOP);  $$ = 1; ntemp--;}
     |	'+' expr1 %prec UNARY	{ $$ = 1; }
     |	'-' expr1 %prec UNARY	{ code_byte(MINUSOP); $$ = 1;}
     | typeconstructor { $$ = $1; }
     |  '@' '(' dimension ')'
     { 
      code_byte(CREATEARRAY);
      $$ = 1;
     }
     |	var             { lua_pushvar ($1); $$ = 1;}
     |	NUMBER          { code_number($1); $$ = 1; }
     |	STRING
     {
      code_byte(PUSHSTRING);
      code_word($1);
      $$ = 1;
      incr_ntemp();
     }
     |	NIL		{code_byte(PUSHNIL); $$ = 1; incr_ntemp();}
     |	functioncall
     {
      $$ = 0;
      if (lua_debug)
      {
       code_byte(SETLINE); code_word(lua_linenumber);
      }
     }
     |	NOT expr1	{ code_byte(NOTOP);  $$ = 1;}
     |	expr1 AND PrepJump {code_byte(POP); ntemp--;} expr1
     { 
      basepc[$3] = ONFJMP;
      code_word_at(basepc+$3+1, pc - ($3 + sizeof(Word)+1));
      $$ = 1;
     }
     |	expr1 OR PrepJump {code_byte(POP); ntemp--;} expr1	
     { 
      basepc[$3] = ONTJMP;
      code_word_at(basepc+$3+1, pc - ($3 + sizeof(Word)+1));
      $$ = 1;
     }
     ;

expr 문법은 프로그래밍 언어론에서 말하는 expression 문법입니다. 사칙연산, 조건문 등 계산하는 문법을 expression 문법이라고 합니다. 위 루아 문법 규칙을 봐도 초반부에 조건식과 사칙연산, 단항 연산자 등이 나옵니다. 코드도 이해하기 쉬우니 쭉 읽고 넘어갑니다. 그리고 변수 이름, 숫자, 문자열은 assignment 구문의 우항으로 사용하는 문법입니다. 함수 호출, NOT으로 결과 반전하는 것 역시 마찬가지 입니다. 조건문에서 AND, OR로 불리언 연산도 expression 문법으로 처리합니다. 쉽네요.

typeconstructor: '@'  
     {
      code_byte(PUSHBYTE);
      $<vWord>$ = pc; code_byte(0);
      incr_ntemp();
      code_byte(CREATEARRAY);
     }
      objectname fieldlist 
     {
      basepc[$<vWord>2] = $4; 
      if ($3 < 0)	/* there is no function to be called */
      {
       $$ = 1;
      }
      else
      {
       lua_pushvar ($3+1);
       code_byte(PUSHMARK);
       incr_ntemp();
       code_byte(PUSHOBJECT);
       incr_ntemp();
       code_byte(CALLFUNC); 
       ntemp -= 4;
       $$ = 0;
       if (lua_debug)
       {
        code_byte(SETLINE); code_word(lua_linenumber);
       }
      }
     }
         ;

typeconstructor는 루아 문법에서 테이블을 선언할 때 생성자 함수 포인터를 지정하는 문법입니다. 바이트 코드 스택에 값이 어떻게 들어가는지는 알겠는데 의미를 모르겠네요. 그냥 읽고 넘어가겠습니다. 다음번에 읽을 때는 의미를 알 수 있기를 바라며…

여기까지 해서 Lua.stx 파일을 반 정도 읽었습니다. 나머지는 다음 글에서 읽겠습니다.

댓글남기기