LoginSignup
0

More than 3 years have passed since last update.

posted at

updated at

Organization

VBScriptを作ってみる(意味解析編・代入式まで)

前回は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)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0