diff --git a/benchmark/benchmark_resolve_rails_env.rb b/benchmark/benchmark_resolve_rails_env.rb new file mode 100644 index 0000000000..42c6ae0fba --- /dev/null +++ b/benchmark/benchmark_resolve_rails_env.rb @@ -0,0 +1,25 @@ +require_relative './utils' + +require 'benchmark/ips' + +# `resolve_type_names` absolutizes every type name in the environment. The +# first resolve must do that work, but long-running clients such as Steep +# re-resolve on every edit: they unload the edited source, re-add it, and +# resolve again. This benchmark reproduces that edit cycle -- unload one +# source, add it back, then resolve -- which is the path that benefits from +# reusing declarations whose type names did not change. + +tmpdir = prepare_collection! + +base_env = new_rails_env(tmpdir) +sample_source = base_env.each_rbs_source.first or raise + +Benchmark.ips do |x| + x.time = 10 + + x.report("resolve_type_names") do + env = base_env.unload([sample_source.buffer]) + env.add_source(sample_source) + env.resolve_type_names + end +end diff --git a/benchmark/memory_resolve_rails_env.rb b/benchmark/memory_resolve_rails_env.rb new file mode 100644 index 0000000000..e058b9217f --- /dev/null +++ b/benchmark/memory_resolve_rails_env.rb @@ -0,0 +1,23 @@ +require_relative './utils' + +require 'memory_profiler' + +# See benchmark_resolve_type_names.rb for the scenario. Here we profile the +# allocations of a single edit-cycle resolve: one source is unloaded and added +# back (not profiled), then `resolve_type_names` is profiled on its own. + +tmpdir = prepare_collection! + +base_env = new_rails_env(tmpdir) +sample_source = base_env.each_rbs_source.first or raise + +env = base_env.unload([sample_source.buffer]) +env.add_source(sample_source) + +_ = resolved = nil + +r = MemoryProfiler.report do + resolved = env.resolve_type_names +end + +r.pretty_print diff --git a/benchmark/utils.rb b/benchmark/utils.rb index d96044281a..5b5f890e3a 100644 --- a/benchmark/utils.rb +++ b/benchmark/utils.rb @@ -1,5 +1,6 @@ require "rbs" require 'tmpdir' +require 'pathname' def prepare_collection! tmpdir = Pathname(Dir.mktmpdir) diff --git a/lib/rbs.rb b/lib/rbs.rb index bbc8c8382c..7211652144 100644 --- a/lib/rbs.rb +++ b/lib/rbs.rb @@ -102,5 +102,51 @@ def print_warning() logger.warn { message } end end + + # Internal helper for `map_type_name` / `map_type` / `resolve_*` paths + # in this gem. The given block is invoked for every element. Returns + # the input array unchanged (the same object) when every mapped result + # is `equal?` to its source; otherwise returns a fresh array with the + # changed elements substituted in. Callers detect a no-op by comparing + # the return value with the input via `equal?`, which avoids + # allocating a `[mapped, changed]` tuple on every invocation. + def map_if_changed(array, &) + return array if array.empty? + + result = array + changed = false + array.each_with_index do |element, i| + new_element = yield(element) + next if new_element.equal?(element) + + unless changed + result = array.dup + changed = true + end + result[i] = new_element + end + result + end + + # Hash counterpart of `map_if_changed`: transforms values through the + # block and returns the receiver unchanged when every value identity + # is preserved. + def transform_values_if_changed(hash, &) + return hash if hash.empty? + + result = hash + changed = false + hash.each do |key, value| + new_value = yield(value) + next if new_value.equal?(value) + + unless changed + result = hash.dup + changed = true + end + result[key] = new_value + end + result + end end end diff --git a/lib/rbs/ast/type_param.rb b/lib/rbs/ast/type_param.rb index b4f599b94a..33cbda9d82 100644 --- a/lib/rbs/ast/type_param.rb +++ b/lib/rbs/ast/type_param.rb @@ -67,25 +67,23 @@ def to_json(state = JSON::State.new) end def map_type(&block) - if b = upper_bound_type - _upper_bound_type = yield(b) - end - - if b = lower_bound_type - _lower_bound_type = yield(b) - end + new_upper_bound_type = upper_bound_type ? yield(upper_bound_type) : nil + new_lower_bound_type = lower_bound_type ? yield(lower_bound_type) : nil + new_default_type = default_type ? yield(default_type) : nil - if dt = default_type - _default_type = yield(dt) + if new_upper_bound_type.equal?(upper_bound_type) && + new_lower_bound_type.equal?(lower_bound_type) && + new_default_type.equal?(default_type) + return self end TypeParam.new( name: name, variance: variance, - upper_bound: _upper_bound_type, - lower_bound: _lower_bound_type, + upper_bound: new_upper_bound_type, + lower_bound: new_lower_bound_type, location: location, - default_type: _default_type + default_type: new_default_type ).unchecked!(unchecked?) end diff --git a/lib/rbs/environment.rb b/lib/rbs/environment.rb index 0fb4347142..4419b2941c 100644 --- a/lib/rbs/environment.rb +++ b/lib/rbs/environment.rb @@ -508,7 +508,7 @@ def resolve_signature(resolver, table, dirs, decls, only: nil) end end - decls = decls.map do |decl| + decls = RBS.map_if_changed(decls) do |decl| if only && !only.member?(decl) decl else @@ -541,7 +541,7 @@ def resolve_type_names(only: nil) end each_ruby_source do |source| - decls = source.declarations.map do |decl| + decls = RBS.map_if_changed(source.declarations) do |decl| if only if only.include?(decl) resolve_ruby_decl(resolver, decl, context: nil, prefix: Namespace.root) @@ -577,9 +577,11 @@ def append_context(context, decl) def resolve_declaration(resolver, map, decl, context:, prefix:) if decl.is_a?(AST::Declarations::Global) # @type var decl: AST::Declarations::Global + new_type = absolute_type(resolver, map, decl.type, context: nil) + return decl if new_type.equal?(decl.type) return AST::Declarations::Global.new( name: decl.name, - type: absolute_type(resolver, map, decl.type, context: nil), + type: new_type, location: decl.location, comment: decl.comment, annotations: decl.annotations @@ -592,32 +594,42 @@ def resolve_declaration(resolver, map, decl, context:, prefix:) inner_context = append_context(outer_context, decl) prefix_ = prefix + decl.name.to_namespace - AST::Declarations::Class.new( - name: decl.name.with_prefix(prefix), - type_params: resolve_type_params(resolver, map, decl.type_params, context: inner_context), - super_class: decl.super_class&.yield_self do |super_class| + new_name = decl.name.with_prefix(prefix) + new_type_params = resolve_type_params(resolver, map, decl.type_params, context: inner_context) + new_super_class = decl.super_class&.then do |super_class| + new_super_name = absolute_type_name(resolver, map, super_class.name, context: outer_context) + new_super_args = RBS.map_if_changed(super_class.args) {|type| absolute_type(resolver, map, type, context: outer_context) } + if new_super_name.equal?(super_class.name) && new_super_args.equal?(super_class.args) + super_class + else AST::Declarations::Class::Super.new( - name: absolute_type_name(resolver, map, super_class.name, context: outer_context), - args: super_class.args.map {|type| absolute_type(resolver, map, type, context: outer_context) }, + name: new_super_name, + args: new_super_args, location: super_class.location ) - end, - members: decl.members.map do |member| - case member - when AST::Members::Base - resolve_member(resolver, map, member, context: inner_context) - when AST::Declarations::Base - resolve_declaration( - resolver, - map, - member, - context: inner_context, - prefix: prefix_ - ) - else - raise - end - end, + end + end + new_members = RBS.map_if_changed(decl.members) do |member| + case member + when AST::Members::Base + resolve_member(resolver, map, member, context: inner_context) + when AST::Declarations::Base + resolve_declaration(resolver, map, member, context: inner_context, prefix: prefix_) + else + raise + end + end + if new_name.equal?(decl.name) && + new_type_params.equal?(decl.type_params) && + new_super_class.equal?(decl.super_class) && + new_members.equal?(decl.members) + return decl + end + AST::Declarations::Class.new( + name: new_name, + type_params: new_type_params, + super_class: new_super_class, + members: new_members, location: decl.location, annotations: decl.annotations, comment: decl.comment @@ -628,81 +640,122 @@ def resolve_declaration(resolver, map, decl, context:, prefix:) inner_context = append_context(outer_context, decl) prefix_ = prefix + decl.name.to_namespace - AST::Declarations::Module.new( - name: decl.name.with_prefix(prefix), - type_params: resolve_type_params(resolver, map, decl.type_params, context: inner_context), - self_types: decl.self_types.map do |module_self| + new_name = decl.name.with_prefix(prefix) + new_type_params = resolve_type_params(resolver, map, decl.type_params, context: inner_context) + new_self_types = RBS.map_if_changed(decl.self_types) do |module_self| + new_self_name = absolute_type_name(resolver, map, module_self.name, context: inner_context) + new_self_args = RBS.map_if_changed(module_self.args) {|type| absolute_type(resolver, map, type, context: inner_context) } + if new_self_name.equal?(module_self.name) && new_self_args.equal?(module_self.args) + module_self + else AST::Declarations::Module::Self.new( - name: absolute_type_name(resolver, map, module_self.name, context: inner_context), - args: module_self.args.map {|type| absolute_type(resolver, map, type, context: inner_context) }, + name: new_self_name, + args: new_self_args, location: module_self.location ) - end, - members: decl.members.map do |member| - case member - when AST::Members::Base - resolve_member(resolver, map, member, context: inner_context) - when AST::Declarations::Base - resolve_declaration( - resolver, - map, - member, - context: inner_context, - prefix: prefix_ - ) - else - raise - end - end, + end + end + new_members = RBS.map_if_changed(decl.members) do |member| + case member + when AST::Members::Base + resolve_member(resolver, map, member, context: inner_context) + when AST::Declarations::Base + resolve_declaration(resolver, map, member, context: inner_context, prefix: prefix_) + else + raise + end + end + if new_name.equal?(decl.name) && + new_type_params.equal?(decl.type_params) && + new_self_types.equal?(decl.self_types) && + new_members.equal?(decl.members) + return decl + end + AST::Declarations::Module.new( + name: new_name, + type_params: new_type_params, + self_types: new_self_types, + members: new_members, location: decl.location, annotations: decl.annotations, comment: decl.comment ) when AST::Declarations::Interface + new_name = decl.name.with_prefix(prefix) + new_type_params = resolve_type_params(resolver, map, decl.type_params, context: context) + new_members = RBS.map_if_changed(decl.members) do |member| + resolve_member(resolver, map, member, context: context) + end + if new_name.equal?(decl.name) && + new_type_params.equal?(decl.type_params) && + new_members.equal?(decl.members) + return decl + end AST::Declarations::Interface.new( - name: decl.name.with_prefix(prefix), - type_params: resolve_type_params(resolver, map, decl.type_params, context: context), - members: decl.members.map do |member| - resolve_member(resolver, map, member, context: context) - end, + name: new_name, + type_params: new_type_params, + members: new_members, comment: decl.comment, location: decl.location, annotations: decl.annotations ) when AST::Declarations::TypeAlias + new_name = decl.name.with_prefix(prefix) + new_type_params = resolve_type_params(resolver, map, decl.type_params, context: context) + new_type = absolute_type(resolver, map, decl.type, context: context) + if new_name.equal?(decl.name) && + new_type_params.equal?(decl.type_params) && + new_type.equal?(decl.type) + return decl + end AST::Declarations::TypeAlias.new( - name: decl.name.with_prefix(prefix), - type_params: resolve_type_params(resolver, map, decl.type_params, context: context), - type: absolute_type(resolver, map, decl.type, context: context), + name: new_name, + type_params: new_type_params, + type: new_type, location: decl.location, annotations: decl.annotations, comment: decl.comment ) when AST::Declarations::Constant + new_name = decl.name.with_prefix(prefix) + new_type = absolute_type(resolver, map, decl.type, context: context) + if new_name.equal?(decl.name) && new_type.equal?(decl.type) + return decl + end AST::Declarations::Constant.new( - name: decl.name.with_prefix(prefix), - type: absolute_type(resolver, map, decl.type, context: context), + name: new_name, + type: new_type, location: decl.location, comment: decl.comment, annotations: decl.annotations ) when AST::Declarations::ClassAlias + new_name = decl.new_name.with_prefix(prefix) + new_old_name = absolute_type_name(resolver, map, decl.old_name, context: context) + if new_name.equal?(decl.new_name) && new_old_name.equal?(decl.old_name) + return decl + end AST::Declarations::ClassAlias.new( - new_name: decl.new_name.with_prefix(prefix), - old_name: absolute_type_name(resolver, map, decl.old_name, context: context), + new_name: new_name, + old_name: new_old_name, location: decl.location, comment: decl.comment, annotations: decl.annotations ) when AST::Declarations::ModuleAlias + new_name = decl.new_name.with_prefix(prefix) + new_old_name = absolute_type_name(resolver, map, decl.old_name, context: context) + if new_name.equal?(decl.new_name) && new_old_name.equal?(decl.old_name) + return decl + end AST::Declarations::ModuleAlias.new( - new_name: decl.new_name.with_prefix(prefix), - old_name: absolute_type_name(resolver, map, decl.old_name, context: context), + new_name: new_name, + old_name: new_old_name, location: decl.location, comment: decl.comment, annotations: decl.annotations @@ -869,14 +922,19 @@ def resolve_ruby_member(resolver, member, context:) def resolve_member(resolver, map, member, context:) case member when AST::Members::MethodDefinition + new_overloads = RBS.map_if_changed(member.overloads) do |overload| + new_method_type = resolve_method_type(resolver, map, overload.method_type, context: context) + if new_method_type.equal?(overload.method_type) + overload + else + overload.update(method_type: new_method_type) + end + end + return member if new_overloads.equal?(member.overloads) AST::Members::MethodDefinition.new( name: member.name, kind: member.kind, - overloads: member.overloads.map do |overload| - overload.update( - method_type: resolve_method_type(resolver, map, overload.method_type, context: context) - ) - end, + overloads: new_overloads, comment: member.comment, overloading: member.overloading?, annotations: member.annotations, @@ -884,9 +942,11 @@ def resolve_member(resolver, map, member, context:) visibility: member.visibility ) when AST::Members::AttrAccessor + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::AttrAccessor.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, kind: member.kind, annotations: member.annotations, comment: member.comment, @@ -895,9 +955,11 @@ def resolve_member(resolver, map, member, context:) visibility: member.visibility ) when AST::Members::AttrReader + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::AttrReader.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, kind: member.kind, annotations: member.annotations, comment: member.comment, @@ -906,9 +968,11 @@ def resolve_member(resolver, map, member, context:) visibility: member.visibility ) when AST::Members::AttrWriter + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::AttrWriter.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, kind: member.kind, annotations: member.annotations, comment: member.comment, @@ -917,46 +981,61 @@ def resolve_member(resolver, map, member, context:) visibility: member.visibility ) when AST::Members::InstanceVariable + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::InstanceVariable.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, comment: member.comment, location: member.location ) when AST::Members::ClassInstanceVariable + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::ClassInstanceVariable.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, comment: member.comment, location: member.location ) when AST::Members::ClassVariable + new_type = absolute_type(resolver, map, member.type, context: context) + return member if new_type.equal?(member.type) AST::Members::ClassVariable.new( name: member.name, - type: absolute_type(resolver, map, member.type, context: context), + type: new_type, comment: member.comment, location: member.location ) when AST::Members::Include + new_name = absolute_type_name(resolver, map, member.name, context: context) + new_args = RBS.map_if_changed(member.args) {|type| absolute_type(resolver, map, type, context: context) } + return member if new_name.equal?(member.name) && new_args.equal?(member.args) AST::Members::Include.new( - name: absolute_type_name(resolver, map, member.name, context: context), - args: member.args.map {|type| absolute_type(resolver, map, type, context: context) }, + name: new_name, + args: new_args, comment: member.comment, location: member.location, annotations: member.annotations ) when AST::Members::Extend + new_name = absolute_type_name(resolver, map, member.name, context: context) + new_args = RBS.map_if_changed(member.args) {|type| absolute_type(resolver, map, type, context: context) } + return member if new_name.equal?(member.name) && new_args.equal?(member.args) AST::Members::Extend.new( - name: absolute_type_name(resolver, map, member.name, context: context), - args: member.args.map {|type| absolute_type(resolver, map, type, context: context) }, + name: new_name, + args: new_args, comment: member.comment, location: member.location, annotations: member.annotations ) when AST::Members::Prepend + new_name = absolute_type_name(resolver, map, member.name, context: context) + new_args = RBS.map_if_changed(member.args) {|type| absolute_type(resolver, map, type, context: context) } + return member if new_name.equal?(member.name) && new_args.equal?(member.args) AST::Members::Prepend.new( - name: absolute_type_name(resolver, map, member.name, context: context), - args: member.args.map {|type| absolute_type(resolver, map, type, context: context) }, + name: new_name, + args: new_args, comment: member.comment, location: member.location, annotations: member.annotations @@ -975,7 +1054,7 @@ def resolve_method_type(resolver, map, type, context:) end def resolve_type_params(resolver, map, params, context:) - params.map do |param| + RBS.map_if_changed(params) do |param| param.map_type {|type| _ = absolute_type(resolver, map, type, context: context) } end end diff --git a/lib/rbs/method_type.rb b/lib/rbs/method_type.rb index cc676fbd6c..7c7ffb603f 100644 --- a/lib/rbs/method_type.rb +++ b/lib/rbs/method_type.rb @@ -63,24 +63,25 @@ def free_variables(set = Set.new) end def map_type(&block) + new_type = type.map_type(&block) + new_block = self.block&.map_type(&block) + if new_type.equal?(type) && new_block.equal?(self.block) + return self + end self.class.new( type_params: type_params, - type: type.map_type(&block), - block: self.block&.map_type(&block), + type: new_type, + block: new_block, location: location ) end def map_type_bound(&block) - if type_params.empty? - self - else - self.update( - type_params: type_params.map {|param| - param.map_type(&block) - } - ) - end + return self if type_params.empty? + + new_type_params = RBS.map_if_changed(type_params) {|param| param.map_type(&block) } + return self if new_type_params.equal?(type_params) + self.update(type_params: new_type_params) end def each_type(&block) diff --git a/lib/rbs/types.rb b/lib/rbs/types.rb index 9c95949bd7..8d01fc900a 100644 --- a/lib/rbs/types.rb +++ b/lib/rbs/types.rb @@ -299,20 +299,17 @@ def to_s(level = 0) end def map_type_name(&block) - ClassSingleton.new( - name: yield(name, location, self), - args: args.map {|type| type.map_type_name(&block) }, - location: location - ) + new_name = yield(name, location, self) + new_args = RBS.map_if_changed(args) {|type| type.map_type_name(&block) } + return self if new_name.equal?(name) && new_args.equal?(args) + ClassSingleton.new(name: new_name, args: new_args, location: location) end def map_type(&block) if block - ClassSingleton.new( - name: name, - args: args.map {|type| yield type }, - location: location - ) + new_args = RBS.map_if_changed(args) {|type| yield type } + return self if new_args.equal?(args) + ClassSingleton.new(name: name, args: new_args, location: location) else enum_for :map_type end @@ -343,20 +340,17 @@ def sub(s) end def map_type_name(&block) - Interface.new( - name: yield(name, location, self), - args: args.map {|type| type.map_type_name(&block) }, - location: location - ) + new_name = yield(name, location, self) + new_args = RBS.map_if_changed(args) {|type| type.map_type_name(&block) } + return self if new_name.equal?(name) && new_args.equal?(args) + Interface.new(name: new_name, args: new_args, location: location) end def map_type(&block) if block - Interface.new( - name: name, - args: args.map {|type| yield type }, - location: location - ) + new_args = RBS.map_if_changed(args) {|type| yield type } + return self if new_args.equal?(args) + Interface.new(name: name, args: new_args, location: location) else enum_for(:map_type) end @@ -387,20 +381,17 @@ def sub(s) end def map_type_name(&block) - ClassInstance.new( - name: yield(name, location, self), - args: args.map {|type| type.map_type_name(&block) }, - location: location - ) + new_name = yield(name, location, self) + new_args = RBS.map_if_changed(args) {|type| type.map_type_name(&block) } + return self if new_name.equal?(name) && new_args.equal?(args) + ClassInstance.new(name: new_name, args: new_args, location: location) end def map_type(&block) if block - ClassInstance.new( - name: name, - args: args.map {|type| yield type }, - location: location - ) + new_args = RBS.map_if_changed(args) {|type| yield type } + return self if new_args.equal?(args) + ClassInstance.new(name: name, args: new_args, location: location) else enum_for :map_type end @@ -429,20 +420,17 @@ def sub(s) end def map_type_name(&block) - Alias.new( - name: yield(name, location, self), - args: args.map {|arg| arg.map_type_name(&block) }, - location: location - ) + new_name = yield(name, location, self) + new_args = RBS.map_if_changed(args) {|type| type.map_type_name(&block) } + return self if new_name.equal?(name) && new_args.equal?(args) + Alias.new(name: new_name, args: new_args, location: location) end def map_type(&block) if block - Alias.new( - name: name, - args: args.map {|type| yield type }, - location: location - ) + new_args = RBS.map_if_changed(args) {|type| yield type } + return self if new_args.equal?(args) + Alias.new(name: name, args: new_args, location: location) else enum_for :map_type end @@ -504,18 +492,16 @@ def each_type(&block) end def map_type_name(&block) - Tuple.new( - types: types.map {|type| type.map_type_name(&block) }, - location: location - ) + new_types = RBS.map_if_changed(types) {|type| type.map_type_name(&block) } + return self if new_types.equal?(types) + Tuple.new(types: new_types, location: location) end def map_type(&block) if block - Tuple.new( - types: types.map {|type| yield type }, - location: location - ) + new_types = RBS.map_if_changed(types) {|type| yield type } + return self if new_types.equal?(types) + Tuple.new(types: new_types, location: location) else enum_for :map_type end @@ -622,18 +608,26 @@ def each_type(&block) end def map_type_name(&block) - Record.new( - all_fields: all_fields.transform_values {|ty, required| [ty.map_type_name(&block), required] }, - location: location - ) + changed = false + new_all_fields = all_fields.transform_values do |ty, required| + new_ty = ty.map_type_name(&block) + changed ||= !new_ty.equal?(ty) + [new_ty, required] + end #: Hash[key, [t, bool]] + return self unless changed + Record.new(all_fields: new_all_fields, location: location) end def map_type(&block) if block - Record.new( - all_fields: all_fields.transform_values {|type, required| [yield(type), required] }, - location: location - ) + changed = false + new_all_fields = all_fields.transform_values do |type, required| + new_type = yield(type) + changed ||= !new_type.equal?(type) + [new_type, required] + end #: Hash[key, [t, bool]] + return self unless changed + Record.new(all_fields: new_all_fields, location: location) else enum_for :map_type end @@ -708,18 +702,16 @@ def each_type end def map_type_name(&block) - Optional.new( - type: type.map_type_name(&block), - location: location - ) + new_type = type.map_type_name(&block) + return self if new_type.equal?(type) + Optional.new(type: new_type, location: location) end def map_type(&block) if block - Optional.new( - type: yield(type), - location: location - ) + new_type = yield(type) + return self if new_type.equal?(type) + Optional.new(type: new_type, location: location) else enum_for :map_type end @@ -803,17 +795,18 @@ def each_type(&block) def map_type(&block) if block - Union.new(types: types.map(&block), location: location) + new_types = RBS.map_if_changed(types, &block) + return self if new_types.equal?(types) + Union.new(types: new_types, location: location) else enum_for :map_type end end def map_type_name(&block) - Union.new( - types: types.map {|type| type.map_type_name(&block) }, - location: location - ) + new_types = RBS.map_if_changed(types) {|type| type.map_type_name(&block) } + return self if new_types.equal?(types) + Union.new(types: new_types, location: location) end def has_self_type? @@ -886,17 +879,18 @@ def each_type(&block) def map_type(&block) if block - Intersection.new(types: types.map(&block), location: location) + new_types = RBS.map_if_changed(types, &block) + return self if new_types.equal?(types) + Intersection.new(types: new_types, location: location) else enum_for :map_type end end def map_type_name(&block) - Intersection.new( - types: types.map {|type| type.map_type_name(&block) }, - location: location - ) + new_types = RBS.map_if_changed(types) {|type| type.map_type_name(&block) } + return self if new_types.equal?(types) + Intersection.new(types: new_types, location: location) end def has_self_type? @@ -936,7 +930,9 @@ def hash def map_type(&block) if block - Param.new(name: name, type: yield(type), location: location) + new_type = yield(type) + return self if new_type.equal?(type) + Param.new(name: name, type: new_type, location: location) else enum_for :map_type end @@ -1035,37 +1031,41 @@ def free_variables(set = Set.new) def map_type(&block) if block + new_required_positionals = RBS.map_if_changed(required_positionals) {|param| param.map_type(&block) } + new_optional_positionals = RBS.map_if_changed(optional_positionals) {|param| param.map_type(&block) } + new_rest_positionals = rest_positionals&.map_type(&block) + new_trailing_positionals = RBS.map_if_changed(trailing_positionals) {|param| param.map_type(&block) } + new_required_keywords = RBS.transform_values_if_changed(required_keywords) {|param| param.map_type(&block) } + new_optional_keywords = RBS.transform_values_if_changed(optional_keywords) {|param| param.map_type(&block) } + new_rest_keywords = rest_keywords&.map_type(&block) + new_return_type = yield(return_type) + + if new_required_positionals.equal?(required_positionals) && + new_optional_positionals.equal?(optional_positionals) && + new_rest_positionals.equal?(rest_positionals) && + new_trailing_positionals.equal?(trailing_positionals) && + new_required_keywords.equal?(required_keywords) && + new_optional_keywords.equal?(optional_keywords) && + new_rest_keywords.equal?(rest_keywords) && + new_return_type.equal?(return_type) + return self + end + Function.new( - required_positionals: amap(required_positionals) {|param| param.map_type(&block) }, - optional_positionals: amap(optional_positionals) {|param| param.map_type(&block) }, - rest_positionals: rest_positionals&.yield_self {|param| param.map_type(&block) }, - trailing_positionals: amap(trailing_positionals) {|param| param.map_type(&block) }, - required_keywords: hmapv(required_keywords) {|param| param.map_type(&block) }, - optional_keywords: hmapv(optional_keywords) {|param| param.map_type(&block) }, - rest_keywords: rest_keywords&.yield_self {|param| param.map_type(&block) }, - return_type: yield(return_type) + required_positionals: new_required_positionals, + optional_positionals: new_optional_positionals, + rest_positionals: new_rest_positionals, + trailing_positionals: new_trailing_positionals, + required_keywords: new_required_keywords, + optional_keywords: new_optional_keywords, + rest_keywords: new_rest_keywords, + return_type: new_return_type ) else enum_for :map_type end end - def amap(array, &block) - if array.empty? - _ = array - else - array.map(&block) - end - end - - def hmapv(hash, &block) - if hash.empty? - _ = hash - else - hash.transform_values(&block) - end - end - def map_type_name(&block) map_type do |type| type.map_type_name(&block) @@ -1261,16 +1261,18 @@ def free_variables(acc = Set.new) def map_type(&block) if block - update(return_type: yield(return_type)) + new_return_type = yield(return_type) + return self if new_return_type.equal?(return_type) + update(return_type: new_return_type) else enum_for :map_type end end def map_type_name(&block) - UntypedFunction.new( - return_type: return_type.map_type_name(&block) - ) + new_return_type = return_type.map_type_name(&block) + return self if new_return_type.equal?(return_type) + UntypedFunction.new(return_type: new_return_type) end def each_type(&block) @@ -1384,10 +1386,15 @@ def sub(s) end def map_type(&block) + new_type = type.map_type(&block) + new_self_type = self_type ? yield(self_type) : nil + if new_type.equal?(type) && new_self_type.equal?(self_type) + return self + end Block.new( required: required, - type: type.map_type(&block), - self_type: self_type ? yield(self_type) : nil + type: new_type, + self_type: new_self_type ) end end @@ -1485,22 +1492,24 @@ def each_type(&block) end def map_type_name(&block) - Proc.new( - type: type.map_type_name(&block), - block: self.block&.map_type {|type| type.map_type_name(&block) }, - self_type: self_type&.map_type_name(&block), - location: location - ) + new_type = type.map_type_name(&block) + new_block = self.block&.map_type {|type| type.map_type_name(&block) } + new_self_type = self_type&.map_type_name(&block) + if new_type.equal?(type) && new_block.equal?(self.block) && new_self_type.equal?(self_type) + return self + end + Proc.new(type: new_type, block: new_block, self_type: new_self_type, location: location) end def map_type(&block) if block - Proc.new( - type: type.map_type(&block), - block: self.block&.map_type(&block), - self_type: self_type ? yield(self_type) : nil, - location: location - ) + new_type = type.map_type(&block) + new_block = self.block&.map_type(&block) + new_self_type = self_type ? yield(self_type) : nil + if new_type.equal?(type) && new_block.equal?(self.block) && new_self_type.equal?(self_type) + return self + end + Proc.new(type: new_type, block: new_block, self_type: new_self_type, location: location) else enum_for :map_type end diff --git a/sig/rbs.rbs b/sig/rbs.rbs index 2b544de827..2a4bb15a43 100644 --- a/sig/rbs.rbs +++ b/sig/rbs.rbs @@ -11,6 +11,10 @@ module RBS def self.print_warning: () { () -> String } -> void + def self.map_if_changed: [T < Object] (Array[T]) { (T) -> T } -> Array[T] + + def self.transform_values_if_changed: [K, V < Object] (Hash[K, V]) { (V) -> V } -> Hash[K, V] + self.@logger: Logger? self.@logger_output: IO? diff --git a/sig/types.rbs b/sig/types.rbs index 2c806de7aa..b7e256bdf7 100644 --- a/sig/types.rbs +++ b/sig/types.rbs @@ -443,10 +443,6 @@ module RBS def has_keyword?: () -> bool - def amap: [A, B] (Array[A]) { (A) -> B } -> Array[B] - - def hmapv: [X, Y, Z] (Hash[X, Y]) { (Y) -> Z } -> Hash[X, Z] - def has_self_type?: () -> bool def has_classish_type?: () -> bool