Documents of TEBA

TEBA演習: 第1回 TEBAを使ったプログラム変換

1 TEBA を用いた構文解析

TEBA でソースプログラムを解析して字句系列に変換するには cparse.pl を、 字句系列からソースプログラムを復元するためには join-token.pl を用います。 cparse.pl は、実行時のコマンド引数に指定されたファイルを構文解析します。 ファイルが指定されなかった場合には標準入力から読み込みます。 join-token.pl も指定されたファイルまたは標準入力を読み込み、 字句の結合を行います。

1.1 練習問題1

以下のように、標準入力からソースプログラムを入力し、どのように変換されるか 確かめなさい。

  1. cparse.pl を実行し、以下のように入力する。

int a; [改行] [Ctrl-d]

最後の [改行] はリターンキーを押すことを、 [Ctrl-d] はコントロールキーを押しながらキー “d” を押すことを意味します。

  1. ターミナルで以下のコマンドを実行する。(シングルクォートも含め正確に入力すること。)

echo 'a = f(1, 2);' | cparse.pl

1.2 練習問題2

正しく動作する簡単なプログラムを作成し、 どのように変換されるか確かめなさい。 字句の数が多いと、ターミナルの画面内に収まらないので、 以下のように less コマンドを用いると良いでしょう。 以下の例では、ファイル名を hoge.c としています。

cparse.pl hoge.c | less

less を終了させるにはキー q を押します。

1.3 練習問題3

hoge.c を変換して得られた字句系列を join-token.pl に入力することで、 元の hoge.c に戻ることを確かめなさい。確かめるには、 以下のように実行すると良いでしょう。

  1. 変換した結果をパイプを使って join-token.pl に渡し、その結果を目で確認する。

cparse.pl hoge.c | join-token.pl

  1. join-token.pl で戻した結果をファイルに保存し、 diff コマンドを用いて、元のファイルとの間に差がないか確認する。

cparse.pl hoge.c | join-token.pl > hoge2.c; diff -u hoge.c hoge2.c

この例では、シェルのリダイレクトの機能を用いて、戻した結果を hoge2.c に保存しています。

自習課題

  • シェルの「パイプ」と「リダイレクト」について調べましょう。
  • less コマンドの使い方を man を使って調べましょう。
  • diff コマンドの使い方を調べましょう。

2 TEBAを用いたソースプログラムの書換え

2.1 パターン記述

TEBA は、パターン変換によるソースプログラムの書換え機能を提供しています。 パターン変換の記述は、 書換え前のパターン記述と書換え後のプログラム断片の2つで構成します。 入れ子になっている if 文を一つの if 文に合成するパターン変換の例を以下に示します。

%before

if (${cond1:EXPR}) {
  if (${cond2:EXPR}) {
    $[stmt: ${:STMT} $]+
  }
}

%after

if (${cond1} && ${cond2}) {
  $[stmt]$;
}

%end

書き換えたい箇所の記述である書換え前パターンを %before の直後に、 その記述をどう書き換えたいかを表す書換え後パターンを %after の直後に記述します。 一般的に、書き換えたい箇所が全く同じ記述であることは少なく、 異なる部分が含まれます。その異なる箇所を、「パターン変数」や「グループ」 を用いて、抽象化した形式で記述します。

2.2 パターン変数

パターン変数は、書換え前パターンと書換え後パターンで、 それぞれ以下のように記述します。

書換え前: ${名前:型} or ${:型}
書換え後: ${名前}

書換え前のパターン変数は型が一致する字句(列)に適合し、 その字句(列)を保持します。 書換え後は、書換え前の同じ名前を持つ変数の参照を表します。 なお、書換え前は名前を省略した変数を記述できますが、 名前がないので書換え後に参照することはできません。

パターン変数の型は、字句の種別か、ユーザが定義した型を書きます。 ユーザ定義の型は、定義ファイルの一つである TEBA/default-pt.rules で定義します。定義するときは、字句の並びに対する文字列の正規表現で記述します。

例として示した入れ子の if のパターンでは、${cond1:EXPR} が、 名前が cond1 という変数で、EXPR型なので構文要素である式に適合します。 また、${:STMT} は、名前のない変数でSTMT型の構文要素である文に適合します。

自習課題:

ファイル TEBA/default-pt.rules では、EXPR, STMT, DECR, DECL といった型を定義しています。それぞれ、C言語の構文では何と呼ぶか調べなさい。 (ヒント: それぞれ英語で何の省略か、また、C言語の教科書ではそれを何と呼んでいるかを調べる。 この資料の中にも一部答えが書かれている。)

2.3 グループ

