lib/opal/nodes/scope.rb



# frozen_string_literal: true
require 'opal/nodes/base'

module Opal
  module Nodes
    class ScopeNode < Base

      # Every scope can have a parent scope
      attr_accessor :parent

      # The class or module name if this scope is a class scope
      attr_accessor :name

      # The given block name for a def scope
      attr_accessor :block_name

      attr_reader :scope_name
      attr_reader :locals
      attr_reader :ivars
      attr_reader :gvars

      attr_accessor :mid

      # true if singleton def, false otherwise
      attr_accessor :defs

      # used by modules to know what methods to donate to includees
      attr_reader :methods

      # uses parents super method
      attr_accessor :uses_super
      attr_accessor :uses_zuper

      attr_accessor :catch_return, :has_break

      attr_accessor :rescue_else_sexp

      def initialize(*)
        super

        @locals   = []
        @temps    = []
        @args     = []
        @ivars    = []
        @gvars    = []
        @parent   = nil
        @queue    = []
        @unique   = 'a'
        @while_stack = []
        @identity = nil
        @defs     = nil

        @methods = []

        @uses_block = false

        # used by classes to store all ivars used in direct def methods
        @proto_ivars = []
      end

      def in_scope(&block)
        indent do
          @parent = compiler.scope
          compiler.scope = self
          block.call self
          compiler.scope = @parent
        end
      end

      # Returns true if this scope is a class/module body scope
      def class_scope?
        @type == :class or @type == :module
      end

      # Returns true if this is strictly a class scope
      def class?
        @type == :class
      end

      # True if this is a module scope
      def module?
        @type == :module
      end

      def sclass?
        @type == :sclass
      end

      # Returns true if this is a top scope (main file body)
      def top?
        @type == :top
      end

      # True if a block/iter scope
      def iter?
        @type == :iter
      end

      def def?
        @type == :def || @type == :defs
      end

      # Is this a normal def method directly inside a class? This is
      # used for optimizing ivars as we can set them to nil in the
      # class body
      def def_in_class?
        !@defs && @type == :def && @parent && @parent.class?
      end

      # Inside a class or module scope, the javascript variable name returned
      # by this function points to the classes' prototype. This is the target
      # to where methods are actually added inside a class body.
      def proto
        "def"
      end

      ##
      # Vars to use inside each scope
      def to_vars
        vars = @temps.dup
        vars.push(*@locals.map { |l| "#{l} = nil" })

        iv = ivars.map do |ivar|
          "if (self#{ivar} == null) self#{ivar} = nil;\n"
        end

        gv = gvars.map do |gvar|
          "if ($gvars#{gvar} == null) $gvars#{gvar} = nil;\n"
        end

        indent = @compiler.parser_indent
        str  = vars.empty? ? '' : "var #{vars.join ', '};\n"
        str += "#{indent}#{iv.join indent}" unless ivars.empty?
        str += "#{indent}#{gv.join indent}" unless gvars.empty?

        if class? and !@proto_ivars.empty?
          #raise "FIXME to_vars"
          pvars = @proto_ivars.map { |i| "#{proto}#{i}"}.join(' = ')
          result = "%s\n%s%s = nil;" % [str, indent, pvars]
        else
          result = str
        end

        fragment(result)
      end

      def add_scope_ivar(ivar)
        if def_in_class?
          @parent.add_proto_ivar ivar
        else
          @ivars << ivar unless @ivars.include? ivar
        end
      end

      def add_scope_gvar(gvar)
        @gvars << gvar unless @gvars.include? gvar
      end

      def add_proto_ivar(ivar)
        @proto_ivars << ivar unless @proto_ivars.include? ivar
      end

      def add_arg(arg)
        @args << arg unless @args.include? arg
        arg
      end

      def add_scope_local(local)
        return if has_local? local

        @locals << local
      end

      def has_local?(local)
        return true if @locals.include? local or @args.include? local or @temps.include? local
        return @parent.has_local?(local) if @parent and @type == :iter
        false
      end

      def add_scope_temp(tmp)
        return if has_temp?(tmp)

        @temps.push(tmp)
      end

      def has_temp?(tmp)
        @temps.include? tmp
      end

      def new_temp
        return @queue.pop unless @queue.empty?

        tmp = next_temp
        @temps << tmp
        tmp
      end

      def next_temp
        while true
          tmp = "$#{@unique}"
          @unique = @unique.succ
          unless has_local?(tmp)
            break
          end
        end
        tmp
      end

      def queue_temp(name)
        @queue << name
      end

      def push_while
        info = {}
        @while_stack.push info
        info
      end

      def pop_while
        @while_stack.pop
      end

      def in_while?
        !@while_stack.empty?
      end

      def uses_block!
        if @type == :iter && @parent
          @parent.uses_block!
        else
          @uses_block = true
          identify!
        end
      end

      def identify!(name = nil)
        return @identity if @identity

        # Parent scope is the defining module/class
        name ||= [(parent && (parent.name || parent.scope_name)), self.mid].compact.join('_')
        @identity = @compiler.unique_temp(name)
        @parent.add_scope_temp @identity if @parent

        @identity
      end

      def identity
        @identity
      end

      def find_parent_def
        scope = self
        while scope = scope.parent
          if scope.def?
            return scope
          end
        end

        nil
      end

      def get_super_chain
        chain, scope, defn, mid = [], self, 'null', 'null'

        while scope
          if scope.type == :iter
            chain << scope.identify!
            scope = scope.parent if scope.parent

          elsif [:def, :defs].include?(scope.type)
            defn = scope.identify!
            mid  = "'#{scope.mid}'"
            break
          else
            break
          end
        end

        [chain, defn, mid]
      end

      def uses_block?
        @uses_block
      end

      def has_rescue_else?
        !!rescue_else_sexp
      end

      def in_ensure
        return unless block_given?

        @in_ensure = true
        result = yield
        @in_ensure = false

        result
      end

      def in_ensure?
        !!@in_ensure
      end
    end
  end
end