Steepコードリーディング(9日目)

# Steepコードリーディング(9日目)

`TypeCheckService#.type_check`の続きを見ていきます。
どうも`construction.synthesize(source.node)`で型検査してるっぽいんだよな。

## `Steep::TypeConstruction`

https://github.com/soutaro/steep/blob/290ab9a3d023f1ed8011fb720c420c611e5473b9/lib/steep/type_construction.rb#L2

### `Steep::TypeConstruction#synthesize`

Qiitaのエディタが固まってまともに使えなくなるくらい長いです。別記事で1つずつ見ていきます。

```rb
def synthesize(node, hint: nil, condition: false)
  Steep.logger.tagged "synthesize:(#{node.location&.yield_self {|loc| loc.expression.to_s.split(/:/, 2).last } || "-"})" do
    Steep.logger.debug node.type
    case node.type
    when :begin, :kwbegin
      yield_self do
        end_pos = node.loc.expression.end_pos

        *mid_nodes, last_node = each_child_node(node).to_a
        if last_node
          pair = mid_nodes.inject(Pair.new(type: AST::Builtin.nil_type, constr: self)) do |pair, node|
            pair.constr.synthesize(node).yield_self {|p| pair + p }.tap do |new_pair|
              if new_pair.constr.context != pair.constr.context
                # update context
                range = node.loc.expression.end_pos..end_pos
                typing.cursor_context.set(range, new_pair.constr.context)
              end
            end
          end

          p = pair.constr.synthesize(last_node, hint: hint, condition: condition)
          last_pair = pair + p
          last_pair.constr.add_typing(node, type: last_pair.type, constr: last_pair.constr)
        else
          add_typing(node, type: AST::Builtin.nil_type)
        end
      end

    when :lvasgn
      yield_self do
        name, rhs = node.children

        case name
        when :_, :__any__
          synthesize(rhs, hint: AST::Builtin.any_type).yield_self do |pair|
            add_typing(node, type: AST::Builtin.any_type, constr: pair.constr)
          end
        when :__skip__
          add_typing(node, type: AST::Builtin.any_type)
        else
          if enforced_type = context.type_env.enforced_type(name)
            case
            when !hint
              hint = enforced_type
            when check_relation(sub_type: enforced_type, super_type: hint).success?
              # enforced_type is compatible with hint and more specific to hint.
              # This typically happens when hint is untyped, top, or void.
              hint = enforced_type
            end
          end

          if rhs
            rhs_type, rhs_constr, rhs_context = synthesize(rhs, hint: hint).to_ary

            constr = rhs_constr.update_type_env do |type_env|
              var_type = rhs_type

              if enforced_type = type_env.enforced_type(name)
                if result = no_subtyping?(sub_type: rhs_type, super_type: enforced_type)
                  typing.add_error(
                    Diagnostic::Ruby::IncompatibleAssignment.new(
                      node: node,
                      lhs_type: enforced_type,
                      rhs_type: rhs_type,
                      result: result
                    )
                  )

                  var_type = enforced_type
                end

                if rhs_type.is_a?(AST::Types::Any)
                  var_type = enforced_type
                end
              end

              type_env.assign_local_variable(name, var_type, enforced_type)
            end

            constr.add_typing(node, type: rhs_type)
          else
            add_typing(node, type: enforced_type || AST::Builtin.any_type)
          end
        end
      end

    when :lvar
      yield_self do
        var = node.children[0]

        if SPECIAL_LVAR_NAMES.include?(var)
          add_typing node, type: AST::Builtin.any_type
        else
          if (type = context.type_env[var])
            add_typing node, type: type
          else
            fallback_to_any(node)
          end
        end
      end

    when :ivasgn
      name = node.children[0]
      rhs = node.children[1]

      rhs_type, constr = synthesize(rhs, hint: context.type_env[name])

      constr.ivasgn(node, rhs_type)

    when :ivar
      yield_self do
        name = node.children[0]

        if type = context.type_env[name]
          add_typing(node, type: type)
        else
          fallback_to_any node
        end
      end

    when :match_with_lvasgn
      each_child_node(node) do |child|
        synthesize(child)
      end
      add_typing(node, type: AST::Builtin.any_type)

    when :op_asgn
      yield_self do
        lhs, op, rhs = node.children

        case lhs.type
        when :lvasgn
          var_node = lhs.updated(:lvar)
          send_node = rhs.updated(:send, [var_node, op, rhs])
          new_node = node.updated(:lvasgn, [lhs.children[0], send_node])

          type, constr = synthesize(new_node, hint: hint)

          constr.add_typing(node, type: type)

        when :ivasgn
          var_node = lhs.updated(:ivar)
          send_node = rhs.updated(:send, [var_node, op, rhs])
          new_node = node.updated(:ivasgn, [lhs.children[0], send_node])

          type, constr = synthesize(new_node, hint: hint)

          constr.add_typing(node, type: type)

        when :cvasgn
          var_node = lhs.updated(:cvar)
          send_node = rhs.updated(:send, [var_node, op, rhs])
          new_node = node.updated(:cvasgn, [lhs.children[0], send_node])

          type, constr = synthesize(new_node, hint: hint)

          constr.add_typing(node, type: type)

        when :gvasgn
          var_node = lhs.updated(:gvar)
          send_node = rhs.updated(:send, [var_node, op, rhs])
          new_node = node.updated(:gvasgn, [lhs.children[0], send_node])

          type, constr = synthesize(new_node, hint: hint)

          constr.add_typing(node, type: type)

        when :send
          new_rhs = rhs.updated(:send, [lhs, node.children[1], node.children[2]])
          new_node = lhs.updated(:send, [lhs.children[0], :"#{lhs.children[1]}=", *lhs.children.drop(2), new_rhs])

          type, constr = synthesize(new_node, hint: hint)

          constr.add_typing(node, type: type)

        else
          Steep.logger.error("Unexpected op_asgn lhs: #{lhs.type}")

          _, constr = synthesize(rhs)
          constr.add_typing(node, type: AST::Builtin.any_type)
        end
      end

    when :super
      yield_self do
        if self_type && method_context!.method
          if super_def = method_context!.super_method
            super_method = Interface::Shape::Entry.new(
              method_name: method_context!.name,
              private_method: true,
              overloads: super_def.defs.map {|type_def|
                type = checker.factory.method_type(type_def.type)
                Interface::Shape::MethodOverload.new(type, [type_def])
              }
            )

            call, constr = type_method_call(
              node,
              receiver_type: self_type,
              method_name: method_context!.name || raise("method context must have a name"),
              method: super_method,
              arguments: node.children,
              block_params: nil,
              block_body: nil,
              tapp: nil,
              hint: hint
            )

            if call && constr
              constr.add_call(call)
            else
              error = Diagnostic::Ruby::UnresolvedOverloading.new(
                node: node,
                receiver_type: self_type,
                method_name: method_context!.name,
                method_types: super_method.method_types
              )
              call = TypeInference::MethodCall::Error.new(
                node: node,
                context: context.call_context,
                method_name: method_context!.name || raise("method context must have a name"),
                receiver_type: self_type,
                errors: [error]
              )

              constr = synthesize_children(node)

              fallback_to_any(node) { error }
            end
          else
            type_check_untyped_args(node.children).fallback_to_any(node) do
              Diagnostic::Ruby::UnexpectedSuper.new(node: node, method: method_context!.name)
            end
          end
        else
          type_check_untyped_args(node.children).fallback_to_any(node)
        end
      end

    when :def
      yield_self do
        # @type var node: Parser::AST::Node & Parser::AST::_DefNode

        name, args_node, body_node = node.children

        with_method_constr(
          name,
          node,
          args: args_node.children,
          self_type: module_context&.instance_type,
          definition: module_context&.instance_definition
        ) do |new|
          # @type var new: TypeConstruction

          new.typing.cursor_context.set_node_context(node, new.context)
          new.typing.cursor_context.set_body_context(node, new.context)

          new.method_context!.tap do |method_context|
            if method_context.method
              if owner = method_context.method.implemented_in || method_context.method.defined_in
                method_name = InstanceMethodName.new(type_name: owner, method_name: name)
                new.typing.source_index.add_definition(method: method_name, definition: node)
              end
            end
          end

          new = new.synthesize_children(args_node)

          body_pair = if body_node
                        return_type = expand_alias(new.method_context!.return_type)
                        if !return_type.is_a?(AST::Types::Void)
                          new.check(body_node, return_type) do |_, actual_type, result|
                            if new.method_context!.attribute_setter?
                              typing.add_error(
                                Diagnostic::Ruby::SetterBodyTypeMismatch.new(
                                  node: node,
                                  expected: new.method_context!.return_type,
                                  actual: actual_type,
                                  result: result,
                                  method_name: new.method_context!.name
                                )
                              )
                            else
                              typing.add_error(
                                Diagnostic::Ruby::MethodBodyTypeMismatch.new(
                                  node: node,
                                  expected: new.method_context!.return_type,
                                  actual: actual_type,
                                  result: result
                                )
                              )
                            end
                          end
                        else
                          new.synthesize(body_node)
                        end
                      else
                        return_type = expand_alias(new.method_context!.return_type)
                        if !return_type.is_a?(AST::Types::Void)
                          result = check_relation(sub_type: AST::Builtin.nil_type, super_type: return_type)
                          if result.failure?
                            if new.method_context!.attribute_setter?
                              typing.add_error(
                                Diagnostic::Ruby::SetterBodyTypeMismatch.new(
                                  node: node,
                                  expected: new.method_context!.return_type,
                                  actual: AST::Builtin.nil_type,
                                  result: result,
                                  method_name: new.method_context!.name
                                )
                              )
                            else
                              typing.add_error(
                                Diagnostic::Ruby::MethodBodyTypeMismatch.new(
                                  node: node,
                                  expected: new.method_context!.return_type,
                                  actual: AST::Builtin.nil_type,
                                  result: result
                                )
                              )
                            end
                          end
                        end

                        Pair.new(type: AST::Builtin.nil_type, constr: new)
                      end

          if body_node
            # Add context to ranges from the end of the method body to the beginning of the `end` keyword
            if node.loc.end
              # Skip end-less def
              begin_pos = body_node.loc.expression.end_pos
              end_pos = node.loc.end.begin_pos
              typing.cursor_context.set(begin_pos..end_pos, body_pair.context)
            end
          end

          if module_context
            module_context.defined_instance_methods << node.children[0]
          end

          add_typing(node, type: AST::Builtin::Symbol.instance_type)
        end
      end

    when :defs
      synthesize(node.children[0]).type.tap do |self_type|
        self_type = expand_self(self_type)
        definition =
          case self_type
          when AST::Types::Name::Instance
            name = self_type.name
            checker.factory.definition_builder.build_instance(name)
          when AST::Types::Name::Singleton
            name = self_type.name
            checker.factory.definition_builder.build_singleton(name)
          end

        args_node = node.children[2]
        new = for_new_method(
          node.children[1],
          node,
          args: args_node.children,
          self_type: self_type,
          definition: definition
        )
        new.typing.cursor_context.set_node_context(node, new.context)
        new.typing.cursor_context.set_body_context(node, new.context)

        new.method_context!.tap do |method_context|
          if method_context.method
            name_ = node.children[1]

            method_name =
              case self_type
              when AST::Types::Name::Instance
                InstanceMethodName.new(type_name: method_context.method.implemented_in || raise, method_name: name_)
              when AST::Types::Name::Singleton
                SingletonMethodName.new(type_name: method_context.method.implemented_in || raise, method_name: name_)
              end

            new.typing.source_index.add_definition(method: method_name, definition: node)
          end
        end

        new = new.synthesize_children(args_node)

        each_child_node(node.children[2]) do |arg|
          new.synthesize(arg)
        end

        if node.children[3]
          return_type = expand_alias(new.method_context!.return_type)
          if !return_type.is_a?(AST::Types::Void)
            new.check(node.children[3], return_type) do |return_type, actual_type, result|
              typing.add_error(
                Diagnostic::Ruby::MethodBodyTypeMismatch.new(
                  node: node,
                  expected: return_type,
                  actual: actual_type,
                  result: result
                )
              )
            end
          else
            new.synthesize(node.children[3])
          end
        end
      end

      if node.children[0].type == :self
        module_context.defined_module_methods << node.children[1]
      end

      add_typing(node, type: AST::Builtin::Symbol.instance_type)

    when :return
      yield_self do
        method_return_type =
          if method_context
            expand_alias(method_context.return_type)
          end

        case node.children.size
        when 0
          value_type = AST::Builtin.nil_type
          constr = self
        when 1
          return_value_node = node.children[0]
          value_type, constr = synthesize(return_value_node, hint: method_return_type)
        else
          # It returns an array
          array = node.updated(:array)
          value_type, constr = synthesize(array, hint: method_return_type)
        end

        if method_return_type
          unless method_context.nil? || method_return_type.is_a?(AST::Types::Void)
            result = constr.check_relation(sub_type: value_type, super_type: method_return_type)

            if result.failure?
              if method_context.attribute_setter?
                typing.add_error(
                  Diagnostic::Ruby::SetterReturnTypeMismatch.new(
                    node: node,
                    method_name: method_context.name,
                    expected: method_return_type,
                    actual: value_type,
                    result: result
                  )
                )
              else
                typing.add_error(
                  Diagnostic::Ruby::ReturnTypeMismatch.new(
                    node: node,
                    expected: method_return_type,
                    actual: value_type,
                    result: result
                  )
                )
              end
            end
          end
        end

        constr.add_typing(node, type: AST::Builtin.bottom_type)
      end

    when :break
      value = node.children[0]

      if break_context
        break_type = break_context.break_type

        if value
          check(value, break_type) do |break_type, actual_type, result|
            typing.add_error(
              Diagnostic::Ruby::BreakTypeMismatch.new(
                node: node,
                expected: break_type,
                actual: actual_type,
                result: result
              )
            )
          end
        else
          unless break_type.is_a?(AST::Types::Bot)
            check_relation(sub_type: AST::Builtin.nil_type, super_type: break_type).else do |result|
              typing.add_error(
                Diagnostic::Ruby::ImplicitBreakValueMismatch.new(
                  node: node,
                  jump_type: break_type,
                  result: result
                )
              )
            end
          end
        end
      else
        synthesize(value) if value
        typing.add_error Diagnostic::Ruby::UnexpectedJump.new(node: node)
      end

      add_typing(node, type: AST::Builtin.bottom_type)

    when :next
      value = node.children[0]

      if break_context
        if next_type = break_context.next_type
          next_type = deep_expand_alias(next_type) || next_type

          if value
            _, constr = check(value, next_type) do |break_type, actual_type, result|
              typing.add_error(
                Diagnostic::Ruby::BreakTypeMismatch.new(
                  node: node,
                  expected: break_type,
                  actual: actual_type,
                  result: result
                )
              )
            end
          else
            check_relation(sub_type: AST::Builtin.nil_type, super_type: next_type).else do |result|
              typing.add_error(
                Diagnostic::Ruby::BreakTypeMismatch.new(
                  node: node,
                  expected: next_type,
                  actual: AST::Builtin.nil_type,
                  result: result
                )
              )
            end
          end
        else
          if value
            synthesize(value)
            typing.add_error Diagnostic::Ruby::UnexpectedJumpValue.new(node: node)
          end
        end
      else
        synthesize(value) if value
        typing.add_error Diagnostic::Ruby::UnexpectedJump.new(node: node)
      end

      add_typing(node, type: AST::Builtin.bottom_type)

    when :retry
      add_typing(node, type: AST::Builtin.bottom_type)

    when :procarg0
      yield_self do
        constr = self #: TypeConstruction

        node.children.each do |arg|
          if arg.is_a?(Symbol)
            if SPECIAL_LVAR_NAMES === arg
              _, constr = add_typing(node, type: AST::Builtin.any_type)
            else
              type = context.type_env[arg]

              if type
                _, constr = add_typing(node, type: type)
              else
                type = AST::Builtin.any_type
                _, constr = lvasgn(node, type)
              end
            end
          else
            _, constr = constr.synthesize(arg)
          end
        end

        Pair.new(constr: constr, type: AST::Builtin.any_type)
      end

    when :mlhs
      yield_self do
        constr = self #: TypeConstruction

        node.children.each do |arg|
          _, constr = constr.synthesize(arg)
        end

        Pair.new(constr: constr, type: AST::Builtin.any_type)
      end

    when :arg, :kwarg
      yield_self do
        var = node.children[0]

        if SPECIAL_LVAR_NAMES.include?(var)
          add_typing(node, type: AST::Builtin.any_type)
        else
          type = context.type_env[var]

          if type
            add_typing(node, type: type)
          else
            type = AST::Builtin.any_type
            lvasgn(node, type)
          end
        end
      end

    when :optarg, :kwoptarg
      yield_self do
        var = node.children[0]
        rhs = node.children[1]

        if SPECIAL_LVAR_NAMES.include?(var)
          synthesize(rhs)
          add_typing(node, type: AST::Builtin.any_type)
        else
          var_type = context.type_env[var]

          if var_type
            type, constr = check(rhs, var_type) do |expected_type, actual_type, result|
              typing.add_error(
                Diagnostic::Ruby::IncompatibleAssignment.new(
                  node: node,
                  lhs_type: expected_type,
                  rhs_type: actual_type,
                  result: result
                )
              )
            end
          else
            type, constr = synthesize(rhs)
          end

          constr.add_typing(node, type: type)
        end
      end

    when :restarg
      yield_self do
        var = node.children[0]

        if SPECIAL_LVAR_NAMES.include?(var)
          return add_typing(node, type: AST::Builtin.any_type)
        end

        type = context.type_env[var]

        unless type
          if context.method_context&.method_type
            Steep.logger.error { "Unknown variable: #{node}" }
          end
          typing.add_error Diagnostic::Ruby::FallbackAny.new(node: node)
          type = AST::Builtin::Array.instance_type(AST::Builtin.any_type)
        end

        add_typing(node, type: type)
      end

    when :kwrestarg
      yield_self do
        var = node.children[0]

        if SPECIAL_LVAR_NAMES.include?(var)
          return add_typing(node, type: AST::Builtin.any_type)
        end

        type = context.type_env[var]
        unless type
          if context.method_context&.method_type
            Steep.logger.error { "Unknown variable: #{node}" }
          end
          typing.add_error Diagnostic::Ruby::FallbackAny.new(node: node)
          type = AST::Builtin::Hash.instance_type(AST::Builtin::Symbol.instance_type, AST::Builtin.any_type)
        end

        add_typing(node, type: type)
      end

    when :float
      add_typing(node, type: AST::Builtin::Float.instance_type)

    when :rational
      add_typing(node, type: AST::Types::Name::Instance.new(name: TypeName("::Rational"), args: []))

    when :complex
      add_typing(node, type: AST::Types::Name::Instance.new(name: TypeName("::Complex"), args: []))

    when :nil
      add_typing(node, type: AST::Builtin.nil_type)

    when :int
      yield_self do
        literal_type = test_literal_type(node.children[0], hint)

        if literal_type
          add_typing(node, type: literal_type)
        else
          add_typing(node, type: AST::Builtin::Integer.instance_type)
        end
      end

    when :sym
      yield_self do
        literal_type = test_literal_type(node.children[0], hint)

        if literal_type
          add_typing(node, type: literal_type)
        else
          add_typing(node, type: AST::Builtin::Symbol.instance_type)
        end
      end

    when :str
      yield_self do
        literal_type = test_literal_type(node.children[0], hint)

        if literal_type
          add_typing(node, type: literal_type)
        else
          add_typing(node, type: AST::Builtin::String.instance_type)
        end
      end

    when :true, :false
      ty = node.type == :true ? AST::Types::Literal.new(value: true) : AST::Types::Literal.new(value: false)

      case
      when hint && check_relation(sub_type: ty, super_type: hint).success? && !hint.is_a?(AST::Types::Any) && !hint.is_a?(AST::Types::Top)
        add_typing(node, type: hint)
      when condition
        add_typing(node, type: ty)
      else
        add_typing(node, type: AST::Types::Boolean.instance)
      end

    when :hash, :kwargs
      # :kwargs happens for method calls with keyword argument, but the method doesn't have keyword params.
      # Conversion from kwargs to hash happens, and this when-clause is to support it.
      type_hash(node, hint: hint).tap do |pair|
        if pair.type == AST::Builtin::Hash.instance_type(fill_untyped: true)
          case hint
          when AST::Types::Any, AST::Types::Top, AST::Types::Void
            # ok
          else
            unless hint == pair.type
              pair.constr.typing.add_error Diagnostic::Ruby::UnannotatedEmptyCollection.new(node: node)
            end
          end
        end
      end

    when :dstr, :xstr
      each_child_node(node) do |child|
        synthesize(child)
      end

      add_typing(node, type: AST::Builtin::String.instance_type)

    when :dsym
      each_child_node(node) do |child|
        synthesize(child)
      end

      add_typing(node, type: AST::Builtin::Symbol.instance_type)

    when :class
      yield_self do
        constr = self

        # @type var name_node: Parser::AST::Node
        # @type var super_node: Parser::AST::Node?

        name_node, super_node, _ = node.children

        if name_node.type == :const
          _, constr, class_name = synthesize_constant_decl(name_node, name_node.children[0], name_node.children[1]) do
            typing.add_error(
              Diagnostic::Ruby::UnknownConstant.new(node: name_node, name: name_node.children[1]).class!
            )
          end
        else
          _, constr = synthesize(name_node)
        end

        if class_name
          typing.source_index.add_definition(constant: class_name, definition: name_node)
        end

        if super_node
          if super_node.type == :const
            _, constr, super_name = constr.synthesize_constant(super_node, super_node.children[0], super_node.children[1]) do
              typing.add_error(
                Diagnostic::Ruby::UnknownConstant.new(node: super_node, name: super_node.children[1]).class!
              )
            end

            if super_name
              typing.source_index.add_reference(constant: super_name, ref: super_node)
            end
          else
            _, constr = synthesize(super_node, hint: nil, condition: false)
          end
        end

        with_class_constr(node, class_name, super_name) do |constructor|
          if module_type = constructor.module_context&.module_type
            _, constructor = constructor.add_typing(name_node, type: module_type)
          else
            _, constructor = constructor.fallback_to_any(name_node)
          end

          constructor.typing.cursor_context.set_node_context(node, constructor.context)
          constructor.typing.cursor_context.set_body_context(node, constructor.context)

          constructor.synthesize(node.children[2]) if node.children[2]

          if constructor.module_context&.implement_name && !namespace_module?(node)
            constructor.validate_method_definitions(node, constructor.module_context.implement_name || raise)
          end
        end

        add_typing(node, type: AST::Builtin.nil_type)
      end

    when :module
      yield_self do
        constr = self

        # @type var name_node: Parser::AST::Node
        name_node, _ = node.children

        if name_node.type == :const
          _, constr, module_name = synthesize_constant_decl(name_node, name_node.children[0], name_node.children[1]) do
            typing.add_error Diagnostic::Ruby::UnknownConstant.new(node: name_node, name: name_node.children[1]).module!
          end
        else
          _, constr = synthesize(name_node)
        end

        if module_name
          constr.typing.source_index.add_definition(constant: module_name, definition: name_node)
        end

        with_module_constr(node, module_name) do |constructor|
          if module_type = constructor.module_context&.module_type
            _, constructor = constructor.add_typing(name_node, type: module_type)
          else
            _, constructor = constructor.fallback_to_any(name_node)
          end

          constructor.typing.cursor_context.set_node_context(node, constructor.context)
          constructor.typing.cursor_context.set_body_context(node, constructor.context)

          constructor.synthesize(node.children[1]) if node.children[1]

          if constructor.module_context&.implement_name && !namespace_module?(node)
            constructor.validate_method_definitions(node, constructor.module_context.implement_name || raise)
          end
        end

        add_typing(node, type: AST::Builtin.nil_type)
      end

    when :sclass
      yield_self do
        type, constr = synthesize(node.children[0]).to_ary

        with_sclass_constr(node, type) do |constructor|
          unless constructor
            typing.add_error(
              Diagnostic::Ruby::UnsupportedSyntax.new(
                node: node,
                message: "sclass receiver must be instance type or singleton type, but type given `#{type}`"
              )
            )
            return constr.add_typing(node, type: AST::Builtin.nil_type)
          end

          constructor.typing.cursor_context.set_node_context(node, constructor.context)
          constructor.typing.cursor_context.set_body_context(node, constructor.context)

          constructor.synthesize(node.children[1]) if node.children[1]

          if constructor.module_context.instance_definition && module_context.module_definition
            if constructor.module_context.instance_definition.type_name == module_context.module_definition.type_name
              module_context.defined_module_methods.merge(constructor.module_context.defined_instance_methods)
            end
          end
        end

        constr.add_typing(node, type: AST::Builtin.nil_type)
      end

    when :self
      add_typing node, type: AST::Types::Self.instance

    when :cbase
      add_typing node, type: AST::Types::Void.instance

    when :const
      yield_self do
        type, constr, name = synthesize_constant(node, node.children[0], node.children[1])

        if name
          typing.source_index.add_reference(constant: name, ref: node)
        end

        Pair.new(type: type, constr: constr)
      end

    when :casgn
      yield_self do
        constant_type, constr, constant_name = synthesize_constant_decl(nil, node.children[0], node.children[1]) do
          typing.add_error(
            Diagnostic::Ruby::UnknownConstant.new(
              node: node,
              name: node.children[1]
            )
          )
        end

        if constant_name
          typing.source_index.add_definition(constant: constant_name, definition: node)
        end

        value_type, constr = constr.synthesize(node.children.last, hint: constant_type)

        result = check_relation(sub_type: value_type, super_type: constant_type)
        if result.failure?
          typing.add_error(
            Diagnostic::Ruby::IncompatibleAssignment.new(
              node: node,
              lhs_type: constant_type,
              rhs_type: value_type,
              result: result
            )
          )

          constr.add_typing(node, type: constant_type)
        else
          constr.add_typing(node, type: value_type)
        end
      end

    when :yield
      if method_context && method_context.method_type
        if block_type = method_context.block_type
          if block_type.type.params
            type = AST::Types::Proc.new(
              type: block_type.type,
              block: nil,
              self_type: block_type.self_type
            )
            args = TypeInference::SendArgs.new(
              node: node,
              arguments: node.children,
              type: type
            )

            # @type var errors: Array[Diagnostic::Ruby::Base]
            errors = []
            constr = type_check_args(
              nil,
              args,
              Subtyping::Constraints.new(unknowns: []),
              errors
            )

            errors.each do |error|
              typing.add_error(error)
            end
          else
            constr = type_check_untyped_args(node.children)
          end

          add_typing(node, type: block_type.type.return_type)
        else
          typing.add_error(Diagnostic::Ruby::UnexpectedYield.new(node: node))
          fallback_to_any node
        end
      else
        fallback_to_any node
      end

    when :zsuper
      yield_self do
        if method_context && method_context.method
          if method_context.super_method
            types = method_context.super_method.method_types.map {|method_type|
              checker.factory.method_type(method_type).type.return_type
            }
            add_typing(node, type: union_type(*types))
          else
            fallback_to_any(node) do
              Diagnostic::Ruby::UnexpectedSuper.new(node: node, method: method_context.name)
            end
          end
        else
          fallback_to_any node
        end
      end

    when :array
      yield_self do
        if node.children.empty?
          if hint
            array = AST::Builtin::Array.instance_type(AST::Builtin.any_type)
            if check_relation(sub_type: array, super_type: hint).success?
              add_typing node, type: hint
            else
              add_typing node, type: array
            end
          else
            typing.add_error Diagnostic::Ruby::UnannotatedEmptyCollection.new(node: node)
            add_typing node, type: AST::Builtin::Array.instance_type(AST::Builtin.any_type)
          end
        else
          if hint
            tuples = select_flatten_types(hint) {|type| type.is_a?(AST::Types::Tuple) } #: Array[AST::Types::Tuple]
            unless tuples.empty?
              tuples.each do |tuple|
                typing.new_child() do |child_typing|
                  if pair = with_new_typing(child_typing).try_tuple_type(node, tuple)
                    return pair.with(constr: pair.constr.save_typing)
                  end
                end
              end
            end
          end

          if hint
            arrays = select_flatten_types(hint) {|type| AST::Builtin::Array.instance_type?(type) } #: Array[AST::Types::Name::Instance]
            unless arrays.empty?
              arrays.each do |array|
                typing.new_child() do |child_typing|
                  pair = with_new_typing(child_typing).try_array_type(node, array)
                  if pair.constr.check_relation(sub_type: pair.type, super_type: hint).success?
                    return pair.with(constr: pair.constr.save_typing)
                  end
                end
              end
            end
          end

          try_array_type(node, nil)
        end
      end

    when :and
      yield_self do
        left_node, right_node = node.children

        left_type, constr, left_context = synthesize(left_node, hint: hint, condition: true).to_ary

        interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
        left_truthy, left_falsy = interpreter.eval(env: left_context.type_env, node: left_node)

        if left_type.is_a?(AST::Types::Logic::Env)
          left_type = left_type.type
        end

        right_type, constr, right_context =
          constr
            .update_type_env { left_truthy.env }
            .tap {|constr| typing.cursor_context.set_node_context(right_node, constr.context) }
            .for_branch(right_node)
            .synthesize(right_node, hint: hint, condition: true).to_ary

        right_truthy, right_falsy = interpreter.eval(env: right_context.type_env, node: right_node)

        case
        when left_truthy.unreachable
          # Always left_falsy
          env = left_falsy.env
          type = left_falsy.type
        when left_falsy.unreachable
          # Always left_truthy ==> right
          env = right_context.type_env
          type = right_type
        when right_truthy.unreachable && right_falsy.unreachable
          env = left_falsy.env
          type = left_falsy.type
        else
          env = context.type_env.join(left_falsy.env, right_context.type_env)
          type = union_type(left_falsy.type, right_type)

          unless type.is_a?(AST::Types::Any)
            if check_relation(sub_type: type, super_type: AST::Types::Boolean.instance).success?
              type = AST::Types::Boolean.instance
            end
          end
        end

        if condition
          type = AST::Types::Logic::Env.new(
            truthy: right_truthy.env,
            falsy: context.type_env.join(left_falsy.env, right_falsy.env),
            type: type
          )
        end

        constr.update_type_env { env }.add_typing(node, type: type)
      end

    when :or
      yield_self do
        left_node, right_node = node.children

        if hint
          left_hint = union_type_unify(hint, AST::Builtin.nil_type, AST::Builtin.false_type)
        end
        left_type, constr, left_context = synthesize(left_node, hint: left_hint, condition: true).to_ary

        interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
        left_truthy, left_falsy = interpreter.eval(env: left_context.type_env, node: left_node)

        if left_type.is_a?(AST::Types::Logic::Env)
          left_type = left_type.type
        end

        right_type, constr, right_context =
          constr
            .update_type_env { left_falsy.env }
            .tap {|constr| typing.cursor_context.set_node_context(right_node, constr.context) }
            .for_branch(right_node)
            .synthesize(right_node, hint: left_truthy.type, condition: true).to_ary

        right_truthy, right_falsy = interpreter.eval(env: right_context.type_env, node: right_node)

        case
        when left_falsy.unreachable
          env = left_truthy.env
          type = left_truthy.type
        when left_truthy.unreachable
          # Always left_falsy ==> right
          env = right_context.type_env
          type = right_type
        when right_truthy.unreachable && right_falsy.unreachable
          env = left_truthy.env
          type = left_truthy.type
        else
          env = context.type_env.join(left_truthy.env, right_context.type_env)
          type = union_type(left_truthy.type, right_type)

          unless type.is_a?(AST::Types::Any)
            if check_relation(sub_type: type, super_type: AST::Types::Boolean.instance).success?
              type = AST::Types::Boolean.instance
            end
          end
        end

        if condition
          type = AST::Types::Logic::Env.new(
            truthy: context.type_env.join(left_truthy.env, right_truthy.env),
            falsy: right_falsy.env,
            type: type
          )
        end

        constr.update_type_env { env }.add_typing(node, type: type)
      end

    when :if
      yield_self do
        cond, true_clause, false_clause = node.children

        cond_type, constr = synthesize(cond, condition: true).to_ary
        interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: constr.typing, config: builder_config)
        truthy, falsy = interpreter.eval(env: constr.context.type_env, node: cond)

        if true_clause
          true_pair =
            constr
              .update_type_env { truthy.env }
              .for_branch(true_clause)
              .tap {|constr| typing.cursor_context.set_node_context(true_clause, constr.context) }
              .synthesize(true_clause, hint: hint)
        end

        if false_clause
          false_pair =
            constr
              .update_type_env { falsy.env }
              .for_branch(false_clause)
              .tap {|constr| typing.cursor_context.set_node_context(false_clause, constr.context) }
              .synthesize(false_clause, hint: hint)
        end

        constr = constr.update_type_env do |env|
          envs = [] #: Array[TypeInference::TypeEnv]

          unless truthy.unreachable
            if true_pair
              unless true_pair.type.is_a?(AST::Types::Bot)
                envs << true_pair.context.type_env
              end
            else
              envs << truthy.env
            end
          end

          if false_pair
            unless falsy.unreachable
              unless false_pair.type.is_a?(AST::Types::Bot)
                envs << false_pair.context.type_env
              end
            end
          else
            envs << falsy.env
          end

          env.join(*envs)
        end

        if truthy.unreachable
          if true_clause
            _, _, _, loc = deconstruct_if_node!(node)

            if loc.respond_to?(:keyword)
              condition_loc = loc #: NodeHelper::condition_loc
              case condition_loc.keyword.source
              when "if", "elsif"
                location = condition_loc.begin || condition_loc.keyword
              when "unless"
                # `else` token always exists
                location = condition_loc.else || raise
              end
            else
              location = true_clause.loc.expression
            end

            typing.add_error(
              Diagnostic::Ruby::UnreachableBranch.new(
                node: true_clause,
                location: location || raise
              )
            )
          end
        end

        if falsy.unreachable
          if false_clause
            _, _, _, loc = deconstruct_if_node!(node)

            if loc.respond_to?(:keyword)
              condition_loc = loc #: NodeHelper::condition_loc

              case condition_loc.keyword.source
              when "if", "elsif"
                # `else` token always exists
                location = condition_loc.else || raise
              when "unless"
                location = condition_loc.begin || condition_loc.keyword
              end
            else
              location = false_clause.loc.expression
            end

            typing.add_error(
              Diagnostic::Ruby::UnreachableBranch.new(
                node: false_clause,
                location: location || raise
              )
            )
          end
        end

        node_type = union_type_unify(true_pair&.type || AST::Builtin.nil_type, false_pair&.type || AST::Builtin.nil_type)
        add_typing(node, type: node_type, constr: constr)
      end

    when :case
      yield_self do
        # @type var node: Parser::AST::Node & Parser::AST::_CaseNode

        cond, *whens, els = node.children

        constr = self #: TypeConstruction
        interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)

        if cond
          types, envs = TypeInference::CaseWhen.type_check(constr, node, interpreter, hint: hint, condition: condition)
        else
          branch_results = [] #: Array[Pair]

          condition_constr = constr

          whens.each do |when_clause|
            when_clause_constr = condition_constr
            body_envs = [] #: Array[TypeInference::TypeEnv]

            # @type var tests: Array[Parser::AST::Node]
            # @type var body: Parser::AST::Node?
            *tests, body = when_clause.children

            branch_reachable = false

            tests.each do |test|
              test_type, condition_constr = condition_constr.synthesize(test, condition: true)
              truthy, falsy = interpreter.eval(env: condition_constr.context.type_env, node: test)
              truthy_env = truthy.env
              falsy_env = falsy.env

              condition_constr = condition_constr.update_type_env { falsy_env }
              body_envs << truthy_env

              branch_reachable ||= !truthy.unreachable
            end

            branch_result =
              if body
                when_clause_constr
                  .for_branch(body)
                  .update_type_env {|env| env.join(*body_envs) }
                  .tap {|constr| typing.cursor_context.set_node_context(body, constr.context) }
                  .synthesize(body, hint: hint)
              else
                Pair.new(type: AST::Builtin.nil_type, constr: when_clause_constr)
              end

            branch_results << branch_result

            unless branch_reachable
              unless branch_result.type.is_a?(AST::Types::Bot)
                typing.add_error(
                  Diagnostic::Ruby::UnreachableValueBranch.new(
                    node: when_clause,
                    type: branch_result.type,
                    location: when_clause.location.keyword || raise
                  )
                )
              end
            end
          end

          if els
            branch_results << condition_constr.synthesize(els, hint: hint)
          end

          types = branch_results.map(&:type)
          envs = branch_results.map {|result| result.constr.context.type_env }

          unless els
            types << AST::Builtin.nil_type
          end
        end

        constr = constr.update_type_env do |env|
          env.join(*envs)
        end

        add_typing(node, type: union_type_unify(*types), constr: constr)
      end

    when :rescue
      yield_self do
        body, *resbodies, else_node = node.children
        body_pair = synthesize(body, hint: hint) if body

        # @type var body_constr: TypeConstruction
        body_constr = if body_pair
                        update_type_env do |env|
                          env.join(env, body_pair.context.type_env)
                        end
                      else
                        self
                      end

        resbody_pairs = resbodies.map do |resbody|
          # @type var exn_classes: Parser::AST::Node
          # @type var assignment: Parser::AST::Node?
          # @type var body: Parser::AST::Node?
          exn_classes, assignment, body = resbody.children

          if exn_classes
            case exn_classes.type
            when :array
              exn_types = exn_classes.children.map {|child| synthesize(child).type }
            else
              Steep.logger.error "Unexpected exception list: #{exn_classes.type}"
            end
          end

          if assignment
            case assignment.type
            when :lvasgn
              var_name = assignment.children[0]
            else
              Steep.logger.error "Unexpected rescue variable assignment: #{assignment.type}"
            end
          end

          resbody_construction = body_constr.for_branch(resbody).update_type_env do |env|
            assignments = {} #: Hash[Symbol, AST::Types::t]

            case
            when exn_classes && var_name && exn_types
              instance_types = exn_types.map do |type|
                type = expand_alias(type)
                case
                when type.is_a?(AST::Types::Name::Singleton)
                  to_instance_type(type)
                else
                  AST::Builtin.any_type
                end
              end

              assignments[var_name] = AST::Types::Union.build(types: instance_types)
            when var_name
              assignments[var_name] = AST::Builtin.any_type
            end

            env.assign_local_variables(assignments)
          end

          if body
            resbody_construction.typing.cursor_context.set_node_context(body, resbody_construction.context)
            resbody_construction.synthesize(body, hint: hint)
          else
            Pair.new(constr: body_constr, type: AST::Builtin.nil_type)
          end
        end

        resbody_pairs.select! do |pair|
          no_subtyping?(sub_type: pair.type, super_type: AST::Types::Bot.instance)
        end

        resbody_types = resbody_pairs.map(&:type)
        resbody_envs = resbody_pairs.map {|pair| pair.context.type_env }

        else_constr = body_pair&.constr || self

        if else_node
          else_type, else_constr = else_constr.for_branch(else_node).synthesize(else_node, hint: hint)
          else_constr
            .update_type_env {|env| env.join(*resbody_envs, env) }
            .add_typing(node, type: union_type(else_type, *resbody_types))
        else
          if resbody_types.empty?
            constr = body_pair ? body_pair.constr : self
            constr.add_typing(node, type: body_pair&.type || AST::Builtin.nil_type)
          else
            update_type_env {|env| env.join(*resbody_envs, else_constr.context.type_env) }
              .add_typing(node, type: union_type(*[body_pair&.type, *resbody_types].compact))
          end
        end
      end

    when :resbody
      yield_self do
        klasses, asgn, body = node.children
        synthesize(klasses) if klasses
        synthesize(asgn) if asgn
        if body
          body_type = synthesize(body, hint: hint).type
          add_typing(node, type: body_type)
        else
          add_typing(node, type: AST::Builtin.nil_type)
        end
      end

    when :ensure
      yield_self do
        body, ensure_body = node.children
        body_type = synthesize(body).type if body
        synthesize(ensure_body) if ensure_body
        if body_type
          add_typing(node, type: body_type)
        else
          add_typing(node, type: AST::Builtin.nil_type)
        end
      end

    when :masgn
      type_masgn(node)

    when :for
      yield_self do
        asgn, collection, body = node.children

        collection_type, constr, collection_context = synthesize(collection).to_ary
        collection_type = expand_self(collection_type)

        if collection_type.is_a?(AST::Types::Any)
          var_type = AST::Builtin.any_type
        else
          if each = calculate_interface(collection_type, :each, private: true)
            method_type = (each.method_types || []).find do |type|
              if type.block
                if type.block.type.params
                  type.block.type.params.first_param
                else
                  true
                end
              end
            end
            if method_type
              if block = method_type.block
                if first_param = block.type&.params&.first_param
                  var_type = first_param.type #: AST::Types::t
                else
                  var_type = AST::Builtin.any_type
                end
              end
            end
          end
        end
        var_name = asgn.children[0] #: Symbol

        if var_type
          if body
            body_constr = constr.update_type_env do |type_env|
              type_env = type_env.assign_local_variables({ var_name => var_type })
              pins = type_env.pin_local_variables(nil)
              type_env.merge(local_variable_types: pins)
            end

            typing.cursor_context.set_body_context(node, body_constr.context)
            _, _, body_context = body_constr.synthesize(body).to_ary

            constr = constr.update_type_env do |env|
              env.join(collection_context.type_env, body_context.type_env)
            end
          else
            constr = self
          end

          add_typing(node, type: collection_type, constr: constr)
        else
          constr = synthesize_children(node, skips: [collection])

          constr.fallback_to_any(node) do
            Diagnostic::Ruby::NoMethod.new(
              node: node,
              method: :each,
              type: collection_type
            )
          end
        end
      end
    when :while, :until
      yield_self do
        cond, body = node.children
        cond_type, constr = synthesize(cond, condition: true).to_ary

        interpreter = TypeInference::LogicTypeInterpreter.new(subtyping: checker, typing: typing, config: builder_config)
        truthy, falsy = interpreter.eval(env: constr.context.type_env, node: cond)
        truthy_env = truthy.env
        falsy_env = falsy.env

        case node.type
        when :while
          body_env, exit_env = truthy_env, falsy_env
        when :until
          exit_env, body_env = truthy_env, falsy_env
        else
          raise
        end

        body_env or raise
        exit_env or raise


        if body
          pins = body_env.pin_local_variables(nil)
          body_env = body_env.merge(local_variable_types: pins)

          _, body_constr =
            constr
              .update_type_env { body_env }
              .for_branch(body, break_context: TypeInference::Context::BreakContext.new(break_type: hint || AST::Builtin.nil_type, next_type: nil))
              .tap {|constr| typing.cursor_context.set_node_context(body, constr.context) }
              .synthesize(body).to_ary

          constr = constr.update_type_env {|env| env.join(exit_env, body_constr.context.type_env) }
        else
          constr = constr.update_type_env { exit_env }
        end

        add_typing(node, type: AST::Builtin.nil_type, constr: constr)
      end

    when :while_post, :until_post
      yield_self do
        cond, body = node.children

        _, cond_constr, = synthesize(cond)

        if body
          for_loop =
            cond_constr
              .update_type_env {|env| env.merge(local_variable_types: env.pin_local_variables(nil)) }
              .for_branch(body, break_context: TypeInference::Context::BreakContext.new(break_type: hint || AST::Builtin.nil_type, next_type: nil))

          typing.cursor_context.set_node_context(body, for_loop.context)
          _, body_constr, body_context = for_loop.synthesize(body)

          constr = cond_constr.update_type_env {|env| env.join(env, body_context.type_env) }

          add_typing(node, type: AST::Builtin.nil_type, constr: constr)
        else
          add_typing(node, type: AST::Builtin.nil_type, constr: cond_constr)
        end
      end

    when :irange, :erange
      begin_node, end_node = node.children

      constr = self
      begin_type, constr = if begin_node
                             constr.synthesize(begin_node).to_ary
                           else
                             [AST::Builtin.nil_type, constr]
                           end
      end_type, constr = if end_node
                           constr.synthesize(end_node).to_ary
                         else
                           [AST::Builtin.nil_type, constr]
                         end

      type = AST::Builtin::Range.instance_type(union_type(begin_type, end_type))
      add_typing(node, type: type, constr: constr)

    when :regexp
      each_child_node(node) do |child|
        synthesize(child)
      end

      add_typing(node, type: AST::Builtin::Regexp.instance_type)

    when :regopt
      # ignore
      add_typing(node, type: AST::Builtin.any_type)

    when :nth_ref
      add_typing(node, type: union_type(AST::Builtin::String.instance_type, AST::Builtin.nil_type))

    when :back_ref
      synthesize(node.updated(:gvar), hint: hint)

    when :or_asgn, :and_asgn
      yield_self do
        asgn, rhs = node.children

        case asgn.type
        when :lvasgn
          type, constr = synthesize(rhs, hint: hint)
          constr.lvasgn(asgn, type)
        when :ivasgn
          type, constr = synthesize(rhs, hint: hint)
          constr.ivasgn(asgn, type)
        when :gvasgn
          type, constr = synthesize(rhs, hint: hint)
          constr.gvasgn(asgn, type)
        when :send
          children = asgn.children.dup
          children[1] = :"#{children[1]}="
          send_arg_nodes = [*children, rhs]
          rhs_ = node.updated(:send, send_arg_nodes)
          node_type = case node.type
                      when :or_asgn
                        :or
                      when :and_asgn
                        :and
                      end
          node_ = node.updated(node_type, [asgn, rhs_])

          synthesize(node_, hint: hint)
        else
          Steep.logger.error { "#{node.type} with #{asgn.type} lhs is not supported"}
          fallback_to_any(node)
        end
      end

    when :defined?
      type_any_rec(node, only_children: true)
      add_typing(node, type: AST::Builtin.optional(AST::Builtin::String.instance_type))

    when :gvasgn
      yield_self do
        name, rhs = node.children
        lhs_type = context.type_env[name]
        rhs_type, constr = synthesize(rhs, hint: lhs_type).to_ary

        type, constr = constr.gvasgn(node, rhs_type)

        constr.add_typing(node, type: type)
      end

    when :gvar
      yield_self do
        name = node.children.first
        if type = context.type_env[name]
          add_typing(node, type: type)
        else
          fallback_to_any(node) do
            Diagnostic::Ruby::UnknownGlobalVariable.new(node: node, name: name)
          end
        end
      end

    when :block_pass
      yield_self do
        value_node = node.children[0]

        constr = self #: TypeConstruction

        if value_node
          type, constr = synthesize(value_node, hint: hint)

          if hint.is_a?(AST::Types::Proc) && value_node.type == :sym
            if hint.one_arg?
              if hint.type.params
                # Assumes Symbol#to_proc implementation
                param_type = hint.type.params.required[0]
                case param_type
                when AST::Types::Any
                  type = AST::Types::Any.instance
                else
                  if method = calculate_interface(param_type, private: true)&.methods&.[](value_node.children[0])
                    return_types = method.method_types.filter_map do |method_type|
                      if method_type.type.params.nil? || method_type.type.params.optional?
                        method_type.type.return_type
                      end
                    end

                    unless return_types.empty?
                      type = AST::Types::Proc.new(
                        type: Interface::Function.new(
                          params: Interface::Function::Params.empty.with_first_param(
                            Interface::Function::Params::PositionalParams::Required.new(param_type)
                          ),
                          return_type: return_types[0],
                          location: nil
                        ),
                        block: nil,
                        self_type: nil
                      )
                    end
                  end
                end
              end
            else
              Steep.logger.error "Passing multiple args through Symbol#to_proc is not supported yet"
            end
          end

          case
          when type.is_a?(AST::Types::Proc)
            # nop
          when AST::Builtin::Proc.instance_type?(type)
            # nop
          else
            type = try_convert(type, :to_proc) || type
          end
        else
          # Anonymous block_pass only happens inside method definition
          if block_type = method_context!.block_type
            type = AST::Types::Proc.new(
              type: block_type.type,
              block: nil,
              self_type: block_type.self_type
            )

            if block_type.optional?
              type = union_type(type, AST::Builtin.nil_type)
            end
          else
            type = AST::Builtin.nil_type
          end
        end

        add_typing node, type: type
      end

    when :blockarg
      yield_self do
        each_child_node node do |child|
          synthesize(child)
        end

        add_typing node, type: AST::Builtin.any_type
      end

    when :cvasgn
      name, rhs = node.children

      type, constr = synthesize(rhs, hint: hint)

      var_type =
        if class_vars = module_context.class_variables
          if ty = class_vars[name]
            checker.factory.type(ty)
          end
        end

      if var_type
        result = constr.check_relation(sub_type: type, super_type: var_type)

        if result.success?
          add_typing node, type: type, constr: constr
        else
          fallback_to_any node do
            Diagnostic::Ruby::IncompatibleAssignment.new(
              node: node,
              lhs_type: var_type,
              rhs_type: type,
              result: result
            )
          end
        end
      else
        fallback_to_any(node)
      end

    when :cvar
      name = node.children[0] #: Symbol
      var_type =
        if cvs = module_context.class_variables
          if ty = cvs[name]
            checker.factory.type(ty)
          end
        end

      if var_type
        add_typing node, type: var_type
      else
        fallback_to_any node
      end

    when :alias
      add_typing node, type: AST::Builtin.nil_type

    when :splat
      yield_self do
        typing.add_error(
          Diagnostic::Ruby::UnsupportedSyntax.new(
            node: node,
            message: "Unsupported splat node occurrence"
          )
        )

        each_child_node node do |child|
          synthesize(child)
        end

        add_typing node, type: AST::Builtin.any_type
      end

    when :args
      constr = self #: TypeConstruction

      each_child_node(node) do |child|
        _, constr = constr.synthesize(child)
      end

      add_typing node, type: AST::Builtin.any_type, constr: constr

    when :assertion
      yield_self do
        # @type var as_type: AST::Node::TypeAssertion
        asserted_node, as_type = node.children

        type = as_type.type(module_context.nesting, checker, [])

        case type
        when Array
          type.each do |error|
            typing.add_error(
              Diagnostic::Ruby::RBSError.new(
                error: error,
                node: node,
                location: error.location || raise
              )
            )
          end

          synthesize(asserted_node, hint: hint)

        when nil, RBS::ParsingError
          synthesize(asserted_node, hint: hint)

        else
          actual_type, constr = synthesize(asserted_node, hint: type)

          if no_subtyping?(sub_type: type, super_type: actual_type) && no_subtyping?(sub_type: actual_type, super_type: type)
            typing.add_error(
              Diagnostic::Ruby::FalseAssertion.new(
                node: node,
                assertion_type: type,
                node_type: actual_type
              )
            )
          end

          constr.add_typing(node, type: type)
        end
      end

    when :tapp
      yield_self do
        # @type var tapp: AST::Node::TypeApplication
        sendish, tapp = node.children

        if (array = tapp.types(module_context.nesting, checker, [])).is_a?(Enumerator)
          array.each do |error|
            typing.add_error(
              Diagnostic::Ruby::RBSError.new(
                error: error,
                node: node,
                location: error.location || raise
              )
            )
          end
        end

        type, constr = synthesize_sendish(sendish, hint: hint, tapp: tapp)

        constr.add_typing(node, type: type)
      end

    when :block, :numblock, :send, :csend
      synthesize_sendish(node, hint: hint, tapp: nil)

    when :forwarded_args, :forward_arg
      add_typing(node, type: AST::Builtin.any_type)

    else
      typing.add_error(Diagnostic::Ruby::UnsupportedSyntax.new(node: node))
      add_typing(node, type: AST::Builtin.any_type)

    end.tap do |pair|
      unless pair.is_a?(Pair) && !pair.type.is_a?(Pair)
        # Steep.logger.error { "result = #{pair.inspect}" }
        # Steep.logger.error { "node = #{node.type}" }
        raise "#synthesize should return an instance of Pair: #{pair.class}, node=#{node.inspect}"
      end
    end
  rescue RBS::BaseError => exn
    Steep.logger.warn("hello")
    Steep.logger.warn { "Unexpected RBS error: #{exn.message}" }
    exn.backtrace&.each {|loc| Steep.logger.warn "  #{loc}" }
    typing.add_error(Diagnostic::Ruby::UnexpectedError.new(node: node, error: exn))
    type_any_rec(node)
  rescue StandardError => exn
    Steep.log_error exn
    typing.add_error(Diagnostic::Ruby::UnexpectedError.new(node: node, error: exn))
    type_any_rec(node)
  end
end
```