グループは、字句の並びをひとまとめに扱うための表現で、 以下のように記述します。

書換え前: $[名前: …字句列… $]
書換え後: $[名前]

書換え前は、ひとまとめに扱いたい範囲を $[名前:$] で囲みます。 書換え後は、その名前で参照をします。パターン変数と同様に、 書換え前は、名前を省略できますが、書換え後に参照できません。 パターン変数と異なり、名前の後の : の後ろには型名を付けない点に注意してください。

書換え前のグループでは、「字句列の繰り返し」を、 以下のようにグループの最後に記号を付けて表現できます。

0または1回の繰り返し: $[名前: …字句列… $]?
0回以上の繰り返し: $[名前: …字句列… $]*
1回以上の繰り返し: $[名前: …字句列… $]+

入れ子のif文例の場合、$[stmt: ${:STMT} $]+ のうち、 $[stmt: がグループの開始、$]+ がグループの終了を意味し、 名前が stmt で1回以上の繰り返しを表すグループを表現しています。 このグループは、1つの文を表わす ${:STMT} だけを含んでいるので、 1つ以上の文の並びに適合します。

自習課題:

  • グループの概念は、文字列の正規表現で用いられるグループの概念に相当します。 文字列の正規表現とは何か調べなさい。
  • egrep, sed や Perl など、文字列の正規表現を用いるツール1つについて、 どのような正規表現が使えるか調べなさい。

2.4 仮想文区切り

グループには、型がないので、グループで参照している字句列が文なのか、 式なのかといった区別ができません。そのため、 グループを用いて文や宣言を参照する場合には、 明示的に文の区切り $; を指定する必要があります。 上記の例で、$[stmt] の直後に記述されています。 なお、グループが式を参照する場合には、指定する必要はありません。

直感的には、C言語で式文の終りに書くセミコロンと同じです。 ただし、セミコロンは “;” というテキストが存在するのに対し、 $; は空のテキストで、書換え後には何も残りません。 パターン記述を構文解析するときの補助的な役割です。

仮想文区切りは、必要であれば書換え前に記述することもできます。 ただし、STMT 型のパターン変数は文であることがわかっているので、 何も指定しなくてもその直後に仮想文区切りが自動的に挿入されます。

2.5 書換え方法

書換えにはコマンド rewrite.pl を使用します。 rewrite.pl は引数に指定されたパターン記述ファイルに従い字句系列を書き換えます。 例えば、最初のパターン記述を double-if.pt というファイルに書き込み、 iff.c というソースプログラムを書き換える場合には、以下のように実行します。

cparse.pl iff.c | rewrite.pl -p double-if.pt | join-token.pl

なお、書き換えた箇所を再帰的に書換えるには rewrite.pl に -r オプションを加えます。これにより、 if 文が3重になっているときも1つの if 文に集約されます。

2.6 練習問題4

上記のパターン記述 double-if.pt と、2重の if 文を持つ iff.c を作り、 書き換えられることを確認しなさい。 なお、iff.c は動作するプログラムであること。

2.7 練習問題5

3重の if 文を持つソースプログラム ifff.c を作り、 書き換えられることを確認しなさい。 なお、ifff.c は動作するプログラムであること。

補足

書換えたときに、スペースや改行を適切に保存しないので、 書換え後のプログラムが乱れた状態になることがあります。 残念ながら、これを綺麗に出力する方法は、今のところありません。 ただし、rewrite.pl には -s というオプションがあり、スペース、改行、 コメントをすべて保存する機能が使えます。書換えの内容によっては、 -s オプションを付けた方が出力が綺麗になる場合があります。

3 複合文に関する問題

3.1 複合文に適合する変数

前の節で示したパターン記述は、以下のように記述することもできます。

% before

if (${cond1:EXPR}) {
  if (${cond2:EXPR})
    ${stmt:STMT}
}

%after

if (${cond1} && ${cond2})
  ${stmt}

%end

最内の文の形が異なることに注意をしてください。すなわち、 書換え前において、内側の if 文が複合文(波括弧で囲まれた文)ではなく、 単体のパターン変数 ${stmt:STMT} になっています。 書換え後の方も、複合文ではなく、単体のパターン変数に変わっています。 パターン変数は型があり、${stmt}が文であることがわかるので、 仮想文区切りも記述していません。

if文は、else がない場合、構文上、次のような構成になっています。 すなわち、if 文は文を1つしか持つことができません。複数の文を直接持たせる方法はありません。

<if文> ::= 'if' '(' <式> ')' <文>

