前回はVBScriptの簡単なソースコードを構文解析して抽象構文木(AST)を作りました。
今回はそのASTをパースすることでソースコードの実行をしようと思います。
とりあえず、前回パースしたASTを再掲します。
#<AST::Program:0x00007f7f2a81f280
@children=
[#<AST::Dim:0x00007f7f2a81f2f8
@var_names=
[#<AST::Identifier:0x00007f7f2a81ff00 @identifier="a">,
#<AST::Identifier:0x00007f7f2a81f6e0 @identifier="b">]>,
#<AST::AssignStatement:0x00007f7f2a81ece0
@children=
[#<AST::LeftExpr:0x00007f7f2a81f190
@children=[],
@identifier=#<AST::Identifier:0x00007f7f2a81f1e0 @identifier="a">>,
#<AST::NumberLiteral:0x00007f7f2a81edd0 @value=10>]>,
#<AST::AssignStatement:0x00007f7f2a81da98
@children=
[#<AST::LeftExpr:0x00007f7f2a81eb50
@children=[],
@identifier=#<AST::Identifier:0x00007f7f2a81eba0 @identifier="b">>,
#<AST::BinaryExpr:0x00007f7f2a81db10
@children=
[#<AST::BinaryExpr:0x00007f7f2a81e038
@children=
[#<AST::LeftExpr:0x00007f7f2a81e600
@children=[],
@identifier=
#<AST::Identifier:0x00007f7f2a81e650 @identifier="a">>,
#<AST::NumberLiteral:0x00007f7f2a81e290 @value=1>],
@operator="+">,
#<AST::NumberLiteral:0x00007f7f2a81dc50 @value=3>],
@operator="*">]>]>
このAST上の各クラスに対して、 evel
メソッドを実行することでASTの実行を行います。
evalはプログラムの実行途中の状態(変数の値とか、宣言済みの関数とか)を格納したオブジェクトを引数にとり、自分の子要素のevalを行うときにそれを渡します。
というわけで、変数や関数(将来的に)を入れるための器であるEnvironmentクラスを作ります。
class Environment
attr_accessor :option_explicit
def initialize(option_explicit)
@option_explicit = option_explicit
@values = {}
end
def add_variable(name)
name = name.downcase
@values[name] = nil
end
def [](name)
@values[name]
end
def []=(name, value)
name = name.downcase
if @option_explicit && !@values.include?(name) && !value.is_a?(Procedure)
raise "unknown variable: #{name}"
end
@values[name] = value
end
このEnvironmentクラスのオブジェクトに対する操作として、evalを実装していきます。
最初にNumberLiteral(数値リテラル)のevalです。
ソースコード中の1や2などの数値リテラルがこれに対応します。
数値リテラルはいかなる時にも同じ値をとるため、environmentによってevalの結果が変わるということはありません。
class NumberLiteral < Leaf
def initialize(value)
@value = value
end
def eval(environment)
@value
end
end
次にIdentifier(識別子)のevalです。
aやbなどの変数がこのクラスに対応します。
environmentから値を取り出し、それをevalの結果とすることで、変数の参照機能が実現されています。
また、未定義変数を参照しようとしたときにはエラーになります。
class Identifier < Leaf
attr_reader :identifier
def initialize(identifier)
@identifier = identifier
end
def eval(environment)
raise "unknown variable: #{@identifier}" if !environment.key?(@identifier)
environment[@identifier]
end
end
次はBinaryExpr(二項式)です。
+や-などがこのクラスに対応します。
色々とごちゃごちゃやってますが、最初に左辺と右辺のそれぞれをevalしています。
そして、演算子の種類(+,-,*,/, etc.)に応じて左辺と右辺に対して計算を行い、その結果をevalの結果にしています。
VBScriptには短絡評価をする演算子はないため、最初に右辺左辺の評価を行っています。
class BinaryExpr < List
def initialize(operator, arg1, arg2)
@operator = operator
super([arg1, arg2])
end
def eval(environment)
left = child(0).eval(environment)
right = child(1).eval(environment)
case @operator
when '+'
add(left, right)
when '-'
sub(left, right)
# 他の二項演算子も同様に
else
raise "unknown operator: #{@operator}"
end
def add(left, right)
if left.is_a?(EmptyObject) && right.is_a?(EmptyObject)
0
elsif left.is_a?(EmptyObject)
right
elsif right.is_a?(EmptyObject)
left
else
left + right
end
end
def sub(left, right)
left = left.coerce('integer') if left.is_a?(EmptyObject)
right = right.coerce('integer') if right.is_a?(EmptyObject)
left - right
end
end
いよいよ代入文です。
今までのAST要素とは違い、代入はenvironmentオブジェクトに対する更新操作を行います。
右辺のeval結果を左辺の識別子名でenvironmentオブジェクトに登録します。
ここで登録された結果は他のevalから読み出すことができるため、これによって計算結果を保存する機能(変数)が実現されています。
class AssignStatement < List
def initialize(arg1, arg2)
super([arg1, arg2])
end
def eval(environment)
left = child(0)
right = child(1)
environment[left.identifier] = right.eval(environment)
end
end
最後に、BlockStatementListです。
これは複数個の文が並んだ状態を表現しているAST要素です。
文を最初から順番に実行しています。
ある文のeval実行結果はenvironmentに対する変更として表現されており、それが次の文の実行のときに渡されます。
class BlockStatementList < List
def initialize
super([])
end
def eval(environment)
children.each do |child|
child.eval(environment)
end
end
end
これらの準備が出来たらプログラムの実行は簡単です。
空っぽの環境を作り、それをastの最上位要素のevalメソッドに渡せばあとは再帰的にいい感じに実行が始まります。
program = ARGF.read
parser = TinyVbsParser.new
environment = Environment.new(false)
ast = parser.scan(program)
ast.eval(environment)