ᵡᵿᵱᵮᵟ ᵣᶌᶅᶇᶌᶃ
5.12 構文解析機構
5.
require ’FuseRule.rb’
require ’ConditionRuby.rb’
require ’TaskRuby.rb’
require ’ActionRuby.rb’
require ’ActionRubyNotime.rb’
class TaskA< TaskRuby def define_task end
end
class Rule< FuseRule def instantiate end
end
図 5.17: スケルトンコード
5.12 構文解析機構
図 5.18: タスク名とタスククラスの関係
そのため,本プログラムにおいては,エディターとパーサーとの連携を容易にする ために動的ルール登録型パーサーDRRP(Dynamic Rule Register Parser)を独自に開発 して対応することとした.DRRPはルールを任意に追加可能であり,トップダウン型 の構文解析を実施し,構文木を生成する.
5.12.1 動的ルール登録型パーサー
本プログラムで開発したDRRPの特徴を示す.ルールは大きく三種類存在し,通常 ルール,特殊ルール,リカバリルールとなっている.通常ルールはパースのためのルー ルである.特殊ルールはコメント等に対応するためのルールであり,読み飛ばすトー クンを指定するためのルールである.リカバリルールは読み飛ばす指定をしたトーク ンを通常トークンに復帰させるためのルールである.
例えば,コメント等で「行末までコメント」というようなルールがあった場合,そ のコメントの行末の改行コードはコメントの一部であり,スキップするトークンとし て認識される.行ごとにコメントが存在する場合にはこのルールで問題ない.
一方,多くの言語では,行の前半がプログラムコードで,後半がコメントであるよ うな場合も存在する.このような場合,行末の改行コードはコメントの終端を意味す ると同時に,言語仕様としての行の終端も意味しており,改行が無くなることで構文 の意味が変わってしまう場合があり得る.そのような場合には改行をリカバリルール に指定することで問題を回避することができる.
5.
ルールの指定は,ルールクラスを定義し,名前を設定し,パーサーに登録するという 手順になる.ルールには名前を設定することができ,設定したルールごとにルールク ラスが生成される.ルールを組み合わせることで上位のルールを生成することができ,
その際には簡単な制御記号を付加することができる.例えば,ルール名の先頭に“ˆ’’を 付加すれば否定を表し,末尾に“?”を付加すれば0回または1回出現することを,”+”
を付加すれば1回以上の連続を,”*”であれば0回以上の連続を表す.図5.19は,型名 及び文字列の定義を例にしたルール記述の例である.
parser.addParseRuleToken("INT","int"); // intをINTと定義 parser.addParseRuleToken("VOID","void"); // voidをVOIDと定義
parser.addParseRuleToken("DQUOTE","\""); // ダブルクオーテーションをDQUOTEと定義
// 型をtypeという名前で定義
ParseRuleStatement type=new ParseRuleStatement("type");
type.addRule("INT");
type.addRule("VOID");
parser.addParseRule(type);
// 文字列をstringという名前で定義
ParseRuleStatement string=new ParseRuleStatement("string");
string.addRule("DQUOTE","^DQUOTE*","DQUOTE");
parser.addParseRule(string);
図 5.19: DRRPにおける型名及び文字列の定義
パースルールを記述する例として,C言語の基本的なコードとして広く利用される
Hello Worldをパースすることを考える.パースルールを記述した例を図5.29に示す.
ここで定義されたパースルールはC言語のパーサーとして機能するものではなく,Hello
Worldを解釈するための,C言語の極めて限定されたサブセットである.実際のパー
ス対象は,C言語の教科書等に多く登場する下記の図5.20に示すコードである.
このコードを本章付録の図5.29に示すルールによって解釈した結果は図5.21のよう になる.
ここで,図5.21に示した構文の場合はHello Worldを形式的にパースしただけであ るため,引数構文argumentは左括弧“(”を検出してから右括弧“)”を検出するまでの 区間として定義されており,その内部にどのようなトークンがあってもエラーが発生
5.12 構文解析機構
#include "stdio.h"
/* これをパースする */
int main(){
printf("hello world !");
}
図 5.20: 一般的なHello Worldのソースコード
しない.これを修正する場合,以下の図5.22のようにパーサーのルールを書き換える ことにより,引数構文には引数なしか,あるいはコンマで区切られた1つ以上の項を 引数としてとることを定義できる.
ルールを書き換えると,引数構文の定義が書き換わる.その状態で再度パースする
と,printfは図5.23のように解釈されるようになる.本研究で開発したパーサーはパー
サーのプログラムを生成するわけではなく,ルールの登録によってパーサーを定義し ているため,プログラムの動作中にも容易にルールを変更することができ,高い拡張 性を有している.
5.12.2 構文ファンクション
DRRPはJavaで記述され,オブジェクト指向の考え方を前提としている.そのため,
木を構成する構文は全てオブジェクトとして扱われる.言語解析とオブジェクト化を 容易にするため,DRRPでは構文に機能クラスを設定することができる.
機能クラスは構文の種別ごとに設定することが可能であり,たとえば「代入構文の 機能クラス」,「メソッド呼び出し構文の機能クラス」という形で設定することができ る.後述する変数追跡等のエディターの機能も,DRRPにおける機能クラスの仕組み を利用して実装している.
5.12.3 動的言語におけるエラー判定及び入力補助
CaSPAは,ルールの設計を容易にするため,行動判断ルールのスクリプト言語によ
る記述が可能となっており,現在JRuby言語による記述機能が実装されている.
Ruby言語は動的言語に分類される言語であり,明示的な型宣言を行わないことが特
5.
ƉƌŽŐƌĂŵ
/ŶĐůƵĚĞΎ ĚĞĨŝŶĞ&ƵŶĐƚŝŽŶн
ƐƚƌŝŶŐ
^,ZW /E>h
η ŝŶĐůƵĚĞ
hKd
͞
ΔhKdΎ
ΔhKd ƐƚĚŝŽ͘Ś
hKd
͞
ƚLJƉĞ /Ed/&/Z ĂƌŐƵŵĞŶƚ /Ed ŵĂŝŶ
ŝŶƚ
ĨƵŶĐƚŝŽŶůŽĐŬ
; Δ;Ύ Ϳ
; EŽŵĂƚĐŚ Ϳ
ĨƵŶĐƚŝŽŶĂůů
/Ed/&/Z ƉƌŝŶƚĨ
ĂƌŐƵŵĞŶƚ
; Δ;Ύ Ϳ
; Ϳ
͖
͖
Δ; Δ;
,ĞůůŽ tŽƌůĚ Δ;
͊
図 5.21: Hello Worldの構文木
徴である.これによって動的言語はCやJavaのような静的言語に比べて大きな柔軟性 を獲得することが可能となったが,同時に変数の型が実行時まで定まらないため,エ ラーの検出,入力補助等が難しいという問題も同時に発生した.
その一方で,Eclipseに代表される近年の統合開発環境(Integrated Developing
Envi-ronment:IDE)においては,Javaのような静的言語に対しては強力なエラー検出及び入
力補助を行うことが可能であり,これらの機能はプログラムの開発効率の向上に大き く貢献している.そのため,本プログラムにおいても,入力補助は重要であり,その 実現方法について検討を行った.
本プログラムでは,構造を解析する際に構文解析を行うため,文法エラーを提示す ることは難しくない.動的言語の型を静的に評価することが難しいことは,動的言語 の本質的な問題であり,これは解決することが困難である.一部のRuby用統合開発環 境ではコードを部分的に実行しながら型を特定するという方式を採用している.
動的言語に対して,静的言語と同じ水準でのエラー検出は困難であるものの,コー ドに含まれる式の中に既知の型を発見し,それを利用して型推定を実施する方法につ
5.12 構文解析機構
parser.addParseRuleToken(",", ","); // コンマを定義
ParseRuleStatement term=new ParseRuleStatement("term"); // 項を定義 term.addRule("IDENTIFIER");
term.addRule("string");
parser.addParseRule(term);
ParseRuleStatement cTerm=new ParseRuleStatement("commaTerm"); // ","と項を定義 cTerm.addRule(",", "IDENTIFIER");
parser.addParseRule(cTerm);
argument.clearRule(); // 引数構文のルールを一度クリア
argument.addRule("(",")"); // 引数なしの場合
argument.addRule("(","term","commaTerm*",")"); // 引数がある場合
図 5.22: Hello Worldを解析するためのルールの記述 いては先行研究が存在する [76].
本研究では,動的言語はJavaと結合されるため,Javaとのインターフェースを手掛 かりにして型推定を行い,それによってエラー判定を行うこととした.
動的言語における型推定の困難性
Javaは静的に型付けされる言語であり,変数の型は一意に定まっている.そのため,
変数の型はソースコードの時点において確定され,開発環境等において誤ったメソッ ド呼び出し,不正な代入等の検出が容易であり,また型が定まることからメソッド呼 び出しの補完等も実現しやすい.
一方,動的型付けの言語の場合は,変数の型をソースコードから確定するのは原理 的に不可能な場合がある.Rubyで記述されたソースコードの一例を以下の図5.24に 示す.
このソースコードでは,ClassAとClassBは互いに継承関係の無い全く独立したクラ スである.しかしながら,同名のメソッドechoを有することから,Rubyにおいては
変数testがClassA型であってもClassB型であってもエラーを出力することなく実行
できる.この柔軟性は動的型付け言語の武器であるが,同時にバグの要因にもなりう る.そして,変数testがClassA型なのかClassB型なのかは乱数によって決まるため,
5.
ĨƵŶĐƚŝŽŶĂůů
/Ed/&/Z ƉƌŝŶƚĨ
ĂƌŐƵŵĞŶƚ
; Δ;Ύ Ϳ
; Ϳ
͖
͖
Δ; Δ;
,ĞůůŽ tŽƌůĚ Δ;
͊
ĨƵŶĐƚŝŽŶĂůů
/Ed/&/Z ƉƌŝŶƚĨ
ĂƌŐƵŵĞŶƚ
; Ϳ
; Ϳ
͖
͖ ƚĞƌŵ
ƐƚƌŝŶŐ hKd
͞
ΔhKdΎ ΔhKd ,ĞůůŽtŽƌůĚ͊
hKd
͞
䝹䞊䝹ኚ᭦๓ 䝹䞊䝹ኚ᭦ᚋ
図 5.23: ルール変更による構文木の変化
class ClassA def echo
p "This is ClassA"
end end
class ClassB def echo
p "This is ClassB"
end end
test=ClassA.new if (rand(2)==0) then
test=ClassB.new end
test.echo
図 5.24: 動的型付けの特徴
5.12 構文解析機構 このソースコードから読み取ることはできない.
ただし,本プログラムはJavaで記述されたエージェントを動作させるためのもので あり,必然的にRubyスクリプト上からJavaのメソッド呼び出しが行われる.Javaの メソッドを呼び出した場合,当然ながら返り値の型は確定する.そのため,それを手 がかりにして型推定を行うことができる.
Javaメソッド呼び出しを基点とした型推定と変数追跡
本プログラムでは,型推定の対象はJavaのクラス型と数値型,文字列型とした.こ れは,Rubyクラスを推定対象とした場合,統一的な推定アルゴリズムにまとめるこ とが難しくなること.またコードをエディターで直接入力する部分の多くにJavaのメ ソッド呼び出しが関わってくることが理由である.
型推定の方法は,基本的に上から下に順番に解析をかけていく.当初,全ての変数 の型は不確定とみなされている.これに対して即値または変数の代入によって型を推 定していく.
本プログラムによる単純な型推定のケースを図5.25に示す.
図 5.25: 単純なケースによる型推定
このケースでは,変数aは即値として150が代入されていることから,整数型であ ると推定される.bは,整数型と整数型の演算であるから,整数型である.cは,即値 として文字列が代入されているために文字列型である.そして,dへの代入の際に,文 字列と整数の乗算をしようとしていることが判明したので,ここでエラーが発生する.
Javaメソッド呼び出しを基点として型推定を実施する場合を図5.26に示す.ここで,
get agentはCaSPAの組み込みメソッドであり,このルールの対象となるエージェン
トオブジェクトを取得することができる.