File: //lib/ruby/gems/3.0.0/gems/typeprof-0.12.0/lib/typeprof/container-type.rb
module TypeProf
class AllocationSite
include Utils::StructuralEquality
def initialize(val, parent = nil)
raise if !val.is_a?(Utils::StructuralEquality) && !val.is_a?(Integer) && !val.is_a?(Symbol)
@val = val
@parent = parent
end
attr_reader :val, :parent
def add_id(val)
AllocationSite.new(val, self)
end
end
class Type # or AbstractValue
# Cell, Array, and Hash are types for global interface, e.g., TypedISeq.
# Do not push such types to local environment, stack, etc.
class ContainerType < Type
def match?(other)
return nil if self.class != other.class
return nil unless @base_type.consistent?(other.base_type)
@elems.match?(other.elems)
end
def each_free_type_variable(&blk)
@elems.each_free_type_variable(&blk)
end
def consistent?(other)
raise "must not be used"
end
def self.create_empty_instance(klass)
base_type = Type::Instance.new(klass)
case klass
when Type::Builtin[:ary] # XXX: check inheritance...
Type::Array.new(Type::Array::Elements.new([], Type.bot), base_type)
when Type::Builtin[:hash]
Type.gen_hash(base_type) {|h| }
else
Type::Cell.new(Type::Cell::Elements.new([Type.bot] * klass.type_params.size), base_type)
end
end
def include_untyped?(scratch)
return true if @base_type.include_untyped?(scratch)
return true if @elems.include_untyped?(scratch)
false
end
end
# The most basic container type for default type parameter class
class Cell < ContainerType
def initialize(elems, base_type)
raise if !elems.is_a?(Cell::Elements)
@elems = elems # Cell::Elements
raise unless base_type
@base_type = base_type
if base_type.klass.type_params.size != elems.elems.size
raise
end
end
attr_reader :elems, :base_type
def inspect
"Type::Cell[#{ @elems.inspect }, base_type: #{ @base_type.inspect }]"
end
def screen_name(scratch)
str = @elems.screen_name(scratch)
if str.start_with?("*")
str = @base_type.screen_name(scratch) + str[1..]
end
str
end
def localize(env, alloc_site, depth)
return env, Type.any if depth <= 0
alloc_site = alloc_site.add_id(:cell).add_id(@base_type)
env, elems = @elems.localize(env, alloc_site, depth)
ty = Local.new(Cell, alloc_site, @base_type)
env = env.deploy_type(alloc_site, elems)
return env, ty
end
def limit_size(limit)
return Type.any if limit <= 0
Cell.new(@elems.limit_size(limit - 1), @base_type)
end
def method_dispatch_info
raise
end
def substitute(subst, depth)
return Type.any if depth <= 0
elems = @elems.substitute(subst, depth)
Cell.new(elems, @base_type)
end
def generate_substitution
subst = {}
tyvars = @base_type.klass.type_params.map {|name,| Type::Var.new(name) }
tyvars.zip(@elems.elems) do |tyvar, elem|
if subst[tyvar]
subst[tyvar] = subst[tyvar].union(elem)
else
subst[tyvar] = elem
end
end
subst
end
class Elements # Cell
include Utils::StructuralEquality
def initialize(elems)
@elems = elems
end
def self.dummy_elements
Elements.new([]) # XXX
end
attr_reader :elems
def to_local_type(id, base_ty)
Type::Local.new(Cell, id, base_ty)
end
def globalize(env, visited, depth)
Elements.new(@elems.map {|ty| ty.globalize(env, visited, depth) })
end
def localize(env, alloc_site, depth)
elems = @elems.map.with_index do |ty, i|
alloc_site2 = alloc_site.add_id(i)
env, ty = ty.localize(env, alloc_site2, depth)
ty
end
return env, Elements.new(elems)
end
def limit_size(limit)
Elements.new(@elems.map {|ty| ty.limit_size(limit) })
end
def screen_name(scratch)
"*[#{ @elems.map {|ty| ty.screen_name(scratch) }.join(", ") }]"
end
def pretty_print(q)
q.group(9, "Elements[", "]") do
q.seplist(@elems) do |elem|
q.pp elem
end
end
end
def match?(other)
return nil if @elems.size != other.elems.size
subst = nil
@elems.zip(other.elems) do |ty0, ty1|
subst2 = Type.match?(ty0, ty1)
return nil unless subst2
subst = Type.merge_substitution(subst, subst2)
end
subst
end
def each_free_type_variable(&blk)
@elems.each do |ty|
ty.each_free_type_variable(&blk)
end
end
def substitute(subst, depth)
Elements.new(@elems.map {|ty| ty.substitute(subst, depth) })
end
def [](idx)
@elems[idx]
end
def update(idx, ty)
Elements.new(Utils.array_update(@elems, idx, @elems[idx].union(ty)))
end
def union(other)
return self if self == other
if @elems.size != other.elems.size
raise "#{ @elems.size } != #{ other.elems.size }"
end
elems = []
@elems.zip(other.elems) do |ty0, ty1|
elems << ty0.union(ty1)
end
Elements.new(elems)
end
def include_untyped?(scratch)
return @elems.any? {|ty| ty.include_untyped?(scratch) }
end
end
end
# Do not insert Array type to local environment, stack, etc.
class Array < ContainerType
def initialize(elems, base_type)
raise unless elems.is_a?(Array::Elements)
@elems = elems # Array::Elements
raise unless base_type
@base_type = base_type
end
attr_reader :elems, :base_type
def inspect
"Type::Array[#{ @elems.inspect }, base_type: #{ @base_type.inspect }]"
#@base_type.inspect
end
def screen_name(scratch)
str = @elems.screen_name(scratch)
if str.start_with?("*")
str = @base_type.screen_name(scratch) + str[1..]
end
str
end
def localize(env, alloc_site, depth)
return env, Type.any if depth <= 0
alloc_site = alloc_site.add_id(:ary).add_id(@base_type)
env, elems = @elems.localize(env, alloc_site, depth - 1)
ty = Local.new(Array, alloc_site, @base_type)
env = env.deploy_type(alloc_site, elems)
return env, ty
end
def limit_size(limit)
return Type.any if limit <= 0
Array.new(@elems.limit_size(limit - 1), @base_type)
end
def method_dispatch_info
raise
end
def substitute(subst, depth)
return Type.any if depth <= 0
elems = @elems.substitute(subst, depth - 1)
Array.new(elems, @base_type)
end
def generate_substitution
{ Type::Var.new(:Elem) => @elems.squash }
end
class Elements # Array
include Utils::StructuralEquality
def initialize(lead_tys, rest_ty = Type.bot)
raise unless lead_tys.all? {|ty| ty.is_a?(Type) }
raise unless rest_ty.is_a?(Type)
@lead_tys, @rest_ty = lead_tys, rest_ty
end
def self.dummy_elements
Elements.new([], Type.any)
end
attr_reader :lead_tys, :rest_ty
def to_local_type(id, base_ty)
Type::Local.new(Array, id, base_ty)
end
def globalize(env, visited, depth)
lead_tys = []
@lead_tys.each do |ty|
lead_tys << ty.globalize(env, visited, depth)
end
rest_ty = @rest_ty&.globalize(env, visited, depth)
Elements.new(lead_tys, rest_ty)
end
def localize(env, alloc_site, depth)
lead_tys = @lead_tys.map.with_index do |ty, i|
alloc_site2 = alloc_site.add_id(i)
env, ty = ty.localize(env, alloc_site2, depth)
ty
end
alloc_site_rest = alloc_site.add_id(:rest)
env, rest_ty = @rest_ty.localize(env, alloc_site_rest, depth)
return env, Elements.new(lead_tys, rest_ty)
end
def limit_size(limit)
Elements.new(@lead_tys.map {|ty| ty.limit_size(limit) }, @rest_ty.limit_size(limit))
end
def screen_name(scratch)
if @rest_ty == Type.bot
if @lead_tys.empty?
return "Array[bot]" # RBS does not allow an empty tuple "[]"
end
s = @lead_tys.map do |ty|
ty.screen_name(scratch)
end
s << "*" + @rest_ty.screen_name(scratch) if @rest_ty != Type.bot
return "[#{ s.join(", ") }]"
end
"*[#{ squash.screen_name(scratch) }]"
end
def pretty_print(q)
q.group(9, "Elements[", "]") do
q.seplist(@lead_tys + [@rest_ty]) do |elem|
q.pp elem
end
end
end
def match?(other)
n = [@lead_tys.size, other.lead_tys.size].min
rest_ty1 = @lead_tys[n..].inject(@rest_ty) {|ty1, ty2| ty1.union(ty2) }
rest_ty2 = other.lead_tys[n..].inject(other.rest_ty) {|ty1, ty2| ty1.union(ty2) }
subst = nil
(@lead_tys[0, n] + [rest_ty1]).zip(other.lead_tys[0, n] + [rest_ty2]) do |ty0, ty1|
subst2 = Type.match?(ty0, ty1)
return nil unless subst2
subst = Type.merge_substitution(subst, subst2)
end
subst
end
def each_free_type_variable(&blk)
@lead_tys.each do |ty|
ty.each_free_type_variable(&blk)
end
@rest_ty&.each_free_type_variable(&blk)
end
def substitute(subst, depth)
lead_tys = @lead_tys.map {|ty| ty.substitute(subst, depth) }
rest_ty = @rest_ty.substitute(subst, depth)
Elements.new(lead_tys, rest_ty)
end
def squash
@lead_tys.inject(@rest_ty) {|ty1, ty2| ty1.union(ty2) } #.union(Type.nil) # is this needed?
end
def squash_or_any
ty = squash
ty == Type.bot ? Type.any : ty
end
def [](idx)
if idx.is_a?(Range)
if @rest_ty == Type.bot
lead_tys = @lead_tys[idx]
if lead_tys
rest_ty = Type.bot
else
return Type.nil
end
else
b, e = idx.begin, idx.end
b = 0 if !b
if !e
lead_tys = @lead_tys[idx] || []
rest_ty = @rest_ty
elsif b >= 0
if e >= 0
if b <= e
if e < @lead_tys.size
lead_tys = @lead_tys[idx]
rest_ty = Type.bot
else
lead_tys = @lead_tys[idx] || []
rest_ty = @rest_ty
end
else
return Type.nil
end
else
lead_tys = @lead_tys[idx] || []
e = idx.exclude_end? ? e : e == -1 ? @lead_tys.size : e + 1
rest_ty = (@lead_tys[e + 1..] || []).inject(@rest_ty) {|ty0, ty1| ty0.union(ty1) }
end
else
lead_tys = []
if e >= 0
rest_ty = e < @lead_tys.size ? Type.bot : @rest_ty
range = [0, @lead_tys.size + b].max .. (idx.exclude_end? ? e - 1 : e)
rest_ty = @lead_tys[range].inject(rest_ty) {|ty0, ty1| ty0.union(ty1) }
else
if b <= e
range = [0, @lead_tys.size + b].max .. (idx.exclude_end? ? e - 1 : e)
rest_ty = @lead_tys[range].inject(@rest_ty) {|ty0, ty1| ty0.union(ty1) }
else
return Type.nil
end
end
end
end
base_ty = Type::Instance.new(Type::Builtin[:ary])
Array.new(Elements.new(lead_tys, rest_ty), base_ty)
elsif idx >= 0
if idx < @lead_tys.size
@lead_tys[idx]
elsif @rest_ty == Type.bot
Type.nil
else
@rest_ty
end
else
i = @lead_tys.size + idx
i = [i, 0].max
ty = @rest_ty
@lead_tys[i..].each do |ty2|
ty = ty.union(ty2)
end
ty
end
end
def update(idx, ty)
if idx
if idx >= 0
if idx < @lead_tys.size
lead_tys = Utils.array_update(@lead_tys, idx, ty)
Elements.new(lead_tys, @rest_ty)
else
rest_ty = @rest_ty.union(ty)
Elements.new(@lead_tys, rest_ty)
end
else
i = @lead_tys.size + idx
if @rest_ty == Type.bot
if i >= 0
lead_tys = Utils.array_update(@lead_tys, i, ty)
Elements.new(lead_tys, Type.bot)
else
# TODO: out of bound? should we emit an error?
Elements.new(@lead_tys, Type.bot)
end
else
i = [i, 0].max
lead_tys = @lead_tys[0, i] + @lead_tys[i..].map {|ty2| ty2.union(ty) }
rest_ty = @rest_ty.union(ty)
Elements.new(@lead_tys, rest_ty)
end
end
else
lead_tys = @lead_tys.map {|ty1| ty1.union(ty) }
rest_ty = @rest_ty.union(ty)
Elements.new(lead_tys, rest_ty)
end
end
def append(ty)
if @rest_ty == Type.bot
if @lead_tys.size < 5 # XXX: should be configurable, or ...?
lead_tys = @lead_tys + [ty]
Elements.new(lead_tys, @rest_ty)
else
Elements.new(@lead_tys, ty)
end
else
Elements.new(@lead_tys, @rest_ty.union(ty))
end
end
def union(other)
return self if self == other
raise "Hash::Elements merge Array::Elements" if other.is_a?(Hash::Elements)
lead_count = [@lead_tys.size, other.lead_tys.size].min
lead_tys = (0...lead_count).map do |i|
@lead_tys[i].union(other.lead_tys[i])
end
rest_ty = @rest_ty.union(other.rest_ty)
(@lead_tys[lead_count..-1] + other.lead_tys[lead_count..-1]).each do |ty|
rest_ty = rest_ty.union(ty)
end
Elements.new(lead_tys, rest_ty)
end
def take_first(num)
base_ty = Type::Instance.new(Type::Builtin[:ary])
if @lead_tys.size >= num
lead_tys = @lead_tys[0, num]
rest_ary_ty = Array.new(Elements.new(@lead_tys[num..-1], @rest_ty), base_ty)
return lead_tys, rest_ary_ty
else
lead_tys = @lead_tys.dup
until lead_tys.size == num
# .union(Type.nil) is needed for `a, b, c = [42]` to assign nil to b and c
lead_tys << @rest_ty.union(Type.nil)
end
rest_ary_ty = Array.new(Elements.new([], @rest_ty), base_ty)
return lead_tys, rest_ary_ty
end
end
def take_last(num)
base_ty = Type::Instance.new(Type::Builtin[:ary])
if @rest_ty == Type.bot
if @lead_tys.size >= num
following_tys = @lead_tys[-num, num]
rest_ary_ty = Array.new(Elements.new(@lead_tys[0...-num], Type.bot), base_ty)
return rest_ary_ty, following_tys
else
following_tys = @lead_tys[-num, num] || []
until following_tys.size == num
following_tys.unshift(Type.nil)
end
rest_ary_ty = Array.new(Elements.new([], Type.bot), base_ty)
return rest_ary_ty, following_tys
end
else
lead_tys = @lead_tys.dup
last_ty = rest_ty
following_tys = []
until following_tys.size == num
last_ty = last_ty.union(lead_tys.pop) unless lead_tys.empty?
following_tys.unshift(last_ty)
end
rest_ty = lead_tys.inject(last_ty) {|ty1, ty2| ty1.union(ty2) }
rest_ary_ty = Array.new(Elements.new([], rest_ty), base_ty)
return rest_ary_ty, following_tys
end
end
def include_untyped?(scratch)
return true if @lead_tys.any? {|ty| ty.include_untyped?(scratch) }
return true if @rest_ty.include_untyped?(scratch)
false
end
end
end
class Hash < ContainerType
def initialize(elems, base_type)
@elems = elems
raise unless elems
@base_type = base_type
end
attr_reader :elems, :base_type
def inspect
"Type::Hash#{ @elems.inspect }"
end
def screen_name(scratch)
@elems.screen_name(scratch)
end
def localize(env, alloc_site, depth)
return env, Type.any if depth <= 0
alloc_site = alloc_site.add_id(:hash).add_id(@base_type)
env, elems = @elems.localize(env, alloc_site, depth - 1)
ty = Local.new(Hash, alloc_site, @base_type)
env = env.deploy_type(alloc_site, elems)
return env, ty
end
def limit_size(limit)
return Type.any if limit <= 0
Hash.new(@elems.limit_size(limit - 1), @base_type)
end
def method_dispatch_info
raise
end
def substitute(subst, depth)
return Type.any if depth <= 0
elems = @elems.substitute(subst, depth - 1)
Hash.new(elems, @base_type)
end
def generate_substitution
tyvar_k = Type::Var.new(:K)
tyvar_v = Type::Var.new(:V)
k_ty0, v_ty0 = @elems.squash
# XXX: need to heuristically replace ret type Hash[K, V] with self, instead of conversative type?
{ tyvar_k => k_ty0, tyvar_v => v_ty0 }
end
class Elements # Hash
include Utils::StructuralEquality
def initialize(map_tys)
map_tys.each do |k_ty, v_ty|
raise unless k_ty.is_a?(Type)
raise unless v_ty.is_a?(Type)
raise if k_ty.is_a?(Type::Union)
raise if k_ty.is_a?(Type::Local)
raise if k_ty.is_a?(Type::Array)
raise if k_ty.is_a?(Type::Hash)
end
@map_tys = map_tys
end
def self.dummy_elements
Elements.new({Type.any => Type.any})
end
attr_reader :map_tys
def to_local_type(id, base_ty)
Type::Local.new(Hash, id, base_ty)
end
def globalize(env, visited, depth)
map_tys = {}
@map_tys.each do |k_ty, v_ty|
v_ty = v_ty.globalize(env, visited, depth)
if map_tys[k_ty]
map_tys[k_ty] = map_tys[k_ty].union(v_ty)
else
map_tys[k_ty] = v_ty
end
end
Elements.new(map_tys)
end
def localize(env, alloc_site, depth)
map_tys = @map_tys.to_h do |k_ty, v_ty|
alloc_site2 = alloc_site.add_id(k_ty)
env, v_ty = v_ty.localize(env, alloc_site2, depth)
[k_ty, v_ty]
end
return env, Elements.new(map_tys)
end
def limit_size(limit)
map_tys = {}
@map_tys.each do |k_ty, v_ty|
k_ty = k_ty.limit_size(limit)
v_ty = v_ty.limit_size(limit)
if map_tys[k_ty]
map_tys[k_ty] = map_tys[k_ty].union(v_ty)
else
map_tys[k_ty] = v_ty
end
end
Elements.new(map_tys)
end
def screen_name(scratch)
if !@map_tys.empty? && @map_tys.all? {|k_ty,| k_ty.is_a?(Type::Symbol) }
s = @map_tys.map do |k_ty, v_ty|
v = v_ty.screen_name(scratch)
"#{ k_ty.sym }: #{ v }"
end.join(", ")
"{#{ s }}"
else
k_ty = v_ty = Type.bot
@map_tys.each do |k, v|
k_ty = k_ty.union(k)
v_ty = v_ty.union(v)
end
k_ty = k_ty.screen_name(scratch)
v_ty = v_ty.screen_name(scratch)
"Hash[#{ k_ty }, #{ v_ty }]"
end
end
def pretty_print(q)
q.group(9, "Elements[", "]") do
q.seplist(@map_tys) do |k_ty, v_ty|
q.group do
q.pp k_ty
q.text '=>'
q.group(1) do
q.breakable ''
q.pp v_ty
end
end
end
end
end
def match?(other)
subst = nil
other.map_tys.each do |k1, v1|
subst2 = nil
@map_tys.each do |k0, v0|
subst3 = Type.match?(k0, k1)
if subst3
subst4 = Type.match?(v0, v1)
if subst4
subst2 = Type.merge_substitution(subst2, subst3)
subst2 = Type.merge_substitution(subst2, subst4)
end
end
end
return nil unless subst2
subst = Type.merge_substitution(subst, subst2)
end
subst
end
def each_free_type_variable(&blk)
@map_tys.each do |k, v|
k.each_free_type_variable(&blk)
v.each_free_type_variable(&blk)
end
end
def substitute(subst, depth)
map_tys = {}
@map_tys.each do |k_ty_orig, v_ty_orig|
k_ty = k_ty_orig.substitute(subst, depth)
v_ty = v_ty_orig.substitute(subst, depth)
k_ty.each_child_global do |k_ty|
# This is a temporal hack to mitigate type explosion
k_ty = Type.any if k_ty.is_a?(Type::Array)
k_ty = Type.any if k_ty.is_a?(Type::Hash)
if map_tys[k_ty]
map_tys[k_ty] = map_tys[k_ty].union(v_ty)
else
map_tys[k_ty] = v_ty
end
end
end
Elements.new(map_tys)
end
def squash
all_k_ty, all_v_ty = Type.bot, Type.bot
@map_tys.each do |k_ty, v_ty|
all_k_ty = all_k_ty.union(k_ty)
all_v_ty = all_v_ty.union(v_ty)
end
return all_k_ty, all_v_ty
end
def [](key_ty)
val_ty = Type.bot
@map_tys.each do |k_ty, v_ty|
if Type.match?(k_ty, key_ty)
val_ty = val_ty.union(v_ty)
end
end
val_ty
end
def update(idx, ty)
map_tys = @map_tys.dup
idx.each_child_global do |idx|
# This is a temporal hack to mitigate type explosion
idx = Type.any if idx.is_a?(Type::Array)
idx = Type.any if idx.is_a?(Type::Hash)
if map_tys[idx]
map_tys[idx] = map_tys[idx].union(ty)
else
map_tys[idx] = ty
end
end
Elements.new(map_tys)
end
def union(other)
return self if self == other
raise "Array::Elements merge Hash::Elements" if other.is_a?(Array::Elements)
map_tys = @map_tys.dup
other.map_tys.each do |k_ty, v_ty|
if map_tys[k_ty]
map_tys[k_ty] = map_tys[k_ty].union(v_ty)
else
map_tys[k_ty] = v_ty
end
end
Elements.new(map_tys)
end
def to_keywords
kw_tys = {}
@map_tys.each do |key_ty, val_ty|
if key_ty.is_a?(Type::Symbol)
kw_tys[key_ty.sym] = val_ty
else
all_val_ty = Type.bot
@map_tys.each do |_key_ty, val_ty|
all_val_ty = all_val_ty.union(val_ty)
end
return { nil => all_val_ty }
end
end
kw_tys
end
def include_untyped?(scratch)
@map_tys.each do |key, val|
return true if key.include_untyped?(scratch)
return true if val.include_untyped?(scratch)
end
false
end
end
end
class Local < ContainerType
def initialize(kind, id, base_type)
@kind = kind
raise if @kind != Cell && @kind != Array && @kind != Hash
@id = id
raise unless base_type
@base_type = base_type
end
attr_reader :kind, :id, :base_type
def inspect
"Type::Local[#{ @kind }, #{ @id }, base_type: #{ @base_type.inspect }]"
end
def screen_name(scratch)
#raise "Local type must not be included in signature"
"Local[#{ @kind }]"
end
def globalize(env, visited, depth)
if visited[self] || depth <= 0
Type.any
else
visited[self] = true
elems = env.get_container_elem_types(@id)
if elems
elems = elems.globalize(env, visited, depth - 1)
else
# TODO: currently out-of-scope array cannot be accessed
elems = @kind::Elements.dummy_elements
end
visited.delete(self)
@kind.new(elems, @base_type)
end
end
def method_dispatch_info
@base_type.method_dispatch_info
end
def update_container_elem_type(subst, env, caller_ep, scratch)
case
when @kind == Cell
tyvars = @base_type.klass.type_params.map {|name,| Type::Var.new(name) }
# XXX: This should be skipped when the called methods belongs to superclass
tyvars.each_with_index do |tyvar, idx|
ty = subst[tyvar]
if ty
env, ty = scratch.localize_type(ty, env, caller_ep)
env = scratch.update_container_elem_types(env, caller_ep, @id, @base_type) do |elems|
elems.update(idx, ty)
end
end
end
when @kind == Array
tyvar_elem = Type::Var.new(:Elem)
if subst[tyvar_elem]
ty = subst[tyvar_elem]
env, ty = scratch.localize_type(ty, env, caller_ep)
env = scratch.update_container_elem_types(env, caller_ep, @id, @base_type) do |elems|
elems.update(nil, ty)
end
end
when @kind == Hash
tyvar_k = Type::Var.new(:K)
tyvar_v = Type::Var.new(:V)
if subst[tyvar_k] && subst[tyvar_v]
k_ty = subst[tyvar_k]
v_ty = subst[tyvar_v]
alloc_site = AllocationSite.new(caller_ep)
env, k_ty = scratch.localize_type(k_ty, env, caller_ep, alloc_site.add_id(:k))
env, v_ty = scratch.localize_type(v_ty, env, caller_ep, alloc_site.add_id(:v))
env = scratch.update_container_elem_types(env, caller_ep, @id, @base_type) do |elems|
elems.update(k_ty, v_ty)
end
end
end
env
end
end
end
end