<commit_before>module Furnace::AVM2
  class Decompiler
    include Furnace::AST
    include Furnace::AVM2::ABC
    include Furnace::AVM2::Tokens

    class ExpressionNotRecognized < StandardError
      attr_reader :context, :opcode

      def initialize(context, opcode)
        @context, @opcode = context, opcode
      end
    end

    def initialize(body, options)
      @body, @method, @options = body, body.method, options.dup
      @closure = @options.delete(:closure)
    end

    ActivationPrologue = Matcher.new do
      [:begin,
        [:push_scope,
          [:get_local, 0]],
        [:set_local, -1,
          [:new_activation]],
        [:set_local, capture(:activation_local),
          [:get_local, -1]],
        [:push_scope,
          [:get_local, -1]],
        skip
      ]
    end

    RegularPrologue = Matcher.new do
      [:begin,
        [:push_scope,
          [:get_local, 0]],
        skip
      ]
    end

    def decompile
      begin
        @locals = Set.new([0]) + (1..@method.param_count).to_a
        @scopes = []

        @nf = @body.code_to_nf

        if captures = ActivationPrologue.match(@nf)
          @closure_slots = {}
          @body.slot_traits.each do |trait|
            @closure_slots[trait.idx] = trait
          end

          @closure_locals = Set.new

          @nf.children.slice! 0...4
        else
          if RegularPrologue.match @nf
            @nf.children.slice! 0...1
          else
            # No prologue; probably a closure.
          end
        end

        @global_slots = @options[:global_slots]

        stmt_block @nf,
          function: !@options[:global_code],
          closure:  @closure

      rescue Exception => e
        @failed = true

        comment = "'Ouch!' cried I, then died.\n" +
          "#{e.class}: #{e.message}\n" +
          "#{e.backtrace[0..5].map { |l| "    #{l}\n"}.join}" +
          "      ... et cetera\n"

        token(ScopeToken, [
          token(CommentToken, comment)
        ], function: !@options[:global_code],
           closure:  @closure)

      ensure
        if stat = @options[:stat]
          stat[:total] += 1

          if @failed
            stat[:failed]  += 1
          elsif @partial
            stat[:partial] += 1
          else
            stat[:success] += 1
          end
        end
      end
    end

    # Statements

    def stmt_block(block, options={})
      nodes = []

      block.children.each do |opcode|
        if respond_to?(:"stmt_#{opcode.type}")
          send :"stmt_#{opcode.type}", opcode, nodes
        else
          catch(:skip) do
            nodes << token(StatementToken, [
              handle_expression(opcode)
            ])
          end
        end
      end

    rescue ExpressionNotRecognized => e
      @partial = true

      comment = "Well, this is embarassing.\n\n" +
        "Expression recognizer failed at:\n" +
        "#{e.opcode.inspect}\n"

      if e.context != e.opcode
        comment << "\nOpcode at the top of stack:\n" +
          "#{e.context.inspect}\n"
      end

      nodes << CommentToken.new(@body, comment, @options)

    ensure
      if $!.nil? || $!.is_a?(ExpressionNotRecognized)
        return token(ScopeToken, nodes, options)
      end
    end

    def stmt_if(opcode, nodes)
      condition, if_true, if_false = opcode.children

      nodes << token(IfToken, handle_expression(condition),
        stmt_block(if_true, continuation: !if_false.nil?))
      nodes << token(ElseToken,
        stmt_block(if_false)) if if_false
    end

    def stmt_label(opcode, nodes)
      name, = opcode.children

      nodes << token(LabelDeclarationToken, name)
    end

    def stmt_while(opcode, nodes)
      condition, body = opcode.children

      nodes << token(WhileToken, handle_expression(condition),
        stmt_block(body))
    end

    def stmt_for(opcode, nodes)
      value_reg, value_type, object_reg, body = opcode.children

      @locals.add(value_reg)

      if opcode.type == :for_in
        klass = ForToken
      elsif opcode.type == :for_each_in
        klass = ForEachToken
      end

      if @closure_slots
        name = token(VariableNameToken, @closure_slots[value_reg].name.name)
      else
        name = token(VariableNameToken, local_name(value_reg))
      end

      nodes << token(klass,
        token(InToken, [
          token(LocalVariableToken, [
            name,
            type_token(value_type)
          ]),
          token(VariableNameToken, local_name(object_reg)),
        ]),
        stmt_block(body))
    end
    alias :stmt_for_in      :stmt_for
    alias :stmt_for_each_in :stmt_for

    def stmt_break(opcode, nodes)
      nodes << token(ReturnToken, exprs(opcode.children))
    end

    def stmt_continue(opcode, nodes)
      nodes << token(ContinueToken, exprs(opcode.children))
    end

    def stmt_throw(opcode, nodes)
      nodes << token(ThrowToken, exprs(opcode.children))
    end

    def stmt_return(opcode, nodes)
      nodes << token(ReturnToken, exprs(opcode.children))
    end
    alias :stmt_return_value :stmt_return
    alias :stmt_return_void  :stmt_return

    def stmt_push_scope(opcode, nodes)
      if @options[:global_code]
        @scopes.push opcode.children.first
      else
        raise "pushscope in nonglobal code"
      end
    end

    def stmt_pop_scope(opcode, nodes)
      if @options[:global_code]
        @scopes.pop
      else
        raise "popscope in nonglobal code"
      end
    end

    # Expressions

    def handle_expression(opcode)
      expression(opcode)
    rescue ExpressionNotRecognized => e
      raise ExpressionNotRecognized.new(opcode, e.opcode)
    end

    def expression(opcode)
      handler = :"expr_#{opcode.type}"
      if respond_to?(handler) && node = send(handler, opcode)
        node
      else
        raise ExpressionNotRecognized.new(nil, opcode)
      end
    end
    alias :expr :expression

    def expressions(opcodes)
      opcodes.map do |opcode|
        expression opcode
      end
    end
    alias :exprs :expressions

    ## Immediates

    def expr_imm(opcode)
      case opcode.type
      when :integer, :double
        token(ImmediateToken, *opcode.children)
      when :null, :undefined, :true, :false
        token(ImmediateToken, opcode.type)
      end
    end
    alias :expr_integer   :expr_imm
    alias :expr_double    :expr_imm
    alias :expr_null      :expr_imm
    alias :expr_undefined :expr_imm
    alias :expr_true      :expr_imm
    alias :expr_false     :expr_imm

    def expr_string(opcode)
      string, = opcode.children
      token(ImmediateToken, string.inspect)
    end

    def expr_nan(opcode)
      token(ImmediateToken, "NaN")
    end

    def expr_new_array(opcode)
      token(ArrayToken, exprs(opcode.children))
    end

    def expr_new_object(opcode)
      token(ObjectToken, opcode.children.
        each_slice(2).map do |key, value|
          token(ObjectPairToken, [
            expr(key),
            expr(value)
          ])
        end)
    end

    ## Locals

    def local_name(index)
      if index < 0
        "sp#{-index}"
      elsif index == 0
        if @options[:static]
          @options[:instance].name.name
        else
          "this"
        end
      elsif index <= @method.param_count
        if @method.has_param_names?
          @method.param_names[index - 1]
        else
          "param#{index - 1}"
        end
      else
        "local#{index - @method.param_count - 1}"
      end
    end

    def expr_get_local(opcode)
      index, = opcode.children
      token(VariableNameToken, local_name(index))
    end

    ActivationGetSlot = Matcher.new do
      [:get_slot,
        capture(:index),
        [:get_scope_object, 1]]
    end

    GlobalGetSlot = Matcher.new do
      [:get_slot,
        capture(:index),
        either[
          [:get_global_scope],    # normalized form
          [:get_scope_object, 0]  # not emitted by ASC
        ]]
    end

    def expr_get_slot(opcode)
      if @closure_slots && captures = ActivationGetSlot.match(opcode)
        # treat as a local variable
        slot = @closure_slots[captures[:index]]
        token(VariableNameToken, slot.name.name)
      elsif captures = GlobalGetSlot.match(opcode)
        # treat as a global property
        if @global_slots
          slot = @global_slots[captures[:index]]
          get_name(nil, slot.name.to_astlet)
        else
          token(PropertyNameToken,
            "$__GLOBAL_#{captures[:index]}")
        end
      end
    end

    IMMEDIATE_TYPE_MAP = {
      :any       => '*',
      :integer   => 'int',
      :unsigned  => 'uint',
      :string    => 'String',
      :double    => 'Number',
      :object    => 'Object',
      :bool      => 'Boolean',
      :true      => 'Boolean',
      :false     => 'Boolean',
    }

    def type_token(type)
      if IMMEDIATE_TYPE_MAP.include?(type)
        token(TypeToken, [
          token(ImmediateTypenameToken, IMMEDIATE_TYPE_MAP[type])
        ])
      else
        token(TypeToken, [
          token(MultinameToken, type.metadata[:origin])
        ])
      end
    end

    XmlLiteralPreMatcher = Matcher.new do
      [:coerce, [:q, "XML"], any]
    end

    def expr_set_var(name, value, type, declare)
      if declare
        token(LocalVariableToken, [
          token(VariableNameToken, name),
          type,
          token(InitializationToken, [
            expr(value)
          ])
        ])
      else
        token(AssignmentToken, [
          token(VariableNameToken, name),
          expr(value)
        ])
      end
    end

    def expr_set_local(opcode)
      index, value = opcode.children
      if IMMEDIATE_TYPE_MAP.include?(value.type)
        type = token(TypeToken, [
          token(ImmediateTypenameToken, IMMEDIATE_TYPE_MAP[value.type])
        ])
      elsif XmlLiteralPreMatcher.match value
        # XML literals work through expr_coerce
        type  = type_token(value.children.first)
        value = value
      elsif value.type == :coerce || value.type == :convert
        # Don't emit spurious coercion; typed variables already
        # imply it
        type  = type_token(value.children.first)
        value = value.children.last
      end

      expr_set_var(local_name(index), value, type, !@locals.include?(index))
    ensure
      @locals.add index if index
    end

    ActivationSetSlot = Matcher.new do
      [:set_slot,
        capture(:index),
        [:get_scope_object, 1],
        capture(:value)]
    end

    GlobalSetSlot = Matcher.new do
      [:set_slot,
        capture(:index),
        either[
          [:get_global_scope],    # normalized form
          [:get_scope_object, 0]  # not emitted by ASC
        ],
        capture(:value)]
    end

    def expr_set_slot(opcode)
      if @closure_slots && captures = ActivationSetSlot.match(opcode)
        # treat as a local variable
        index, value = captures.values_at(:index, :value)
        slot = @closure_slots[index]

        type = type_token(slot.type.to_astlet) if slot.type
        expr = expr_set_var(slot.name.name, value, type,
              !@closure_locals.include?(index))
        @closure_locals.add index

        expr
      elsif captures = GlobalSetSlot.match(opcode)
        # treat as a global property
        index, value = captures.values_at(:index, :value)

        if @global_slots
          slot = @global_slots[index]
          name = get_name(nil, slot.name.to_astlet)
        else
          token(PropertyNameToken, "$__GLOBAL_#{index}")
        end

        token(AssignmentToken, [
          name,
          expr(value)
        ])
      end
    end

    ## Arithmetics

    INPLACE_OPERATOR_MAP = {
      :inc_local   => :"++",
      :inc_local_i => :"++",
      :dec_local   => :"--",
      :dec_local_i => :"--",
    }

    def expr_inplace_arithmetic(opcode)
      token(UnaryPostOperatorToken, [
        token(VariableNameToken, local_name(*opcode.children)),
      ], INPLACE_OPERATOR_MAP[opcode.type])
    end

    alias :expr_inc_local   :expr_inplace_arithmetic
    alias :expr_inc_local_i :expr_inplace_arithmetic
    alias :expr_dec_local   :expr_inplace_arithmetic
    alias :expr_dec_local_i :expr_inplace_arithmetic

    PSEUDO_OPERATOR_MAP = {
      :increment   => [:"+", 1],
      :increment_i => [:"+", 1],
      :decrement   => [:"-", 1],
      :decrement_i => [:"-", 1],
    }

    def expr_pseudo_arithmetic(opcode)
      inside = expr(*opcode.children)
      inside = token(ParenthesesToken, [inside]) if inside.complex?

      operator, other_value = PSEUDO_OPERATOR_MAP[opcode.type]
      token(BinaryOperatorToken, [
        inside,
        token(ImmediateToken, other_value)
      ], operator)
    end

    alias :expr_increment   :expr_pseudo_arithmetic
    alias :expr_increment_i :expr_pseudo_arithmetic
    alias :expr_decrement   :expr_pseudo_arithmetic
    alias :expr_decrement_i :expr_pseudo_arithmetic

    OPERATOR_MAP = {
      :and         => :"&&",
      :or          => :"||",

      :add         => :"+",
      :add_i       => :"+",
      :subtract    => :"-",
      :subtract_i  => :"-",
      :multiply    => :"*",
      :multiply_i  => :"*",
      :divide      => :"/",
      :modulo      => :"%",
      :multiply    => :"*",
      :negate      => :"-",
      :negate_i    => :"-",

      :!           => :!,
      :>           => :>,
      :>=          => :>=,
      :<           => :<,
      :<=          => :<=,
      :==          => :==,
      :===         => :===,
      :!=          => :!=,
      :"!=="       => :"!==",

      :bit_and     => :"&",
      :bit_or      => :"|",
      :bit_xor     => :"^",
      :bit_not     => :"~",
      :lshift      => :"<<",
      :rshift      => :">>",
      :urshift     => :">>>",
    }

    def expr_arithmetic(opcode)
      if OPERATOR_MAP.include?(opcode.type)
        insides = parenthesize_each(exprs(opcode.children))

        if insides.count == 1
          token(UnaryOperatorToken, insides, OPERATOR_MAP[opcode.type])
        elsif insides.count == 2
          token(BinaryOperatorToken, insides, OPERATOR_MAP[opcode.type])
        else
          token(CommentToken, "Unexpected #{insides.count}-ary operator")
        end
      end
    end

    alias :expr_and         :expr_arithmetic
    alias :expr_or          :expr_arithmetic

    alias :expr_add         :expr_arithmetic
    alias :expr_add_i       :expr_arithmetic
    alias :expr_subtract    :expr_arithmetic
    alias :expr_subtract_i  :expr_arithmetic
    alias :expr_multiply    :expr_arithmetic
    alias :expr_multiply_i  :expr_arithmetic
    alias :expr_divide      :expr_arithmetic
    alias :expr_modulo      :expr_arithmetic
    alias :expr_negate      :expr_arithmetic

    alias :expr_!           :expr_arithmetic
    alias :"expr_>"         :expr_arithmetic
    alias :"expr_>="        :expr_arithmetic
    alias :"expr_<"         :expr_arithmetic
    alias :"expr_<="        :expr_arithmetic
    alias :"expr_=="        :expr_arithmetic
    alias :"expr_==="       :expr_arithmetic
    alias :"expr_!="        :expr_arithmetic
    alias :"expr_!=="       :expr_arithmetic

    alias :expr_bit_and     :expr_arithmetic
    alias :expr_bit_or      :expr_arithmetic
    alias :expr_bit_xor     :expr_arithmetic
    alias :expr_bit_not     :expr_arithmetic
    alias :expr_lshift      :expr_arithmetic
    alias :expr_rshift      :expr_arithmetic
    alias :expr_urshift     :expr_arithmetic

    def expr_in(opcode)
      token(InToken, parenthesize_each(exprs(opcode.children)))
    end

    ## Properties and objects

    This = Matcher.new do
      [:get_local, 0]
    end

    PropertyGlobal = Matcher.new do
      [any,
        either_multi[
          [
            [ either[:find_property, :find_property_strict],
                  capture(:multiname)],
            backref(:multiname),
          ],
          [
            [:find_property_strict,
              [:m, [:set, "*"], any]],
            capture(:multiname)
          ],
          [
            [:get_scope_object, 0],
            capture(:multiname)
          ],
        ],
        capture_rest(:arguments)]
    end

    def expr_get_lex(opcode)
      multiname, = opcode.children
      get_name(nil, multiname)
    end

    def expr_get_property(opcode)
      if captures = PropertyGlobal.match(opcode)
        get_name(nil, captures[:multiname])
      else
        subject, multiname, = opcode.children
        get_name(expr(subject), multiname)
      end
    end

    def expr_get_super(opcode)
      subject, multiname, = opcode.children

      stmt = get_name(token(SuperToken), multiname)

      unless This.match subject
        stmt = token(SupplementaryCommentToken,
          "subject != this: #{subject.inspect}",
          [ stmt ])
      end

      stmt
    end

    def expr_set_property(opcode)
      if captures = PropertyGlobal.match(opcode)
        token(AssignmentToken, [
          get_name(nil, captures[:multiname]),
          expr(*captures[:arguments])
        ])
      else
        subject, multiname, value, = opcode.children
        token(AssignmentToken, [
          get_name(expr(subject), multiname),
          expr(value)
        ])
      end
    end
    alias :expr_init_property :expr_set_property

    def expr_set_super(opcode)
      subject, multiname, value = opcode.children

      stmt = token(AssignmentToken, [
        get_name(token(SuperToken), multiname),
        expr(value)
      ])

      unless This.match subject
        stmt = token(SupplementaryCommentToken,
          "subject != this: #{subject.inspect}",
          [ stmt ])
      end

      stmt
    end

    def expr_delete_property(opcode)
      subject, multiname, = opcode.children
      token(DeleteToken, [get_name(expr(subject), multiname)])
    end

    def expr_do_property(opcode, klass)
      if captures = PropertyGlobal.match(opcode)
        token(klass, [
          get_name(nil, captures[:multiname]),
          token(ArgumentsToken, exprs(captures[:arguments]))
        ])
      else
        subject, multiname, *args = opcode.children
        token(klass, [
          get_name(expr(subject), multiname),
          token(ArgumentsToken, exprs(args))
        ])
      end
    end

    def expr_call_property(opcode)
      expr_do_property(opcode, CallToken)
    end

    def expr_call_super(opcode)
      subject, multiname, *args = opcode.children
      if This.match subject
        token(CallToken, [<commit_msg>Implement callproplex in decompiler.<commit_after>module Furnace::AVM2
  class Decompiler
    include Furnace::AST
    include Furnace::AVM2::ABC
    include Furnace::AVM2::Tokens

    class ExpressionNotRecognized < StandardError
      attr_reader :context, :opcode

      def initialize(context, opcode)
        @context, @opcode = context, opcode
      end
    end

    def initialize(body, options)
      @body, @method, @options = body, body.method, options.dup
      @closure = @options.delete(:closure)
    end

    ActivationPrologue = Matcher.new do
      [:begin,
        [:push_scope,
          [:get_local, 0]],
        [:set_local, -1,
          [:new_activation]],
        [:set_local, capture(:activation_local),
          [:get_local, -1]],
        [:push_scope,
          [:get_local, -1]],
        skip
      ]
    end

    RegularPrologue = Matcher.new do
      [:begin,
        [:push_scope,
          [:get_local, 0]],
        skip
      ]
    end

    def decompile
      begin
        @locals = Set.new([0]) + (1..@method.param_count).to_a
        @scopes = []

        @nf = @body.code_to_nf

        if captures = ActivationPrologue.match(@nf)
          @closure_slots = {}
          @body.slot_traits.each do |trait|
            @closure_slots[trait.idx] = trait
          end

          @closure_locals = Set.new

          @nf.children.slice! 0...4
        else
          if RegularPrologue.match @nf
            @nf.children.slice! 0...1
          else
            # No prologue; probably a closure.
          end
        end

        @global_slots = @options[:global_slots]

        stmt_block @nf,
          function: !@options[:global_code],
          closure:  @closure

      rescue Exception => e
        @failed = true

        comment = "'Ouch!' cried I, then died.\n" +
          "#{e.class}: #{e.message}\n" +
          "#{e.backtrace[0..5].map { |l| "    #{l}\n"}.join}" +
          "      ... et cetera\n"

        token(ScopeToken, [
          token(CommentToken, comment)
        ], function: !@options[:global_code],
           closure:  @closure)

      ensure
        if stat = @options[:stat]
          stat[:total] += 1

          if @failed
            stat[:failed]  += 1
          elsif @partial
            stat[:partial] += 1
          else
            stat[:success] += 1
          end
        end
      end
    end

    # Statements

    def stmt_block(block, options={})
      nodes = []

      block.children.each do |opcode|
        if respond_to?(:"stmt_#{opcode.type}")
          send :"stmt_#{opcode.type}", opcode, nodes
        else
          catch(:skip) do
            nodes << token(StatementToken, [
              handle_expression(opcode)
            ])
          end
        end
      end

    rescue ExpressionNotRecognized => e
      @partial = true

      comment = "Well, this is embarassing.\n\n" +
        "Expression recognizer failed at:\n" +
        "#{e.opcode.inspect}\n"

      if e.context != e.opcode
        comment << "\nOpcode at the top of stack:\n" +
          "#{e.context.inspect}\n"
      end

      nodes << CommentToken.new(@body, comment, @options)

    ensure
      if $!.nil? || $!.is_a?(ExpressionNotRecognized)
        return token(ScopeToken, nodes, options)
      end
    end

    def stmt_if(opcode, nodes)
      condition, if_true, if_false = opcode.children

      nodes << token(IfToken, handle_expression(condition),
        stmt_block(if_true, continuation: !if_false.nil?))
      nodes << token(ElseToken,
        stmt_block(if_false)) if if_false
    end

    def stmt_label(opcode, nodes)
      name, = opcode.children

      nodes << token(LabelDeclarationToken, name)
    end

    def stmt_while(opcode, nodes)
      condition, body = opcode.children

      nodes << token(WhileToken, handle_expression(condition),
        stmt_block(body))
    end

    def stmt_for(opcode, nodes)
      value_reg, value_type, object_reg, body = opcode.children

      @locals.add(value_reg)

      if opcode.type == :for_in
        klass = ForToken
      elsif opcode.type == :for_each_in
        klass = ForEachToken
      end

      if @closure_slots
        name = token(VariableNameToken, @closure_slots[value_reg].name.name)
      else
        name = token(VariableNameToken, local_name(value_reg))
      end

      nodes << token(klass,
        token(InToken, [
          token(LocalVariableToken, [
            name,
            type_token(value_type)
          ]),
          token(VariableNameToken, local_name(object_reg)),
        ]),
        stmt_block(body))
    end
    alias :stmt_for_in      :stmt_for
    alias :stmt_for_each_in :stmt_for

    def stmt_break(opcode, nodes)
      nodes << token(ReturnToken, exprs(opcode.children))
    end

    def stmt_continue(opcode, nodes)
      nodes << token(ContinueToken, exprs(opcode.children))
    end

    def stmt_throw(opcode, nodes)
      nodes << token(ThrowToken, exprs(opcode.children))
    end

    def stmt_return(opcode, nodes)
      nodes << token(ReturnToken, exprs(opcode.children))
    end
    alias :stmt_return_value :stmt_return
    alias :stmt_return_void  :stmt_return

    def stmt_push_scope(opcode, nodes)
      if @options[:global_code]
        @scopes.push opcode.children.first
      else
        raise "pushscope in nonglobal code"
      end
    end

    def stmt_pop_scope(opcode, nodes)
      if @options[:global_code]
        @scopes.pop
      else
        raise "popscope in nonglobal code"
      end
    end

    # Expressions

    def handle_expression(opcode)
      expression(opcode)
    rescue ExpressionNotRecognized => e
      raise ExpressionNotRecognized.new(opcode, e.opcode)
    end

    def expression(opcode)
      handler = :"expr_#{opcode.type}"
      if respond_to?(handler) && node = send(handler, opcode)
        node
      else
        raise ExpressionNotRecognized.new(nil, opcode)
      end
    end
    alias :expr :expression

    def expressions(opcodes)
      opcodes.map do |opcode|
        expression opcode
      end
    end
    alias :exprs :expressions

    ## Immediates

    def expr_imm(opcode)
      case opcode.type
      when :integer, :double
        token(ImmediateToken, *opcode.children)
      when :null, :undefined, :true, :false
        token(ImmediateToken, opcode.type)
      end
    end
    alias :expr_integer   :expr_imm
    alias :expr_double    :expr_imm
    alias :expr_null      :expr_imm
    alias :expr_undefined :expr_imm
    alias :expr_true      :expr_imm
    alias :expr_false     :expr_imm

    def expr_string(opcode)
      string, = opcode.children
      token(ImmediateToken, string.inspect)
    end

    def expr_nan(opcode)
      token(ImmediateToken, "NaN")
    end

    def expr_new_array(opcode)
      token(ArrayToken, exprs(opcode.children))
    end

    def expr_new_object(opcode)
      token(ObjectToken, opcode.children.
        each_slice(2).map do |key, value|
          token(ObjectPairToken, [
            expr(key),
            expr(value)
          ])
        end)
    end

    ## Locals

    def local_name(index)
      if index < 0
        "sp#{-index}"
      elsif index == 0
        if @options[:static]
          @options[:instance].name.name
        else
          "this"
        end
      elsif index <= @method.param_count
        if @method.has_param_names?
          @method.param_names[index - 1]
        else
          "param#{index - 1}"
        end
      else
        "local#{index - @method.param_count - 1}"
      end
    end

    def expr_get_local(opcode)
      index, = opcode.children
      token(VariableNameToken, local_name(index))
    end

    ActivationGetSlot = Matcher.new do
      [:get_slot,
        capture(:index),
        [:get_scope_object, 1]]
    end

    GlobalGetSlot = Matcher.new do
      [:get_slot,
        capture(:index),
        either[
          [:get_global_scope],    # normalized form
          [:get_scope_object, 0]  # not emitted by ASC
        ]]
    end

    def expr_get_slot(opcode)
      if @closure_slots && captures = ActivationGetSlot.match(opcode)
        # treat as a local variable
        slot = @closure_slots[captures[:index]]
        token(VariableNameToken, slot.name.name)
      elsif captures = GlobalGetSlot.match(opcode)
        # treat as a global property
        if @global_slots
          slot = @global_slots[captures[:index]]
          get_name(nil, slot.name.to_astlet)
        else
          token(PropertyNameToken,
            "$__GLOBAL_#{captures[:index]}")
        end
      end
    end

    IMMEDIATE_TYPE_MAP = {
      :any       => '*',
      :integer   => 'int',
      :unsigned  => 'uint',
      :string    => 'String',
      :double    => 'Number',
      :object    => 'Object',
      :bool      => 'Boolean',
      :true      => 'Boolean',
      :false     => 'Boolean',
    }

    def type_token(type)
      if IMMEDIATE_TYPE_MAP.include?(type)
        token(TypeToken, [
          token(ImmediateTypenameToken, IMMEDIATE_TYPE_MAP[type])
        ])
      else
        token(TypeToken, [
          token(MultinameToken, type.metadata[:origin])
        ])
      end
    end

    XmlLiteralPreMatcher = Matcher.new do
      [:coerce, [:q, "XML"], any]
    end

    def expr_set_var(name, value, type, declare)
      if declare
        token(LocalVariableToken, [
          token(VariableNameToken, name),
          type,
          token(InitializationToken, [
            expr(value)
          ])
        ])
      else
        token(AssignmentToken, [
          token(VariableNameToken, name),
          expr(value)
        ])
      end
    end

    def expr_set_local(opcode)
      index, value = opcode.children
      if IMMEDIATE_TYPE_MAP.include?(value.type)
        type = token(TypeToken, [
          token(ImmediateTypenameToken, IMMEDIATE_TYPE_MAP[value.type])
        ])
      elsif XmlLiteralPreMatcher.match value
        # XML literals work through expr_coerce
        type  = type_token(value.children.first)
        value = value
      elsif value.type == :coerce || value.type == :convert
        # Don't emit spurious coercion; typed variables already
        # imply it
        type  = type_token(value.children.first)
        value = value.children.last
      end

      expr_set_var(local_name(index), value, type, !@locals.include?(index))
    ensure
      @locals.add index if index
    end

    ActivationSetSlot = Matcher.new do
      [:set_slot,
        capture(:index),
        [:get_scope_object, 1],
        capture(:value)]
    end

    GlobalSetSlot = Matcher.new do
      [:set_slot,
        capture(:index),
        either[
          [:get_global_scope],    # normalized form
          [:get_scope_object, 0]  # not emitted by ASC
        ],
        capture(:value)]
    end

    def expr_set_slot(opcode)
      if @closure_slots && captures = ActivationSetSlot.match(opcode)
        # treat as a local variable
        index, value = captures.values_at(:index, :value)
        slot = @closure_slots[index]

        type = type_token(slot.type.to_astlet) if slot.type
        expr = expr_set_var(slot.name.name, value, type,
              !@closure_locals.include?(index))
        @closure_locals.add index

        expr
      elsif captures = GlobalSetSlot.match(opcode)
        # treat as a global property
        index, value = captures.values_at(:index, :value)

        if @global_slots
          slot = @global_slots[index]
          name = get_name(nil, slot.name.to_astlet)
        else
          token(PropertyNameToken, "$__GLOBAL_#{index}")
        end

        token(AssignmentToken, [
          name,
          expr(value)
        ])
      end
    end

    ## Arithmetics

    INPLACE_OPERATOR_MAP = {
      :inc_local   => :"++",
      :inc_local_i => :"++",
      :dec_local   => :"--",
      :dec_local_i => :"--",
    }

    def expr_inplace_arithmetic(opcode)
      token(UnaryPostOperatorToken, [
        token(VariableNameToken, local_name(*opcode.children)),
      ], INPLACE_OPERATOR_MAP[opcode.type])
    end

    alias :expr_inc_local   :expr_inplace_arithmetic
    alias :expr_inc_local_i :expr_inplace_arithmetic
    alias :expr_dec_local   :expr_inplace_arithmetic
    alias :expr_dec_local_i :expr_inplace_arithmetic

    PSEUDO_OPERATOR_MAP = {
      :increment   => [:"+", 1],
      :increment_i => [:"+", 1],
      :decrement   => [:"-", 1],
      :decrement_i => [:"-", 1],
    }

    def expr_pseudo_arithmetic(opcode)
      inside = expr(*opcode.children)
      inside = token(ParenthesesToken, [inside]) if inside.complex?

      operator, other_value = PSEUDO_OPERATOR_MAP[opcode.type]
      token(BinaryOperatorToken, [
        inside,
        token(ImmediateToken, other_value)
      ], operator)
    end

    alias :expr_increment   :expr_pseudo_arithmetic
    alias :expr_increment_i :expr_pseudo_arithmetic
    alias :expr_decrement   :expr_pseudo_arithmetic
    alias :expr_decrement_i :expr_pseudo_arithmetic

    OPERATOR_MAP = {
      :and         => :"&&",
      :or          => :"||",

      :add         => :"+",
      :add_i       => :"+",
      :subtract    => :"-",
      :subtract_i  => :"-",
      :multiply    => :"*",
      :multiply_i  => :"*",
      :divide      => :"/",
      :modulo      => :"%",
      :multiply    => :"*",
      :negate      => :"-",
      :negate_i    => :"-",

      :!           => :!,
      :>           => :>,
      :>=          => :>=,
      :<           => :<,
      :<=          => :<=,
      :==          => :==,
      :===         => :===,
      :!=          => :!=,
      :"!=="       => :"!==",

      :bit_and     => :"&",
      :bit_or      => :"|",
      :bit_xor     => :"^",
      :bit_not     => :"~",
      :lshift      => :"<<",
      :rshift      => :">>",
      :urshift     => :">>>",
    }

    def expr_arithmetic(opcode)
      if OPERATOR_MAP.include?(opcode.type)
        insides = parenthesize_each(exprs(opcode.children))

        if insides.count == 1
          token(UnaryOperatorToken, insides, OPERATOR_MAP[opcode.type])
        elsif insides.count == 2
          token(BinaryOperatorToken, insides, OPERATOR_MAP[opcode.type])
        else
          token(CommentToken, "Unexpected #{insides.count}-ary operator")
        end
      end
    end

    alias :expr_and         :expr_arithmetic
    alias :expr_or          :expr_arithmetic

    alias :expr_add         :expr_arithmetic
    alias :expr_add_i       :expr_arithmetic
    alias :expr_subtract    :expr_arithmetic
    alias :expr_subtract_i  :expr_arithmetic
    alias :expr_multiply    :expr_arithmetic
    alias :expr_multiply_i  :expr_arithmetic
    alias :expr_divide      :expr_arithmetic
    alias :expr_modulo      :expr_arithmetic
    alias :expr_negate      :expr_arithmetic

    alias :expr_!           :expr_arithmetic
    alias :"expr_>"         :expr_arithmetic
    alias :"expr_>="        :expr_arithmetic
    alias :"expr_<"         :expr_arithmetic
    alias :"expr_<="        :expr_arithmetic
    alias :"expr_=="        :expr_arithmetic
    alias :"expr_==="       :expr_arithmetic
    alias :"expr_!="        :expr_arithmetic
    alias :"expr_!=="       :expr_arithmetic

    alias :expr_bit_and     :expr_arithmetic
    alias :expr_bit_or      :expr_arithmetic
    alias :expr_bit_xor     :expr_arithmetic
    alias :expr_bit_not     :expr_arithmetic
    alias :expr_lshift      :expr_arithmetic
    alias :expr_rshift      :expr_arithmetic
    alias :expr_urshift     :expr_arithmetic

    def expr_in(opcode)
      token(InToken, parenthesize_each(exprs(opcode.children)))
    end

    ## Properties and objects

    This = Matcher.new do
      [:get_local, 0]
    end

    PropertyGlobal = Matcher.new do
      [any,
        either_multi[
          [
            [ either[:find_property, :find_property_strict],
                  capture(:multiname)],
            backref(:multiname),
          ],
          [
            [:find_property_strict,
              [:m, [:set, "*"], any]],
            capture(:multiname)
          ],
          [
            [:get_scope_object, 0],
            capture(:multiname)
          ],
        ],
        capture_rest(:arguments)]
    end

    def expr_get_lex(opcode)
      multiname, = opcode.children
      get_name(nil, multiname)
    end

    def expr_get_property(opcode)
      if captures = PropertyGlobal.match(opcode)
        get_name(nil, captures[:multiname])
      else
        subject, multiname, = opcode.children
        get_name(expr(subject), multiname)
      end
    end

    def expr_get_super(opcode)
      subject, multiname, = opcode.children

      stmt = get_name(token(SuperToken), multiname)

      unless This.match subject
        stmt = token(SupplementaryCommentToken,
          "subject != this: #{subject.inspect}",
          [ stmt ])
      end

      stmt
    end

    def expr_set_property(opcode)
      if captures = PropertyGlobal.match(opcode)
        token(AssignmentToken, [
          get_name(nil, captures[:multiname]),
          expr(*captures[:arguments])
        ])
      else
        subject, multiname, value, = opcode.children
        token(AssignmentToken, [
          get_name(expr(subject), multiname),
          expr(value)
        ])
      end
    end
    alias :expr_init_property :expr_set_property

    def expr_set_super(opcode)
      subject, multiname, value = opcode.children

      stmt = token(AssignmentToken, [
        get_name(token(SuperToken), multiname),
        expr(value)
      ])

      unless This.match subject
        stmt = token(SupplementaryCommentToken,
          "subject != this: #{subject.inspect}",
          [ stmt ])
      end

      stmt
    end

    def expr_delete_property(opcode)
      subject, multiname, = opcode.children
      token(DeleteToken, [get_name(expr(subject), multiname)])
    end

    def expr_do_property(opcode, klass)
      if captures = PropertyGlobal.match(opcode)
        token(klass, [
          get_name(nil, captures[:multiname]),
          token(ArgumentsToken, exprs(captures[:arguments]))
        ])
      else
        subject, multiname, *args = opcode.children
        token(klass, [
          get_name(expr(subject), multiname),
          token(ArgumentsToken, exprs(args))
        ])
      end
    end

    def expr_call_property(opcode)
      expr_do_property(opcode, CallToken)
    end
    alias :expr_call_property_lex :expr_call_property

    def expr_call_super(opcode)
      subject, multiname, *args = opcode.children
      if This.match subject
        token(CallToken, [
{'fraction_non_alphanumeric': 0.11087656626342995, 'fraction_numerical': 0.0020567778950502016, 'mean_word_length': 2.0110821382007824, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 123, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3853100', 'n_tokens_mistral': 11712, 'n_tokens_neox': 11423, 'n_words': 2266}