Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor restriction mechanism for autocasting #12014

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1339,68 +1339,72 @@ module Crystal
end
end

class NumberAutocastType
class AutocastType
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
# 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
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
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