Skip to content

Commit

Permalink
Refactor restriction mechanism for autocasting (#12014)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Apr 27, 2022
1 parent 1656cb2 commit 57caede
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 89 deletions.
44 changes: 0 additions & 44 deletions src/compiler/crystal/semantic/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -49,34 +49,6 @@ module Crystal
end
end
end

def can_autocast_to?(other_type)
self_type = self.type

case {self_type, other_type}
when {IntegerType, IntegerType}
self_min, self_max = self_type.range
other_min, other_max = other_type.range
other_min <= self_min && self_max <= other_max
when {IntegerType, FloatType}
# Float32 mantissa has 23 bits,
# Float64 mantissa has 52 bits
case self_type.kind
when .i8?, .u8?, .i16?, .u16?
# Less than 23 bits, so convertable to Float32 and Float64 without precision loss
true
when .i32?, .u32?
# Less than 52 bits, so convertable to Float64 without precision loss
other_type.kind.f64?
else
false
end
when {FloatType, FloatType}
self_type.kind.f32? && other_type.kind.f64?
else
false
end
end
end

class Var
Expand Down Expand Up @@ -859,22 +831,6 @@ module Crystal
end
end

class NumberLiteral
def can_autocast_to?(other_type)
case {self.type, other_type}
when {IntegerType, IntegerType}
min, max = other_type.range
min <= integer_value <= max
when {IntegerType, FloatType}
true
when {FloatType, FloatType}
true
else
false
end
end
end

# Fictitious node to mean a location in code shouldn't be reached.
# This is used in the implicit `else` branch of a case.
class Unreachable < ASTNode
Expand Down
94 changes: 49 additions & 45 deletions src/compiler/crystal/semantic/restrictions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1346,68 +1346,72 @@ module Crystal
end
end

class NumberAutocastType
class AutocastType
# Returns true if the AST node associated with `self` denotes a value of the
# given *type*.
def matches_exactly?(type : Type) : Bool
false
end

# Returns true if the AST node associated with `self` denotes a value that
# may be interpreted in the given *type*, but is itself not of that type.
def matches_partially?(type : Type) : Bool
false
end

def restrict(other, context)
if other.is_a?(IntegerType) || other.is_a?(FloatType)
# Check for an exact match, which can't produce an ambiguous call
if literal.type == other
if other.is_a?(Type)
if matches_exactly?(other)
set_exact_match(other)
other
elsif !exact_match? && literal.can_autocast_to?(other)
return other
elsif !exact_match? && matches_partially?(other)
add_match(other)
other
else
literal.type.restrict(other, context)
return other
end
else
type = literal.type.restrict(other, context) ||
super(other, context)
if type == self
type = @match || literal.type
end
type
end

literal_type = literal.type?
type = literal_type.try(&.restrict(other, context)) || super(other, context)
if type == self
# if *other* is an AST node (e.g. `Path`) or a complex type (e.g.
# `UnionType`), `@match` may be set from recursive calls to `#restrict`,
# so we propagate any exact matches found during those calls
type = @match || literal_type
end
type
end

def compatible_with?(type)
literal.type == type || literal.can_autocast_to?(type)
matches_exactly?(type) || matches_partially?(type)
end
end

class SymbolAutocastType
def restrict(other, context)
case other
when SymbolType
set_exact_match(other)
other
when EnumType
if !exact_match? && other.find_member(literal.value)
add_match(other)
other
else
literal.type.restrict(other, context)
end
else
type = literal.type.restrict(other, context) ||
super(other, context)
if type == self
type = @match || literal.type
end
type
end
class NumberAutocastType
def matches_exactly?(type : IntegerType | FloatType) : Bool
literal.type == type
end

def compatible_with?(type)
case type
when SymbolType
true
when EnumType
!!(type.find_member(literal.value))
def matches_partially?(type : IntegerType | FloatType) : Bool
literal = self.literal

if literal.is_a?(NumberLiteral)
literal.representable_in?(type)
else
false
literal_type = literal.type
(literal_type.is_a?(IntegerType) || literal_type.is_a?(FloatType)) && literal_type.subset_of?(type)
end
end
end

class SymbolAutocastType
def matches_exactly?(type : SymbolType) : Bool
true
end

def matches_partially?(type : EnumType) : Bool
!type.find_member(literal.value).nil?
end
end
end

private def get_generic_type(node, context)
Expand Down
19 changes: 19 additions & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,25 @@ module Crystal
end
end

# Returns true if this literal is representable in the *other_type*. Used to
# define number literal autocasting.
#
# TODO: if *other_type* is a `FloatType` then precision loss and overflow
# may occur (#11710)
def representable_in?(other_type)
case {self.type, other_type}
when {IntegerType, IntegerType}
min, max = other_type.range
min <= integer_value <= max
when {IntegerType, FloatType}
true
when {FloatType, FloatType}
true
else
false
end
end

def clone_without_location
NumberLiteral.new(@value, @kind)
end
Expand Down
37 changes: 37 additions & 0 deletions src/compiler/crystal/types.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,31 @@ module Crystal
raise "Bug: called 'range' for non-integer literal"
end
end

# Returns true if every _finite_ member of this type is also exactly
# representable in the *other_type*. Used to define legal autocasts of
# number-typed variables
def subset_of?(other_type : IntegerType) : Bool
self_min, self_max = self.range
other_min, other_max = other_type.range
other_min <= self_min && self_max <= other_max
end

# :ditto:
def subset_of?(other_type : FloatType) : Bool
# Float32 mantissa has 23 bits,
# Float64 mantissa has 52 bits
case kind
when .i8?, .u8?, .i16?, .u16?
# Less than 23 bits, so convertable to Float32 and Float64 without precision loss
true
when .i32?, .u32?
# Less than 52 bits, so convertable to Float64 without precision loss
other_type.kind.f64?
else
false
end
end
end

class FloatType < PrimitiveType
Expand All @@ -1359,6 +1384,18 @@ module Crystal
raise "Bug: called 'range' for non-float literal"
end
end

# Returns true if every _finite_ member of this type is also exactly
# representable in the *other_type*. Used to define legal autocasts of
# number-typed variables.
def subset_of?(other_type : IntegerType) : Bool
false
end

# :ditto:
def subset_of?(other_type : FloatType) : Bool
kind.f32? && other_type.kind.f64?
end
end

class SymbolType < PrimitiveType
Expand Down

0 comments on commit 57caede

Please sign in to comment.