では、VBScriptのインタープリターを作ってみましょう。
筆者がBASIC系言語に触れる経験がなかったため、VBScriptの言語構文が特殊に見え、そのため数々の罠にハマってきました。
言語を理解するためには、その言語のインタープリターを実装してみるのが良いと思ったので、試しにVBScriptのスモールセットのインタープリタを実装してみることにしました。
以下の機能が実装されていることを目標とします。
- 制御構文(If, While, For)
- 関数及びサブルーチン
逆に、以下の機能は間に合わなかったために、実装を見送りました。
- 配列
- オブジェクト指向
では、最初にVBScriptのソースコードを字句解析します。
字句解析はソースコードをトークン列に分解する操作で、多くの場合にインタープリターやコンパイラーの処理の最初に行われます。
例えば、(1)のVBScriptのコードを字句解析すると、(2)の結果が得られます。
Dim hoge, fuga ' 変数宣言
hoge = 1
fuga = (hoge + 1) * 2
[
"Dim",
"hoge",
",",
"fuga",
"¥n",
"hoge",
"=",
"1",
"¥n",
"fuga",
"=",
"(",
"hoge",
"+",
"1",
")",
"*",
"2"
"¥n",
]
空白やタブ文字及びコメントはこの時点で除去されます。
CやJavaなどの改行がホワイトスペース以上の意味を持たない言語では改行も除去されますが、VBScriptは改行が分の区切りという意味を持っているので、字句解析の結果に改行は含まれます。
ここで改行を除去してしまうと、その後の構文解析ができなくなってしまいます。
では、VBScriptを字句解析するためのプログラムを書きます。
字句解析をするためには、ソースコードを前方から読み、特定の正規表現にマッチする部分を切り出すという手法がよく行われます。
簡単なプログラムなので自分で作ってもいいですが、字句解析を支援するライブラリがあるので、それを使ってみます。
以下のrexicalはC言語で書かれた字句解析器としておなじみのlexのRuby版です。
raccという構文解析器(yaccのRuby版)とセットで使うことが想定されています。
VBScriptの文法規則とにらめっこすることによって、以下の字句解析ルールを得ることが出来ます。
class TinyVbsParser
macro
STRING_CHAR [^"]
DATE_CHAR [[\ -~]&&[^\#]]
ID_NAME_CHAR [[\ -~]&&[^\[\]]]
HEX_DIGIT [0-9a-f]
OCT_DIGIT [0-7]
WS [\ \t\f\v]
ID_TAIL [0-9a-z_]
COMMENT_LINE '|rem
rule
\r\n|\r|\n|: { [:NEWLINE, nil] }
{WS}+|_{WS}*\r?\n? { }
({COMMENT_LINE}).* { }
# Literal
"({STRING_CHAR}|"")*" { [:STRING_LITERAL, parse_string_literal(text)] }
\d*\.\d+(e[+-]?\d+)? { [:FLOAT_LITERAL, parse_float_literal(text)] }
\d+e[+-]?\d+ { [:FLOAT_LITERAL, parse_float_literal(text)] }
\d+ { [:INT_LITERAL, parse_int_literal(text)] }
&H{HEX_DIGIT}+&? { [:HEX_LITERAL, parse_hex_literal(text)] }
&{OCT_DIGIT}+&? { [:OCT_LITERAL, parse_oct_literal(text)] }
\#{DATE_CHAR}+# { [:DATE_LITERAL, text] }
# Reserved word
(option|explicit|dim|true|false|imp|eqv|xor|or|and|not|if|elseif|else|then|end|class|set|new|mod) { [text.capitalize, text.capitalize] }
(do|while|until|loop|wend|for|to|step|next|each|in|select|case|sub|function|call|public|private|byval|byref|const|null|empty) { [text.capitalize, text.capitalize] }
# Identifier
[a-z]{ID_TAIL}* { [:ID, text] }
\[{ID_NAME_CHAR}*\] { [:ID, parse_square_braketed_id(text)] }
# Symbol
= { [text.capitalize, text.capitalize] }
\+ { [text.capitalize, text.capitalize] }
- { [text.capitalize, text.capitalize] }
\* { [text.capitalize, text.capitalize] }
\/ { [text.capitalize, text.capitalize] }
\( { [text.capitalize, text.capitalize] }
\) { [text.capitalize, text.capitalize] }
\^ { [text.capitalize, text.capitalize] }
, { [text.capitalize, text.capitalize] }
>= { [text.capitalize, text.capitalize] }
<= { [text.capitalize, text.capitalize] }
<> { [text.capitalize, text.capitalize] }
> { [text.capitalize, text.capitalize] }
< { [text.capitalize, text.capitalize] }
& { [text.capitalize, text.capitalize] }
\. { [text.capitalize, text.capitalize] }
inner
def parse_string_literal(text)
text[1..-2].gsub('""', '"')
end
def parse_float_literal(text)
text.to_f
end
def parse_int_literal(text)
text.to_i
end
def parse_hex_literal(text)
text.gsub(/[&H]/i, '').to_i(16)
end
def parse_oct_literal(text)
text.gsub('&', '').to_i(8)
end
def parse_square_braketed_id(text)
text[1..-2]
end
end
他の言語にはあまり見られない、VBScriptに特徴的な字句解析ルールは以下のものです。
ホワイトスペース文字に改行が含まれない
macro
WS [\ \t\f\v]
rule
\r\n|\r|\n|: { [:NEWLINE, nil] }
VBScriptでは改行が意味を持つため、改行文字を空白やタブと同等に扱うことはしません。
改行文字一般は :NEWLINE
というシンボルに還元され、構文解析器に送られます。
文字列リテラル中の"の表現方法
rule
"({STRING_CHAR}|"")*" { [:STRING_LITERAL, parse_string_literal(text)] }
inner
def parse_string_literal(text)
text[1..-2].gsub('""', '"')
end
多くの言語では文字列リテラル中に "
を書くときにはそれをバックスラッシュでエスケープします \"
。
VBScriptでは "
を2つ重ねます。
[ ]で囲まれた識別子
rule
\[{ID_NAME_CHAR}*\] { [:ID, parse_square_braketed_id(text)] }
inner
def parse_square_braketed_id(text)
text[1..-2]
end
VBscriptでは識別子を []
で囲むことによって、識別子中に空白や記号を入れることができます。