LoginSignup
3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-12

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

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
  3. You can use dark theme
What you can do with signing up
3
0