TEBA の解析器 cparse.pl から徐々に読んでいきます。 完璧に理解できなくても良いので、 全体としてどのような処理として実現しているのか理解してください。
cparse.pl は、解析器インスタンスを生成したあと、 標準入力から読み込んだテキストで解析し、標準出力に出力します。 以下に、主要な記述を抜粋したものを示します。
#!/usr/bin/env perl
use FindBin qw($Bin);
use lib "$Bin/../TEBA";
use CParser;
$cp = CParser->new();
$t = $cp->parse(join('', <>));
print $t;
この記述の最初の2つの use
は、TEBA
のライブラリの読み出しパスの設定を行うものです。
“use Cparser;
” は、TEBA のモジュールファイル Cparser.pm
を読み込みます。
ファイルは、直前で設定したライブラリの読み出しパスに従って探します。
Perl
のモジュールは、オブジェクト指向におけるクラスとして扱うことができ、
CParser.pm にはインスタンスを生成するメソッド new()
が定義されています。 Cparser->new()
により、メソッドが呼ばれ、インスタンスが返されます。
この場合、インスタンスは変数 $cp
が参照します。
クラス Cparser には解析を行うメソッドである parse()
が定義されています。 parse()
は、ソースプログラムをテキストとして受け取り、構文解析をした結果を返します。
join('', <>)
は、標準入力から行単位の文字列をすべて読み取り、
文字 ''
で挟んで結合します。文字 ''
は空文字なので、
実質的には標準入力から読み取ったすべての行を結合します。
ここで、<>
はリストを記述する文脈(リストコンテキスト)で使われています。
スカラーコンテキストの場合は、1行のみの読み取りとなりますが、
リストコンテキストの場合には、すべての行を読み込み、
各行を要素と持つリストになります。 (注:
実際には、実際に各行が必要になるまで読み込みは遅延されます。)
CParser.pm
は、C言語の構文解析器であり、その解析フィルタのインスタンスを生成し、
それらを結合して処理を構成します。 最初の package Cparser;
はモジュールの名前の設定です。 そのあとの、use
で読み込んでいるモジュールは、解析器を構成するフィルタの定義です。
メソッド new()
は、CParser
インスタンスを生成し、
解析に必要な定義ファイルを読み込んで、インスタンスに設定します。
インスタンスの生成は、特定のデータ構造に bless
命令を適用して実現します。 CParser は無名ハッシュテーブル
{ }
を用いてインスタンスを生成しており、
ハッシュテーブルとしての機能にメソッドが付加されたオブジェクトになります。
ハッシュテーブルへの登録は、以下のようにキー(key)を指定して値(value)を設定します。
参照のときも同様です。
$self->{'key'} = 'value'; # 'key' というキーで値 'value' を登録
print $self->{'key'}, "\n"; # 'key' に該当する値を出力
各設定ファイルは open()
を用いて開きます。第1引数はファイルハンドラで、 < と >
で囲うことで、ファイルを行単位で読み出します。
1行だけ読み出すか、すべての行を読み出すかはコンテキストに依存します。
読み出しあとは、close()
でファイルハンドラを閉じます。
open()
のあとの die
は理由を提示してプログラムを停止させる命令です。
解析に必要なフィルタのインスタンスを生成して、ハッシュテーブルに登録します。 作成するフィルタは以下の通りです。
フィルタ名(key) クラス 用途 Tokenizer Tokenizer 字句解析器 (token.def) Prep RewriteTokens 前処理命令解析 (prep.rules) BracketsID BracketsID 括弧対応関係解析 CoarseGrainedParser CoarseGrainedParser 粗粒度構文解析 BeginEnd BeginEnd 仮想字句対応関係解析 NameSpaceRules RewriteTokens 名前空間解析 (namespace.rules) MacroStmt RewriteTokens マクロ文補正 (macro-stmt.rules) EParser EParser 式解析
これらのフィルタを作成したらキー build
の値を
1
に設定することで、 build()
が実行されているかどうか判定できるようにしています。
メソッド parse()
では、字句解析をして字句系列を得てから、
字句系列に対して構文解析を適用して、その結果を返します。 解析を行う前に
build()
していることが事前条件となるので、
build()
されていない場合には build()
を実行します。
生成したフィルタを順次適用することで、解析を実現します。 主な流れを下図に示します。
各フィルタの内容は以下の通りです。
字句解析 | 字句定義に従って字句を切り分けます。 | 前処理命令解析 | 前処理命令の範囲を特定し、仮想字句である B_DIRE と E_DIRE で囲うなどの処理を行います。 | 括弧対応解析 | 対応する括弧の組に同じID(識別記号)を属性値として与えます。また、前処理の条件分岐により括弧の対応が取 マクロ文補正 | 関数呼び出し(に見えるもの)だけの行で、行末にセミコロンがない場合に、 仮想的なセミコロンを挿入し、文 粗粒度構文解析 | 字句系列を書換えながら、関数や文などを識別していきます。この段階では、識別子の名前空間は区別せず、宣 名前空間解析 | 識別子をタグ, ラベル, 構造体・共用体メンバ, マクロ, 型, 変数・関数に分類します。また、この結果に基づ 仮想字句対応関係解析 | 対応関係にある仮想字句に同じIDを属性値として与えます。| 式解析 | 式レベルの構造を解析します。 |
これらのうち、字句解析と括弧対応解析、仮想字句対応関係解析以外のフィルタは、 書換え規則を用いて定義しており、字句系列の書換え系である RewriteTokens.pm を用いて処理をしています。 全体を書換えルールで書くことで、見通しの良い設計になるようにしています。
仮想字句対応関係解析は、粗粒度構文解析の中でも何度か呼ばれています。 TEBA の解析は、基本的に仮想字句を挿入するだけの作業であり、 仮想字句には識別記号を割り振っていきます。 粗粒度構文解析の冒頭までは、識別記号がない状態で処理をし、 基本的な構造が解析できた時点で、識別記号を割り振ります。 それ以降は、仮想字句を挿入する時点で識別記号を割り振りますが、 フィルタ間で重複した識別記号を割り振る可能性があるので、 式解析の前に再度割り振り直しています。
C言語について、以下のことを調べなさい。
- 前処理命令にはどのようなものがあるか
- 「宣言」とは何か
- C言語で「名前空間」とは何か、また、TEBA が扱う名前空間との差異は何か
Toknizer は、字句解析を指定されたルールに従って行います。 ルールでは、Perl の正規表現を用いて字句を定義しており、 この定義が字句解析において重要な要素となります。 以下のように、字句の種別のあとに正規表現を記述します。
# Control word followed by condition expressions and a block
/(?:for|while|switch)\b/
CT_BE /if\b/
CT_BEI # Control word followed by a block
/(?:else)\b/
CT_B /(?:do)\b/
CT_BD
/(?:typedef|struct|union|enum)\b/
RE_TP /(?:case|default)\b/
RE_LB /(?:return|continue|goto|break)\b/
RE_JP
/(?:void|int|char|long|short|float|double)\b/
ID_TP
# type_specifier (except ID_T)
/(?:un)?signed\b/
ID_TPSP # storage class specifier
/(?:static|extern|register|auto)\b/
ID_TPCS # type qualifier
/(?:volatile|const|restrict)\b/
ID_TPQ # function specifier
/(?:inline)\b/
ID_TPFS
/__[a-z_]+__\b/
ATTR
/(?:sizeof|defined)\b/
OP
/\(/
P_L /\)/
P_R /\[/
A_L /\]/
A_R /{/
C_L /}/
C_R /;/
SC /,/ CA
Tokenizer を使うには、インスタンスを生成し、load() メソッドまたは
set_def() メソッドで、このような定義を読み込ませておきます。 parse()
メソッドに解析対象のテキストを入力させると、
字句系列に変換したテキストが返ります。 解析にあたっては、
字句定義の先頭にあるものから順に正規表現に適合するかどうかを調べ、
適合した部分を字句として切り出します。 “#ifdef” と “#if” のように、
包含関係にある字句の場合には、
長い字句の方を先に定義する必要があります。 なお、定義で #
で始まる行はコメントです。
TEBA では、ファイル token.def にC言語の字句の定義が書かれています。 また、TEBA の各種定義ファイルを読むときにも Tokenizer を利用しており、 ファイル syntax-token.def には後述する RewriteTokens.pm が読み込む書換えルールのための字句の定義が記述されています。
TEBA では、字句系列が基本データであり、 字句系列の書換えを繰り返すことで構文解析を実現します。 字句の書換えでは以下の3つの操作を組み合わせたルールを記述します。
RewriteTokens のインスタンスは、書換えルールの定義を読み込み、 そのルールに従って書換えをするフィルタになります。
複数の書換えルールを定義した場合、原則としては上から下に向かって適用されますが、 以下の3種類の戦略を選択できます。
このうち1番の戦略がデフォルトになっています。 これは、規則間の依存関係を気にせずに適用したい場合に適していますが、 ルールの順序によっては実行時間が極端に長くなることがあります。 2番目の戦略は、規則間の依存関係にループが存在しない場合に利用でき、 繰り返しが存在しないので、最も効率が良くなります。 3番目は、複数の規則を組み合わせて繰り返し適用する必要がある場合に用います。 1番に比べて挙動がわかりやすいので、こちらの利用を推奨します。
戦略の選択は、RewriteTokens のインスタンスを生成したあとに
戦略を決めるメソッドを実行します。具体的には、 2番目の戦略はメソッド
seq()
を、 3番目の戦略はメソッド
rep()
を呼ぶと選択できます。
何もメソッドを呼び出さないと1番目の戦略になります。
戦略1がデフォルトになっている理由は、開発の初期段階で使用していたので、 その互換性を保つためです。 初期段階では、ルールが整理されておらず、あらゆるルールを1つのインスタンスで実行しつつ、 ルール間の優先順位を制御したいという難しい要求を実現するために採用しました。 現在は、書換えルールで実現する機能が整理されているので、 ルールの適用順序を細かく制御できるよう、戦略2と戦略3を使い分けています。 まだ、戦略1を使っている箇所がありますが、これは戦略3と互換で、 実行時間を測定したところ戦略1の方が短かかったので、 そのまま戦略1を採用しています。
書換えルールの例を以下に示します。
# (ルールA) SUE型の字句(予約語 struct, union, enum のどれか)と
# それに続くタグを B_SUE と E_SUE で囲う
@SUE => "RE_TP\s+<(?:struct|union|enum)>\n"
{ $st:SUE '(?:':X $sp:SP $tag:IDN ')?':X }
=> {'':B_SUE $st $sp $tag '':E_SUE }
# (ルールB) 構造体・共用体のメンバの型を IDN から ID_MB に変更
@MEM_REF => "OP\s+<(?:\.|->)>\n"
{ $ref:MEM_REF $sp:SP $mem:IDN } => { $ref $sp $mem:ID_MB }
# (ルールC) 一時的な作業用の仮想字句 _B_ARG と _E_ARG を削除
{ $a:_B_ARG } => { }
{ $a:_E_ARG } => { }
# (ルールD1) 演算子 *, /, % について、それぞれ右側の式と結合するように
# 仮想字句 _B_OP03 と _E_OP03 で囲む
@OP03 => "OP\s+<[\*\/%]>\n"
{ $op:OP03 $sp:SP $bx#1:B_X $any:XEXPR $ex#1:E_X }
=>> { ''#1:_B_OP03 $op $sp $bx:B_P $any $ex:E_P ''#1:_E_OP03 }
# (ルールD2) 演算子 *, /, % について、それぞれ左側の式と結合するように
# 仮想字句 _B_X と _E_X で囲み、仮想字句 _B_OP03 と _E_OP03 は除去
{ $bx1#1:B_X '(?>':X $any1:XEXPR $ex1#1:E_X ')':X $sp1:SP
$bx2#2:_B_OP03 $op:OP03 $sp2:SP $any2:XEXPR $ex2#2:_E_OP03 }
=>> { ''#1:B_X $bx1:B_P $any1 $ex1:E_P $sp1 $op $sp2 $any2 ''#1:E_X }
ルールは { 書換え前字句系列 } => { 書換え後字句系列 }
と書きます。 書換え前は、字句(系列)を $変数名:種別
の形式で表現し、 その変数で適合した字句(系列)を書換え後は
$変数名
で参照します。
書換え後に種別名を変更したい場合には、変数名の後ろに変更後の種別を
$変数名:変更後の種別
と記述します。
また、新たに字句を加えたい場合には '字句':種別
と記述します。
Perlの正規表現のグループ化を使用でき、例えば、
ルールAのようにタグ名が存在する場合と存在しない場合の両方を条件として記述できます。
ルールAの '(?:':X
がグループ化の始まりを、
')?':X
がグループ化の終りを表します。 グループ化の終りの
‘?
’ が、そのグループの範囲が1つ存在する、
または存在しないという意味を表します。
型 X を用いていますが、型そのものは未定義です。 RewriteTokens
は、内部で文字列の正規表現に変換しており、
書換え前字句系列に'字句':種別
と記述した場合、種別と関係なく、
その正規表現に字句
を配置します。
よって、正規表現に関する記述を書けば、そのまま反映されることになります。
グループ化は、正規表現の記述をそのまま利用しているだけなので、
意味がわからない場合には、まず、正規表現について復習をしてください。
書換えルールは、字句系列の中で、
書換え前字句系列に適合するすべての箇所を書換えます。
ただし、書き変わった箇所に再度適用可能な箇所があっても、
その部分は無視されます。書き変わった箇所にも再帰的に適用したい場合には、
書換えルールで =>
の代わりに
=>>
と書きます。
上記の例ではルールD1とルールD2が繰り返しになっています。
ルールD1とルールD2は、複数の演算子を含む式の構造を解析するルールであり、
変数 $any
, $any1
, $any2
が適合した字句列に対しても、再帰的に
同じルールを適用する必要があります。
すでに説明したルール群の適用の戦略での繰り返しと混乱しないように気をつけてください。 ここでの説明は、あくまで、単体のルールの適用の繰り返しです。 また、ルール群での繰り返しの中で、単体のルールの繰り返しをすると、 二重ループになるので、効率が悪くなります。 効率の良い書換えを実現するためには、できるだけ、繰り返しは避けることが必要です。
ルールで用いる種別は、字句解析後に、各字句の先頭に記述される属性が該当します。 ただし、自分で定義することもできます。上記のルールAの SUE という種別は実際には存在せず、以下のように定義をしています。
@SUE => "RE_TP\s+<(?:struct|union|enum)>\n"
これは、種別 RE_TP の字句で、 struct, union, enum のいずれかをテキストで持つことを意味します。 ルールA, C, D1, D2 では共通して SP という種別を用いていますが、 これは任意の空白、改行、コメントおよび前処理命令の連続を意味しており、 以下のように定義しています。
@_SPC => "SP.*+\n"
@_DIRE => "B_DIRE.*+\n(?>(?:@ANY)E_DIRE).*+\n"
@_SP => "@_SPC|@_DIRE"
@SP => "(?:@_SP)*+"
すべてを1行で書くと複雑で保守性が悪くなるので、定義を構造化しています。 TEBA では、各書換えルールに共通する定義はファイル token-patterns.def に定義し、 特定の書換えルールだけの定義は書換えルールを定義しているファイルの中に記述しています。
@SP の定義に
“*+
” がありますが、これは単に “*
”
と書いた場合と同じです。
Perlの正規表現のエンジンは、バックトラックを抑制する仕組みを提供しており、
その仕組みを使うことで実行時間を改善できることがあります。
この場合は空白の連続は常にひとまとめに扱い、
バックトラックする必要がないことを指示しています。
バックトラックを抑制する方法として、(?> ... )
という括弧を使った表現があります。 ルールD2の '(?>':X
と
')':X
がその利用例です。
これらの記法は、最適化のために用いているので、意味がわからない場合には、 とりあえず無視して読んでください。
TEBA
では、仮想字句や括弧字句は、組み合わせを表現する識別記号を持っています。
プログラムを書き換えるときには、この組み合わせを正しく取り扱う必要があり、
RewriteTokens は同じ識別記号を持つことを示す方法として、 変数名の直後に
#
と記号を記述します。
ここでの記号は、字句が持つ識別記号とは無関係にルールごとに定め、
英数字の組み合わせで表現します。
例えば、ルールD1で、変数$bx#1:B_X
と$ex#1:E_X
には
#1
が指定されていますが、
これにより変数$bx
と$ex
が適合する仮想字句 B_X
と E_X は同じ識別記号を持つことを意味します。
なお、この対応関係を表す#
記号は、
ユーザが定義する種別(たとえば、SUE 型)には使えません。
これはユーザ定義型は単体の字句と適合するとは限らないためです。
書換え時には、このような対応関係を探すだけでなく、
挿入する字句に対応関係を与えたい場合があります。 ルールD1で
''#1:_B_OP03
と ''#1:_E_OP03
は
仮想字句の挿入を表し、#1
を指定したことで、
この2つの字句は同じ識別記号が自動的に割り振られます。 なお、TEBA
の書換えルールでは #
の後ろに数字を書いていることが多いですが、
これはルールの記述を短かくするためで、
わかりやすくなるよう英単語を使っても構いません。
_B_OP03 のように、名前が “_” で始まる仮想字句は 解析の過程で一時的に用いるもので、最終的には削除されます。 もしこの仮想字句が含まれている場合には、 想定外の要素を含む文が存在して、条件文を正しく解析できていないことを意味します。
RewriteTokens は、インスタンスを生成し、書換えルールを設定すると、 そのルールに従って書換えるフィルタオブジェクトになります。 例えば、次のように書くと、フィルタとして使うことができます。
#!/usr/bin/env perl
use RewriteTokens;
$rt = RewriteTokens->new(); # インスタンスを生成
$rt->load("hoge.rules"); # hoge.rules というファイルからルールを読み込む
$text = join('', <>); # 標準入力(またはコマンド引数で指定したファイル)から
# 字句系列を読み込み、変数 $text に保存
$text = $rt->rewrite($text); # 字句系列 $text を書換え
print $text; # 書換え結果を出力
書換えルールを設定するには、load()
の引数にファイル名を指定するか、 set_rules()
でルールを記述した文字列を指定します。
ルールが適用される戦略を変えるには生成時に以下のように記述します。
$rt = RewriteTokens->new()->seq(); # ルールを上から下に適用するだけ
$rt = RewriteTokens->new()->rep(); # ルールを上から下に適用することを繰り返す
これを実行するためには、RewriteTokens.pm を perl
が探し出して読み込めることが必要です。 実行前に環境変数
PERLLIB
を設定しておきましょう。
export PERLLIB=~/teba-X.YY/TEBA
これを .bashrc に書いておけば、いつでもこの設定が有効になります。
書換え内容によっては、対応関係がずれることがあり、 その場合は補正が必要になります。 補正は、以下の3つフィルタを生成し、各変換を適用することで実現できます。
$be = BeginEnd->new();
$co = BracketsID->new();
$fc = FixCma->new();
$text = $fc->conv($be->conv($co->conv($text)));
対応関係がずれるかどうかわからないときは、常に補正するようにすると良いでしょう。
TEBA は、プログラムの構文解析をするにあたり、書換えルールを用いています。 書換えルールは以下のファイルから構成され、順次適用されます。
粗粒度構文解析 - parser-stage1.rules - parser-stage2.rules - parser-condstmt.rules 名前空間解析 - namespace.rules | 式解析 - expr-base.rules | - expr-p01.rules | - expr-p02.rules | - expr-p03-12.rules | - expr-p13.rules | - expr-p14.rules |
最初は、parser-stage1.rule によって、文の切れ目を特定したり、 構造体/共用体/列挙体の定義の開始位置を特定するなど、解析の準備をします。 この段階では、仮想字句の対応関係のIDは使用しません。 また、初期化式を構成する中括弧も文として扱うなど、粗い解析をします。
次は、仮想字句の対応関係を用いて、文の構造を正確に識別していきます。 parser-stage2.rules は、関数とラベルの識別をしたあと、 制御構造を識別するための準備として、制御文の開始位置を特定します。 このとき、if文の else パートも、else で始まる制御構造として独立に扱います。 同様に、do-whie も do 文と while 文の2つで構成されているものとして扱います。 parser-condstmt.rules では、再帰的に制御文を識別していきます。 入れ子になっている制御文を内側から識別していく戦略を用いており、 if-else の構造も正確に識別することができます。 また、ラベルは、文の内部の要素ですが、最初はラベルを文の外に出しながら 多重に書かれたラベルも再帰的に解析し、そのあとで文の内部に入れ直します。
ラベルとタグは前後の文脈から明らかに区別がつくので、 まず、それらの種別をIDNから変更します。次に、 明らかに型の文脈で出現する識別子をIDNからID_TPに変更します。 メンバは、直前の演算子等から区別できるので、すべてID_MEに変更します。 この時点で、IDNになっている識別子については、 同名の識別子が型になっている場合にはすべて型(ID_TP)へ、 それ以外は変数・関数(ID_VF)に変更します。 ここでは、一般的に型と変数に同じ名前を使うことはないという前提に立っています。 一部のオープンソースでは、この前提が成り立たないものがあり、 改善が必要ですが、極めて限られています。
式は優先度の高い演算子から順次識別していくことで木構造をボトムアップ的に 構成していきます。ルールのファイル名の p01, p02 といった数字は優先度を表しており、 p03 から p12 はルールの形が同じになるので、1つのファイルにまとめています。
また、左結合の演算子と同じように右結合の演算子を定義すると、 大量のバックトラックが発生しやすいので、 右結合についてはルールを細分化するといった対策を行なっています。
ルールファイルを以下の点に着目しながら読んでみましょう。
- 仮想字句を削除するルールが存在するが、その理由は何か
- _B_IF など、一時的な仮想字句が存在する理由は何か
- 構造体はメンバの定義がある場合とない場合があるが、 それぞれどのように解析されているか
- if 文はどのようにして解析されていくか
次のプログラムを cparse.pl で解析し、どのような字句の構成になるか確認しなさい。 また、対応関係にある字句に同じ記号が割り振られていることを確認しなさい。
int a, b;
{
;
x y(1, 2, 3);
f= 1, b = 2;
a }
join-token.pl にはデバッグ支援機能があり、 仮想字句などの情報を残しながらプログラムを再構成する仕組みがあります。 次のように実行して確認してみましょう。 なお、ターミナルの設定によっては正常に見えないことがあります。
cparse.pl hoge.c | join-token.pl -d | less -R
プログラミングの演習などで自分が作成したプログラムや、 オープンソースを対象に解析し、どのようになるか確認をしてみましょう。
字句系列の書換えルールを書いたファイルを引数に指定すると、 そのルールに従って字句系列を書換えるフィルタを作成しなさい。 作成にあたっては、以下の記述を参考にすること。 また、実行する前に、環境変数 PERLLIB を適切に設定すること。
#!/usr/bin/env perl
use RewriteTokens;
use BeginEnd;
use BracketsID;
use FixCma;
# 引数に指定されたファイルの中身を変数 $rule に読み込む
die "No filename specified." if (@ARGV != 1);
$file = shift(@ARGV);
open(F, $file) || die "can't open file: $file.";
$rule = join('', <F>);
close(F);
# ここに書換えルールに基づいて書換える処理を書く
# (RewriteTokens の set_rule() でルールを設定し、rewrite() で書換える)
$be = BeginEnd->new();
$co = BracketsID->new();
$fc = FixCma->new();
$text = $fc->conv($be->conv($co->conv($text)));
print $text;
また、書換えルールを書いたファイルを用意すること。 以下に簡単な書換えルールの例を示す。
@SP => "(?:SP_\w+\s+<.*?>\n)*?"
{ $be#1:B_DE $int:'int' $sp:SP $var:ID_VF $semi:';' $en#1:E_DE }
=> { $be 'long':ID_TP $sp $var $semi $en }
RewriteTokens.pm を用いて、次の例のように2つ識別子を含む宣言を 1つの変数の宣言に分割するフィルタを作成しなさい。 (最低限、例が処理できれば良いが、できるだけ汎用的に作ること)
変更前:
char a, *b;
int f(int), g(char, int);
変更後:
char a; char *b;
int f(int); int g(char, int);
ルールを考えるにあたっては、実際に上記のプログラム断片がどのような字句になるか 確認しながら考えること。
「汎用的」とは、宣言される変数の数や引数の数などが異なっても正しく処理できること。
中括弧のない制御文に、中括弧を挿入する方法を考えて実装しなさい。 実装にあたっては、cparse.pl の出力である属性付き字句系列を入力として受け取り、 中括弧を挿入した字句系列を出力するフィルタ型のプログラムとして書くこと。 書換え後の字句系列をソースプログラムのテキストに変換するときは、 作成したフィルタと join-token.pl をパイプでつないで使うものとする。 RewriteTokens を使うことが望ましいが、必ずしも使わなくてもよい。
書換え結果に仮想字句の識別記号(ID)が適切に入っているか確認すること。
課題3-2をテストするために用意するべきプログラムはどのようなものか、 その基準を述べよ。また、if 文を例に、必要となる記述例をすべて列挙せよ。 また、課題3-2で作ったフィルタは、その記述例に対して、 適切に動いたかどうか結果を示せ。