複数の文を持つ場合があるのではないかと思う人は誤解をしています。 複数の文を持たせたいときは、「複合文」という1つの文を持たせます。 複合文は、複数の文を持つことができる文ですが、それ自体では1つの文として扱います 構文上は次のような構成になっています。 なお、*は 0 個以上の繰り返しを意味します。

<複合文> ::= '{' <文>* '}'

入れ子のif文を変換するときに、if文が持つ文がどうなっているかは重要ではありません。 変換したあとに、それが残っていれば問題ありません。構文に従えば、 if文は1つの文しか持たないので、パターン記述中の次の部分は、if文が持つ文が式文であろうと、 複合文であろうと適合します。

  if (${cond2:EXPR})
    ${stmt:STMT}

最初のパターン記述では次のように書いていましたが、これは複合文を持つ場合にしか適合できません。 また、複合文は複数の文を持つので、グループを使って表現をしており、より複雑なパターンになっていました。

  if (${cond2:EXPR}) {
    $[stmt: ${:STMT} $]+
  }

このように、汎用性や可読性の観点では、if文が1つの文を持つと表現したパターンの方が望ましいことになります。 なお、教科書等で、「文」とは何かを確認するようにしましょう。 a = 1; を「代入文」と呼ぶ人がいますが、間違いです。 printf("hello\n"); を関数呼出し文と呼ぶ人がいるかもしれませんが、これも間違いです。 この両者とも「式文」です。正しい用語、正しい構文規則を理解していないと、適切にパターン記述を書くことができません。

3.2 波括弧の省略

この説明を読んで、「ならば、外側の if 文の波括弧を省略できないのか?」 と疑問を持った人がいれば、良いセンスの持ち主です。 もちろん省略はできます。ただし、その場合、波括弧がある場合に対応できなくなります。 では、絶対に不可能なのかというと、そうではありません。 発想を変えて、すべてを1つのパターンで記述するではなく、 複数のパターンで記述するようにすれば簡単に解決します。 最も簡単に思いつく方法は、波括弧がある場合とない場合のパターン記述を作り、 それぞれの書換えを適用するという方法です。 波括弧がない場合は以下のように書けばよいだけです。

% before

if (${cond1:EXPR})
  if (${cond2:EXPR})
    ${stmt:STMT}

%after

if (${cond1} && ${cond2})
  ${stmt}

%end

波括弧の有無が違う2つのパターンをそれぞれを double-if1.pt, double-if2.pt という名前のファイルに記述しているなら、次のようにパイプで接続すれば目的を達成します。

cparse.pl iff.c | rewrite.pl -p double-if1.pt | rewrite.pl -p double-if2.pt | join-token.pl

もうひとつ別の方法があります。良いプログラムの書き方として、 統一的なスタイルを用いるということがあります。 波括弧が存在するかどうかを気にするということは、書換えの対象となる プログラムが統一的なスタイルでは記述されていないということを示唆します。 したがって、入れ子の if 文で波括弧がついていない場合には、 波括弧を挿入するというパターン(あるいはその逆をするパターン)を記述して、 まずそのパターンを適用するという方法があります。 こちらの方法の方が、問題の本質を見極めていることや、 スタイルの統一は再利用性の高い作業なので、 より綺麗な方法と言えるでしょう。

3.3 練習問題6

「入れ子の if 文で波括弧がついていない場合には波括弧を挿入するパターン」 を記述して、目的が達成できることを確認しなさい。

自習課題

  • C言語の制御文(for, while, do-while, if, switch)について、 それぞれの構文を調べなさい。 また、それぞれについて複合文を使った記述例と使わない記述例を調べなさい。

  • もっと一般化して、すべての制御文は必ず複合文を持つように波括弧を挿入する方法も考えてみましょう。

4 演習課題

4.1 課題1-1

プログラムをデバッグする手段の1つとして、 デバッグ時に変数の値などを出力するような処理を加えることがある。 デバッグしていないときは出力して欲くないので、ここでは変数 debugを用意し、 値が真のときのみ出力するよう if 文を使って制御する。 記述例を以下に示す。

例1:

if (debug)
    print_debug_info(p);  /* 複合文なし */

例2:

if (debug) { /*複合文あり */
    printf(stderr, "DEBUG INFO: x = %d\n", x);
    printf(stderr, "DEBUG INFO: y = %d\n", y);
}

デバッグが終了したあとも、これを残しておくと、実行するたびに条件判定が行われるので、 実行効率が悪くなる。そこで、この変数 debug を条件式に持つ if 文を削除したい。 削除するためのパターン記述を書きなさい。 ただし、複合文の使用の有無に関わらず削除できるよう1つのパターン記述で表現すること。 また、削除とは書換え後は何もない状態にすることである。

