Compiler Construction Lecture 1/17

Menu Menu


先週の復習

先週は、register を使った compiler の作成方法を勉強した。最適化を考えなければ、コード生成自身はstack versionとほとんど変わらない。これは、registerの番号の上にstackのコードを部分計算することに相当する。先週解いた問題は、このような方法でどのようなコードが生成されるかを問うものだった。もちろん、これよりも、賢いコード生成をおこなうことは簡単だが、それを問題の答えとするならば、同時にコード生成のアルゴリズムを示さなければ完結した解答とはいえない。


モードに依存する文法

文法規則だけでは解釈する情報が足りない場合がある。例えば、このような場合は、文法解釈の途中で、さらに文法構造とは別のモード(mode)を用意する。あるいは、いったん曖昧なまま構文木を作ってしまい、その後に解釈を行う方法もある。しかし、いったん木を作った後、木の構造を操作すると見通しが悪く間違いを起こしやすい。構文解析の途中で、mode を設定する方法が直観的で分かりやすいことも多い。例えば、 Cの文法は、かなり複雑であるが、modeを使うと簡単になる部分がある。

例えば、Micro-C では、Cの以下の宣言を一つの文法で済ましている。

これらは、mode 変数の、

    45	#define TOP	0
    46	#define GDECL	1    /* global 変数 */
    47	#define GSDECL	2    /* global struct */
    48	#define GUDECL	3    /* global union */
    49	#define ADECL	4    /* argument 変数 */
    50	#define LDECL	5    /* local 変数 */
    51	#define LSDECL	6    /* local struct */
    52	#define LUDECL	7    /* local union */
    53	#define STADECL 8    /* static */
    54	#define STAT	9    /* function の最初 */
    55	#define GTDECL	10   /* global typedef */
    56	#define LTDECL	11   /* local typedef */

によって区別されている。getsym() では、このmodeを見ながら、どの処理を変えている。ただし、このmodeによる区別はプログラムの見通し悪くするので使い方には気を付けよう。


Symbol Table

Tokenizerでは、記号の切り出しとkeywordの切り出しをおこなう。同時に、名前の登録をおこなう必要がある。実際、Tokenizerが、普通の名前、(例えば英字で始まる英数字の列などだが)に出会ったとしよう。それは、名前(name)、つまり、予約語(reserved word)か、変数名か関数名である。Micor-C では、getsym() という関数がTokenizerである。

これらは compiler 内部の表に登録されなければならない。この表を Symbol Tableという。Interpreter の場合でもSymbol Tableは必要である。Compilerが出力したコードでは、Symbol Tableは不要である。しかし、分割compile (Separate compilation)の場合は、相互のSymbol Tableを接続する必要があるので、Symbol Table そのものも出力しなくてはならない。この問題は、異なるcomputer上で分散計算を行う場合にも出て来る。

名前には様々な属性がある。例えば、ある名前はlocalであり、ある関数や、関数の一部でしか有効でない。ある名前で指し示される(実体/objectゥ)ものは、名前で呼ばれなくなった後も存在し続ける場合がある。あるいは、そのようなものを別な名前で呼びたい時もある。このような属性には以下のようなものがある。

例えば、Cだったら、scopeとextentには以下のようなものがある。

compilerにとって問題なのは変数の有効範囲であり、大域名(global)と局所名(local)が衝突した場合には大域名を優先する必要があり、局所名が有効でなくなれば、その名前を再利用する必要がある。

名前には、その名前が指すもの(object)の型がある。一つの名前には一つの型がある言語(Cはそうだ)もあるが、一つの名前を複数の型に使うことができるもの(Perlなど)もある。前者の場合は、名前を検索することにより型が分かる。しかし、後者の場合には、型は名前とは別に指定する必要がある。後者の方がどちらかといえば優れているようである。Cには以下のような型がある。

さらに、Cには、cast という概念があり、型を変換することができる。この時の構文はなかなか面白い。このあたりのcompileの詳細は、また後日、追求することにしよう。


Implementation of Symbol Table

Symbloc Table(記号表)の実装は、さまざまな方法があるが、hash tableと、stack を組み合わせるのが容易である。

localな名前は、stack上に作られる。入れ子(nest)になったものを実現する場合には常にstackを使う。localな名前が作れる度に名前はstackにつまれる。そして、compileが、そのscope(手続き、または{}(statement))から抜けると、必要な所までstackを開放する。検索を、localな名前を格納したstack そして、大域名を格納したhash tableという順番に探すことにより、Cの変数の有効範囲を実現することができる。

名前にはreserved word(予約語)が取られることがある。例えば、if, for, continue などはCの予約語である。これらの語を字句解析のレベルで切り分ける手もあるが、ユーザの定義よりも先に表に登録してしまうという手もある。すると、reserved wordかどうかは、一種の名前の型となる。 </p>

さて、Micro-CではSymbol tableは以下のように定義されている。

   168  typedef struct nametable {
   169          char nm[9];
   170          int sc,ty,dsp; } NMTBL;
   171  
   172  NMTBL ntable[GSYMS+LSYMS],*nptr,*gnptr,*decl0(),*decl1(),*lsearch(),*gse

