文字列の表現
次に文字列を扱えるようにしてみましょう。Javaの文法と同様に「"」で囲んで表すようにします。
まず生成したパーサーが文字列内の日本語を処理できるようにUNICODE_INPUT
オプションを追加します。
//オプション定義 options{ STATIC=false; MULTI=true; VISITOR=true; NODE_EXTENDS="codezine.expr.BaseNode"; UNICODE_INPUT=true; }
文字列処理のためのトークンを追加します。
//文字列 TOKEN: { <STR_START: "\""> : IN_STR } <IN_STR> TOKEN: { <STR: ( (~["\"", "\\", "\n", "\r", "\t"]) | ("\\" (["n", "r", "t"])) )+> | <STR_END: "\""> : DEFAULT }
続いて文字列の生成規則を追加します。
void Value() #void: {} { Integer() | <LPAREN> AddExpr() <RPAREN> | String() } void String(): { Token t;} { <STR_START> t = <STR> { jjtThis.nodeValue = t.image;} <STR_END> }
Javaコードとして文字列リテラルのノードの処理を追加します。
/** 文字列リテラル */ public Object visit(ASTString node, Object data) { return node.nodeValue; }
今回の式言語では、文字列の連結に+演算子を使うことにします。また、足し算の処理で、どちらかの項が文字列だったときは文字列の連結になるようにします。こうすることでJava言語の文字列連結の場合と同様の処理になります。
/** 足し算 */ public Object visit(ASTAdd node, Object data) { Object left = node.jjtGetChild(0).jjtAccept(this, null); Object right = node.jjtGetChild(1).jjtAccept(this, null); if(left instanceof String || right instanceof String){ return left.toString() + right.toString(); } return (Integer)left + (Integer)right; }
こうしてant cc
タスク、ant compile
タスクで構築して実行すると、次のように文字列の処理ができます。
C:\java\product\CodeZine\build\classes>java codezine.expr.Expr "結果は" + (4 + 3) 結果は7
括弧で囲まないと次のようになります。Javaの構文と同じように、「+」演算子のどちらかの項が文字列だった場合には文字列の連結になるようにしているからです。
C:\java\product\CodeZine\build\classes>java codezine.expr.Expr "結果は" + 4 + 3 結果は43
字句解析の状態
文字列の中では「+」や「123」などをも単なる文字として扱うようにする必要があるため、「"」で囲まれている部分とそうではない部分で字句解析の処理を切り替える必要があります。そこでJavaCCの字句解析では、状態を指定することができます。今回の式言語では文字列の外と中で状態を切り替えることにします。「"」が現れると文字列として解釈する状態に遷移して、「+」や「123」などを特別なトークンとして扱わないようにし、再び「"」が現れると通常の字句解析の状態に戻すようにします。
トークンを処理したときの状態の遷移先を指定するには次のようにします。
<トークン名 : パターン> : 遷移先の状態名
ここでは、「"」を処理したときに状態IN_STR
に遷移するように指定しています。
<STR_START: "\""> : IN_STR
この状態IN_STR
での字句解析を指定するには、「TOKEN
」などの前に状態を指定します。
<状態名> TOKEN { ... }
ここでは、状態IN_STR
で処理するトークンを次のように指定しています。
<IN_STR> TOKEN: {
TOKENなどで状態を指定していない場合は、字句解析の状態は「DEFAULT
」になります。そこで、状態IN_STR
で閉じ側の「"」を処理した場合には状態DEFAULT
に遷移するようにしています。
| <STR_END: "\""> : DEFAULT
字句解析中の処理
ここまでの文字列処理では、構文として「"」を「\"」として文字列中に記述できるように定義をしていますが、実際には\を省く処理を行っていないため、「"」を表示しようとして「\"」と文字列中に記述しても「\"」になってしまっています。
C:\java\product\CodeZine\build\classes>java codezine.expr.Expr "\"4+3\"の結果は" + (4+3) \"4+3\"の結果は7
この「\」を省くために次のような一括処理をするのも、何か効率の悪い気がします。字句解析中に一度は「\"」であるかどうかを判定しているはずだからです。
/** 文字列リテラル */ public Object visit(ASTString node, Object data) { return node.nodeValue.replaceAll("\\\"", "\").replaceAll("\\\\", "\\"); }
今回のサンプルでは、まったく効率について考慮してないのですが、そうは言っても一度字句解析で判断しているものをもう1度判定するのはもったいない気がします。同じ処理が2度無駄に行われてしまうことは避けたいものです。そこで、字句解析中に特殊文字の処理を埋め込んでみます。字句解析中に処理を行うときには「MORE
」を使います。
//文字列 TOKEN: { <STR_START: "\""> : IN_STR } <IN_STR> MORE: { <~["\"", "\\", "\n", "\r", "\t"]> | <"\\\"" | "\\\\"> { image.deleteCharAt(image.length() - 2); } | <"\\n" | "\\r"> { image.delete(image.length() - 2, image.length()); image.append("\n");} | <"\\t"> { image.delete(image.length() - 2, image.length()); image.append("\t");} } <IN_STR> TOKEN: { <STR: "\"">{ image.deleteCharAt(image.length() - 1); matchedToken.image = image.toString(); } : DEFAULT }
文字列リテラルの生成規則からは閉じ側の「"」をはずします。閉じ側の「"」もトークンの処理中に取り除いたからです。
void String():
{ Token t;}
{
<STR_START>
t = <STR> { jjtThis.nodeValue = t.image;}
}
これで「\"」などの処理も行えるようになります。処理用のJavaコードを変更する必要はありません。
ant cc
タスクとant compile
タスクで構築して実行すると次のようになります。
C:\java\product\CodeZine\build\classes>java codezine.expr.Expr "\"4+3\"の結果は" + (4+3) "4+3"の結果は7
MOREの扱い方
MOREは字句解析中に処理を行うために使います。最初は理解のために文字列用トークンの定義からいろいろな処理をはずして純粋な構文定義の部分だけを見てみましょう。次のようになっています。
//文字列 TOKEN: { <STR_START: "\""> : IN_STR } <IN_STR> MORE: { <~["\"", "\\", "\n", "\r", "\t"]> | <"\\\"" | "\\\\"> | <"\\n" | "\\r"> | <"\\t"> } <IN_STR> TOKEN: { <STR: "\""> : DEFAULT }
MORE
で処理された文字列は次に現れたトークンに追加されます。ここではMORE
のあとにはSTR
トークンが続くことになるので、MORE
の中で現れた文字列はSTR
トークンに組み入れられます。つまり、「"」が現れるとIN_STR
状態になり、MORE
として処理が続き、「"」が現れるとMORE
で処理された文字列が追加されてSTR
トークンとして扱われ、DEFAULT
状態に戻るということになります。閉じ側の「"」がSTR
トークンとして扱われていることに注意してください。
それではMORE
の中で指定している文字クラスを一つずつ見てみましょう。最初の定義では、通常の文字を指定しています。つまり「"」「\」やタブ文字改行文字以外の文字です。ここでは特別な処理をする必要はありません。
<~["\"", "\\", "\n", "\r", "\t"]>
次は「\"」と「\\」です。これらの文字列が現れたときに、最初の\をはずす処理を行います。
| <"\\\"" | "\\\\">
{ image.deleteCharAt(image.length() - 2); }
image
は、その時点で受け入れている文字列が入ったStringBuffer
オブジェクトです。このimage
の最後から2番目の文字を削除します。このときの最後から2番目の文字は\になっているはずです。
それから改行文字をあらわす「\n」「\r」の処理です。今回は「\n」「\r」をはずして代わりに改行文字を追加しています。
| <"\\n" | "\\r"> { image.delete(image.length() - 2, image.length()); image.append("\n");}
最後にタブ文字をあらわす「\t」の処理です。「\t」をはずして代わりにタブ文字を追加しています。
| <"\\t"> { image.delete(image.length() - 2, image.length()); image.append("\t");}
ここで注意しないといけないことは、image
オブジェクトに収められているのは一時的な処理のための文字列で、このオブジェクトを操作しただけでは実際の解釈結果としては扱われないことです。ここまでの操作を実際の構文解釈で扱うには、image
オブジェクトに収められている文字列を、トークンの処理でmatchedToken
のimage
フィールドに保存する必要があります。
今回のサンプルでは、閉じ側の「"」で文字列の保存を行っています。また、そのとき「"」の削除も行っています。
<STR: "\"">{
image.deleteCharAt(image.length() - 1);
matchedToken.image = image.toString();
} : DEFAULT
まとめ
少しだけ実用的なサンプルができました。次回はいよいよスクリプト言語の作成です。