4.2 課題1-2

デバッグをしないときに、条件判定によって実行効率が下がる問題は、 前処理指令を用いることで解消できる。 前処理指令を用いた例を以下に示す。

例1:

#ifdef DEBUG
    print_debug_info(p);
#endif

例2:

#ifdef DEBUG
    printf(stderr, "DEBUG INFO: x = %d\n", x);
    printf(stderr, "DEBUG INFO: y = %d\n", y);
#endif

例3:

#ifdef DEBUG
    if (data < 0) {
      printf(stderr, "Illegal data: data = %d\n", data);
      exit(2);
    }
#endif

これらは、前処理指令を用いて、コンパイル時にマクロ DEBUG が定義されていれば、 デバッグ用の文が有効になった実行ファイルが生成される。一方、未定義の場合には、 実行ファイルにこれらのデバッグ用の文は反映されない。

実行時の実行効率が悪くなる問題は解消されるが、 特定のバグを解決するためのデバッグの処理であれば、 バグの解消後はソースコードから削除したいこともある。 そこで、この前処理指令とそれに囲まれた文をすべて削除するパターン変換を記述しなさい。 デバッグ処理内には 0 個以上の文が記述されるものとし、 記述した1つのパターン記述で文の数に関係なく削除できるようにすること。

4.3 課題1-3

プログラム中に次のような記述があったとする。

  if (! 式C)
      処理A
  else 
      処理B

このプログラムに対して、if文の条件式内の否定演算子を削除し、条件式を式Cだけにし、 さらに、then 節と else 節(この場合は処理Aと処理B)を入れ替えたとき、 プログラムとしては同じ動作をする。 また、これにより否定演算子が減るので、プログラムとしては読みやすくなる。

例えば、次のような条件文があったとする。条件を計算する式が真のとき、else 側の 式文g(); が実行されることになり、わかりにくい。

  if (! cond(a, b))
      f();
  else 
      g();

これを次のように書き換えれば、真のときに g(); が実行されることが明確になり、 読みやすい。

  if (cond(a, b))
      g();
  else 
      f();

そこで、if 文の条件式の先頭の否定演算子を削除し、かつ、 プログラムとしては同じ動作をするように書換えるパターン変換記述を1つ書きなさい。 if文が持つ文が複合文か否かに関係なく変換できるようにすること。 なお、次の課題にあるように、先頭が否定演算子があっても変換できないプログラムも存在する。 ここでは、上記の例のように単純なプログラムに対して、変換ができればよい。

4.4 課題1-4

パターン変換は、常に完璧に書けるとは限らない。 完璧でなくても、何ができて、何ができないのか、という境界を把握することが大切である。 課題1-3のパターン変換記述は、指示に対して素直に作った場合、汎用性が弱い。 条件式の先頭が否定演算子でありながら、課題1-3が適用できない、 すなわち変換できない if 文の例を書きなさい。

4.5 課題1-5

C言語には、繰り返し文として do-while 文(「do文」と呼ぶこともある)が存在する。 do-while 文を使ったプログラムの例を次に示す。

  char *str = "ABCDEF";
  int i = 0;

  do {
      printf("c = %0x ", str[i]);
  } while (str[i++] != '\0');

  printf("\n");

このプログラムに対して、良く似た次のプログラムを実行すると出力が異なる。

  char *str = "ABCDEF";
  int i = 0;

  while (str[i++] != '\0') {
      printf("c = %0x ", str[i]);
  }

  printf("\n");

すなわち、do-while 文と while 文は良く似ているが、動作が異なっている。

while 文は理解しているが、do-while 文を十分に理解できていない人の学習支援の方法として、 do-while 文を使っているプログラムを while 文を使いつつ同じ動作をするプログラムに書換えて提示する方法がある。 そこで、そのような変換を行うプログラムパターン記述を書きなさい。 なお、do-while 文の内部では break や continue は使っていないものとする。 また、動作として等価であればよく、 インデントの乱れといったコードの見栄えの良さは気にしなくてよい。

4.6 課題1-6

課題1-5 の変換では、 break や contine を使っていないという前提がある。 もし break や cotinue が含まれているプログラムに、課題1-5の変換を適用すると、 どのような問題が起こるのか、また、問題が起こらないように書換えるには どのようにすればよいのかを考え、問題とその解決方法について説明しなさい。 (パターン記述を示す必要はない)

Copyrighted by Atsushi Yoshida. atsu@nanzan-u.ac.jp
Contact: Yoshida Atsushi, atsu@nanzan-u.ac.jp
[Documents of TEBA]