arch();

9文字しか変数名は見ないらしい。逆に言えば、iという変数に対しても9文字分のデータが取られている。実用的にはこんなものかも知れない。GSYMS,LSYMSは、global, local の記号の最大値である。定数は、このように大文字のマクロで書くのが C 流である。sc は、たぶん、symbol class の意味で、以下のどれかである。これと ty との値で型が決まる。ty には、struct, unioon を表す木構造(tree)か INT が入る。

さて、getsym()を見てみよう。

  2520  getsym()
  2521  {NMTBL *nptr0,*nptr1;
  2522  int i;
  2523  char c;
  2524          if (alpha(skipspc()))
  2525          {       i = hash = 0;
  2526                  while (alpha(ch) || digit(ch))
  2527                  {       if (i &lt;= 7) hash=7*(hash+(name[i++]=ch));
  2528                          getch();
  2529                  }
  2530                  name[i] = '\0';
  2531                  nptr0 = gsearch();
  2532                  if (nptr0->sc == RESERVE) return sym = nptr0->dsp;

ここでは、hash table という技術を使っている。名前によって決まったrandamな値により、表を引く。このようにすることにより、randamな値が重ならなければ一度だけで記号に対応する値を取りだすことができる。2527ではhashを計算するだけで、実際の検索は、gsearch(),lsearch() でおこなう。

  2661  NMTBL *gsearch()
  2662  {NMTBL *nptr,*iptr;
  2663          iptr=nptr= &ntable[hash % GSYMS];
  2664          while(nptr->sc!=EMPTY && neqname(nptr->nm))
  2665          {       if (++nptr== &ntable[GSYMS]) nptr=ntable;
  2666                  if (nptr==iptr) error(GSERR);
  2667          }
  2668          if (nptr->sc == EMPTY) copy(nptr->nm);
  2669          return nptr;
  2670  }
  2671  NMTBL *lsearch()
  2672  {NMTBL *nptr,*iptr;
  2673          iptr=nptr= &ntable[hash%LSYMS+GSYMS];
  2674          while(nptr->sc!=EMPTY && neqname(nptr->nm))
  2675          {       if (++nptr== &ntable[LSYMS+GSYMS]) nptr= &ntable[GSYMS];
  2676                  if (nptr==iptr) error(LSERR);
  2677          }
  2678          if (nptr->sc == EMPTY) copy(nptr->nm);
  2679          return nptr;
  2680  }

このsearchは、もし表になかった場合には登録も行う。これは標準的な実装である。lsearch()とgsearch()の差はどこにあるか考えてみよう。

  2532                  if (nptr0->sc == RESERVE) return sym = nptr0->dsp;
  2533                  if (nptr0->sc == MACRO && !mflag)
  2534                  {       mflag++;
  2535                          chsave = ch;
  2536                          chptrsave = chptr;
  2537                          chptr = (char *)nptr0->dsp;
  2538                          getch();
  2539                          return getsym();
  2540                  }
  2541                  sym = IDENT;
  2542                  gnptr=nptr=nptr0;
  2543                  if (mode==GDECL || mode==GSDECL || mode==GUDECL ||
  2544                      mode==GTDECL || mode==TOP)
  2545                          return sym;
  2546                  nptr1=lsearch();
  2547                  if (mode==STAT)
  2548                          if (nptr1->sc == EMPTY) return sym;
  2549                          else { nptr=nptr1; return sym;}
  2550                  nptr=nptr1;
  2551                  return sym;
  2552          }

検索が修了すると、その後で予約語とマクロの処理を行う。どちらでもなければglobalかlocalかどちらかである。この後、その記号のscとtyが決まることになる。これらは、構文解析の途中で決定するので、そこで代入される。mode により、記号の扱いが変わることに注意しよう。


型のコンパイル

C では、変数の宣言は以下のように行われる。

     int i,*ptr;

これが、表れる場所によって、global変数やlocal変数となる。シンボル表の登録は、getsym()によってglobalとlocalに分けて、行われるので、getsym() が呼ばれる前に、mode が決まっていなければならない。シンボルのsymbol classやdispの設定は、def() でやはりmodeを見て行われる。def() では、初期値の設定もおこなわれる。

ここで型名には、いろいろなものがくる。

しかも、この型名の構文は、通常の関数の定義と、cast (型変換の場合で異なる。例えば、関数へのポインタの宣言と、関数へのポインタの大きさを取る場合では以下のようになる。

    int (*func)();
    j  = sizeof(int (*)());

前者は typespec()とdecl0()で処理され、後者は、typename()とndecl0()で処理される。これを例えば以下のように使うことができる。(まるでアセンブラのようだ...)

    func  = (int (*)())i;
    return (*func)(j);

宿題

    int j;
    j  = sizeof(int (*)());

が、どのようにコンパイルされるかを、文字列と手続名と行番号の対応で示せ。 (ヒント、sizeof は、expr13()にあります。gdb とかを有効に使うと楽勝) 単なるtraceだと何が何だか分からないと思います。

Shinji KONO / Mon Jan 17 09:45:52 2000