Proposal: 'T(literal)' should construct T using the appropriate literal protocol if possible


(John McCall) #1

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.


Literal initialization via coercion
Literal initialization via coercion
(Austin Zheng) #2

+1.

The primary advantage is that it aligns the language semantics with how
most programmers expect this common C-language-family idiom to behave and
removes a potential source of silently wrong code.

The primary disadvantage is that it introduces special-case behavior to
certain types of initializers (although, to be fair, this special-case
behavior is easily recognizable: unlabeled one-argument initializer with a
literal as the argument).

I think the advantage outweighs the disadvantage.

This problem should be addressed one way or another. I prefer this
solution, but if it is rejected for whatever reason we should at least
explicitly outlaw A(literal) syntax in favor of "literal as A".

Austin

···

On Thu, Jun 2, 2016 at 9:08 AM, John McCall via swift-evolution < swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does *not* attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution using
the standard rules, i.e. considering only single-argument unlabelled
initializers of a type which conforms to IntegerLiteralConvertible. Often
this leads to static ambiguities or, worse, causes the literal to be built
using a default type (such as Int); this may have semantically very
different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's clear
that programmers are going to continue to independently try to use it, so
it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an
*expr-call* with a single, unlabelled argument) where B is an
*expr-literal* or *expr-collection*, if A has type T.Type for some type T
and there is a declared conformance of T to an appropriate literal protocol
for B, then the expression is always resolves as a literal construction of
type T (as if the expression were written "B as A") rather than as a
general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by
wrapping the literal in parentheses. This might seem distasteful; it would
be easy enough to allow the form of B to include extra parentheses. It's
potentially useful to have a way to suppress this rule and get a normal
construction, but there are several other ways of getting that effect, such
as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional conformance.
This permits the applicability of the rule to be decided without having to
first decide the type arguments, which greatly simplifies the type-checking
problem (and may be necessary for soundness; I didn't explore this in
depth, but it certainly feels like a very nasty sort of dependence). We
could potentially weaken this for cases where A is a direct type reference
with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but
I think there's some benefit from having a simpler specification, both for
the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Christopher Kornher) #3

+1
I have started using the ‘()’ syntax without even thinking about it. I never knew that it behaved in the way described. Using ‘()’ syntax will be natural for anyone with C++ experience.

···

On Jun 2, 2016, at 10:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #4

I’m not entirely sure what an “expr-collection” is. Does your proposal mean that in this code:
func foo() -> Int {...}
var w = 0
var x = T(foo())
var y = T(w)
var z = T(0)
different initializers would be used for `x`,`y`, and `z`? If so, that seems a potential source of much subtler problems.

I don’t disagree that you’ve identified a potential source of issues, but it’s conceivable that there might be circumstances where the "semantically very different results” are desired. I can’t think of any off the top of my head, but I’m not convinced that means they don’t exist.

So… I’m tentatively -1

- Dave Sweeris

···

On Jun 2, 2016, at 11:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #5

+1 to this. It seems like a very straightforward thing to do.

···

Sent from my iPad

On Jun 2, 2016, at 11:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Russ Bishop) #6

+1 to the proposal; I’m embarrassed to admit I thought it worked this way already.

Any type that wants more control shouldn’t be adopting the literal convertible protocols anyway.

Russ

···

On Jun 2, 2016, at 9:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.


(Brent Royal-Gordon) #7

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

I've seen developers do this; in one memorable case, it resulted in Swift taking a ridiculously long time to typecheck an expression, since the seemingly pinned-down types of the literals had actually become *more* ambiguous, not less.

However, it's not difficult to teach developers to use `as`. Usually what's happening is that their mental model of the language is wrong: they think of `UInt16(foo)` as a cast to a primitive type, and are surprised to learn that it's actually an initializer on a struct and they're initializing an instance. Learning this helps them understand how the language works, what the difference is between initializers and `as`, and how they can write the same things they see in the standard library types.

I think *actually* turning this into magic would be counterproductive. The better solution is to make the compiler replace me in that story, by having it emit a warning with a fix-it. It keeps initializer calls meaning exactly what they say. (And it doesn't require an evolution proposal to do, since you can add a warning with a mere bug.)

  UInt16(42)
  ^~~~~~ ^~
  Use of initializer with integer literal does not cast '42' to 'UInt16'
  Fix-It: Replace with '42 as UInt16'

···

--
Brent Royal-Gordon
Architechies


(Tony Allevato) #8

+1. As someone who thought "var x: Int32 = 0" and "var x = Int32(0)" were
equivalent, this is very good to know (and very good to fix).

I'm starting to wonder now if some of the times I've hit "expression was
too complex" errors with large 64-bit multi-term expressions with literals
were caused by coercions happening that I didn't realize.

···

On Thu, Jun 2, 2016 at 9:31 AM John McCall via swift-evolution < swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does *not* attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution using
the standard rules, i.e. considering only single-argument unlabelled
initializers of a type which conforms to IntegerLiteralConvertible. Often
this leads to static ambiguities or, worse, causes the literal to be built
using a default type (such as Int); this may have semantically very
different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's clear
that programmers are going to continue to independently try to use it, so
it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an
*expr-call* with a single, unlabelled argument) where B is an
*expr-literal* or *expr-collection*, if A has type T.Type for some type T
and there is a declared conformance of T to an appropriate literal protocol
for B, then the expression is always resolves as a literal construction of
type T (as if the expression were written "B as A") rather than as a
general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by
wrapping the literal in parentheses. This might seem distasteful; it would
be easy enough to allow the form of B to include extra parentheses. It's
potentially useful to have a way to suppress this rule and get a normal
construction, but there are several other ways of getting that effect, such
as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional conformance.
This permits the applicability of the rule to be decided without having to
first decide the type arguments, which greatly simplifies the type-checking
problem (and may be necessary for soundness; I didn't explore this in
depth, but it certainly feels like a very nasty sort of dependence). We
could potentially weaken this for cases where A is a direct type reference
with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but
I think there's some benefit from having a simpler specification, both for
the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Vladimir) #9

Often

> this leads to static ambiguities or, worse, causes the literal to be built
> using a default type (such as Int); this may have semantically very
> different results which are only caught at runtime.

Seems like I'm very slow today.. Could you present a couple of examples where such initialization(like UInt16(7)) can produce some unexpected behavior / error at runtime?

···

On 02.06.2016 19:08, John McCall via swift-evolution wrote:

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does /not/ attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution using
the standard rules, i.e. considering only single-argument unlabelled
initializers of a type which conforms to IntegerLiteralConvertible. Often
this leads to static ambiguities or, worse, causes the literal to be built
using a default type (such as Int); this may have semantically very
different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's clear
that programmers are going to continue to independently try to use it, so
it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an
/expr-call/ with a single, unlabelled argument) where B is
an /expr-literal/ or /expr-collection/, if A has type T.Type for some type
T and there is a declared conformance of T to an appropriate literal
protocol for B, then the expression is always resolves as a literal
construction of type T (as if the expression were written "B as A") rather
than as a general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by
wrapping the literal in parentheses. This might seem distasteful; it would
be easy enough to allow the form of B to include extra parentheses. It's
potentially useful to have a way to suppress this rule and get a normal
construction, but there are several other ways of getting that effect, such
as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional conformance.
This permits the applicability of the rule to be decided without having to
first decide the type arguments, which greatly simplifies the type-checking
problem (and may be necessary for soundness; I didn't explore this in
depth, but it certainly feels like a very nasty sort of dependence). We
could potentially weaken this for cases where A is a direct type reference
with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but
I think there's some benefit from having a simpler specification, both for
the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(L Mihalkovic) #10

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Looking transversally at all literal protocols as this proposes to operates reminds me that the knowledge that a protocol has the right semantic is based on a convention, rather than on conformance. Would it be conceibable to look into something like the following, that all others would specialize.

protocol LiteralConvertible {}

This might offer a stronger identification than the name. It might also be interesting to define an associated type, but that would exclude NilLiteralConvertible.

Note: as compiler expert, I would appreciate your thinking on the notion of formally expressing what might otherwise be a known strong semantic relationship. Is there any incentive to pursue, known disavantages, ...

···

On Jun 2, 2016, at 6:08 PM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #11

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax.

I completely agree that this is a problem that we need to solve. In addition to the trap of using [U]Int64 values on 32-bit targets, it is embarrassing that we reject (on all targets):

  let x = UInt64(0x8000_0000_0000_0000)

and require people to use the less obvious syntax:

  let x = 0x1000_0000_0000_0000 as UInt64

Therefore, I propose that we adopt the following typing rule:

I’m sorry of this has already been covered down-thread (just getting caught up now, and haven’t read it all), but this seems like a LOT of magic in the type checker to solve this problem.

Can’t we just require that literal convertibles implement an initializer that the type checker will already consider to be more specific than any of the other overloads? This would eliminate the need for magic like this in the type checker. Right now, we have this:

public protocol IntegerLiteralConvertible {
  associatedtype IntegerLiteralType : _BuiltinIntegerLiteralConvertible
  init(integerLiteral value: IntegerLiteralType)
}

Change it to be an unlabeled requirement like this probably isn’t enough to make it privileged in the case of ambiguity:

public protocol IntegerLiteralConvertible {
  associatedtype IntegerLiteralType : _BuiltinIntegerLiteralConvertible
  init(_ value: IntegerLiteralType)
}

but perhaps we could have:

public protocol IntegerLiteralConvertible {
  associatedtype IntegerLiteralType : _BuiltinIntegerLiteralConvertible
  init(integerLiteral value: IntegerLiteralType)
  init<T : IntegerLiteralConvertible>(_ value: T)
}

and get the type checker to consider the later one to be a“more specific” match than the other overloads, when confronted with a literal?

-Chris

···

On Jun 2, 2016, at 9:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:


(Dave Abrahams) #12

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution
using the standard rules, i.e. considering only single-argument
unlabelled initializers of a type which conforms to
IntegerLiteralConvertible. Often this leads to static ambiguities or,
worse, causes the literal to be built using a default type (such as
Int); this may have semantically very different results which are only
caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's
clear that programmers are going to continue to independently try to
use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an
expr-call with a single, unlabelled argument) where B is an
expr-literal or expr-collection, if A has type T.Type for some type T
and there is a declared conformance of T to an appropriate literal
protocol for B, then the expression is always resolves as a literal
construction of type T (as if the expression were written "B as A")
rather than as a general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately
known.

I realize this is somewhat tangential, but... IMO this may not be entirely
about literals.

We have a standard that full-width type conversions are written as a
label-free initializer
<https://swift.org/documentation/api-design-guidelines/#type-conversion>.
I believe that is partly responsible for setting up the expectation that
Int(42) works as one would expect. It gets ultra-weird when you can
convert from type A to type B using B(someA) but you can't write
B(someB). We should automatically generate a label-free “copy
initializer” for value types, to complete implementation of the expected
mental model.

···

on Thu Jun 02 2016, John McCall <swift-evolution@swift.org> wrote:

Note that, as specified, it is possible to suppress this typing rule
by wrapping the literal in parentheses. This might seem distasteful;
it would be easy enough to allow the form of B to include extra
parentheses. It's potentially useful to have a way to suppress this
rule and get a normal construction, but there are several other ways
of getting that effect, such as explicitly typing the literal argument
(e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional
conformance. This permits the applicability of the rule to be decided
without having to first decide the type arguments, which greatly
simplifies the type-checking problem (and may be necessary for
soundness; I didn't explore this in depth, but it certainly feels like
a very nasty sort of dependence). We could potentially weaken this
for cases where A is a direct type reference with bound parameters,
e.g. Foo<Int>([]) or the same with a typealias, but I think there's
some benefit from having a simpler specification, both for the
implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(John McCall) #13

I’m not entirely sure what an “expr-collection” is.

Collection literals, e.g. [x,y,z] and [a: x, b: y].

Does your proposal mean that in this code:
func foo() -> Int {...}
var w = 0
var x = T(foo())
var y = T(w)
var z = T(0)
different initializers would be used for `x`,`y`, and `z`?

z would be initialized using the literal initializer if T conforms to that protocol, yes.

If so, that seems a potential source of much subtler problems.

Note that this is only an issue for types that conform to the literal protocols.

I don’t disagree that you’ve identified a potential source of issues, but it’s conceivable that there might be circumstances where the "semantically very different results” are desired. I can’t think of any off the top of my head, but I’m not convinced that means they don’t exist.

I do not think that anybody writes UInt64(0) and *wants* the 0 to be built as an Int and then coerced to UInt64.

John.

···

On Jun 2, 2016, at 10:49 AM, David Sweeris <davesweeris@mac.com> wrote:

So… I’m tentatively -1

- Dave Sweeris

On Jun 2, 2016, at 11:08 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Austin Zheng) #14

I think we should actually go further.

If this proposal is accepted, disallow "as" as a way of specifying the type
to construct from a literal.

If this proposal isn't accepted, disallow using literal values as the
argument to one-unlabeled-argument constructors.

In either case we should disabuse users of the notion that A(literal) is an
initializer that behaves exactly the same as other initializers.

Austin

···

On Thu, Jun 2, 2016 at 11:46 AM, Austin Zheng <austinzheng@gmail.com> wrote:

+1.

The primary advantage is that it aligns the language semantics with how
most programmers expect this common C-language-family idiom to behave and
removes a potential source of silently wrong code.

The primary disadvantage is that it introduces special-case behavior to
certain types of initializers (although, to be fair, this special-case
behavior is easily recognizable: unlabeled one-argument initializer with a
literal as the argument).

I think the advantage outweighs the disadvantage.

This problem should be addressed one way or another. I prefer this
solution, but if it is rejected for whatever reason we should at least
explicitly outlaw A(literal) syntax in favor of "literal as A".

Austin

On Thu, Jun 2, 2016 at 9:08 AM, John McCall via swift-evolution < > swift-evolution@swift.org> wrote:

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does *not* attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution using
the standard rules, i.e. considering only single-argument unlabelled
initializers of a type which conforms to IntegerLiteralConvertible. Often
this leads to static ambiguities or, worse, causes the literal to be built
using a default type (such as Int); this may have semantically very
different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's clear
that programmers are going to continue to independently try to use it, so
it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an
*expr-call* with a single, unlabelled argument) where B is an
*expr-literal* or *expr-collection*, if A has type T.Type for some type
T and there is a declared conformance of T to an appropriate literal
protocol for B, then the expression is always resolves as a literal
construction of type T (as if the expression were written "B as A") rather
than as a general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by
wrapping the literal in parentheses. This might seem distasteful; it would
be easy enough to allow the form of B to include extra parentheses. It's
potentially useful to have a way to suppress this rule and get a normal
construction, but there are several other ways of getting that effect, such
as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional conformance.
This permits the applicability of the rule to be decided without having to
first decide the type arguments, which greatly simplifies the type-checking
problem (and may be necessary for soundness; I didn't explore this in
depth, but it certainly feels like a very nasty sort of dependence). We
could potentially weaken this for cases where A is a direct type reference
with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but
I think there's some benefit from having a simpler specification, both for
the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #15

+1

l8r
Sean

···

On Jun 2, 2016, at 1:46 PM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

+1.

The primary advantage is that it aligns the language semantics with how most programmers expect this common C-language-family idiom to behave and removes a potential source of silently wrong code.

The primary disadvantage is that it introduces special-case behavior to certain types of initializers (although, to be fair, this special-case behavior is easily recognizable: unlabeled one-argument initializer with a literal as the argument).

I think the advantage outweighs the disadvantage.

This problem should be addressed one way or another. I prefer this solution, but if it is rejected for whatever reason we should at least explicitly outlaw A(literal) syntax in favor of "literal as A".

Austin

On Thu, Jun 2, 2016 at 9:08 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:
The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #16

+1.

The primary advantage is that it aligns the language semantics with how most programmers expect this common C-language-family idiom to behave and removes a potential source of silently wrong code.

The primary disadvantage is that it introduces special-case behavior to certain types of initializers (although, to be fair, this special-case behavior is easily recognizable: unlabeled one-argument initializer with a literal as the argument).

I think the advantage outweighs the disadvantage.

Agree. This change basically means the label isn’t intended to be used by callers, but is only present to distinguish the initializer used by the protocol from any other unlabeled initializer accepting the same type. But conceptually it is treated as the *most specific* unlabelled initializer possible, thus winning the overload resolution.

How important is it that we have the ability to distinguish between literals and non-literals with the same type? If that isn’t important, maybe the literal convertible protocols could be reworked such that the label isn’t necessary. That would eliminate the special-case elision of the label.

···

On Jun 2, 2016, at 1:46 PM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

This problem should be addressed one way or another. I prefer this solution, but if it is rejected for whatever reason we should at least explicitly outlaw A(literal) syntax in favor of "literal as A".

Austin

On Thu, Jun 2, 2016 at 9:08 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(John McCall) #17

> Often
> this leads to static ambiguities or, worse, causes the literal to be built
> using a default type (such as Int); this may have semantically very
> different results which are only caught at runtime.

Seems like I'm very slow today.. Could you present a couple of examples where such initialization(like UInt16(7)) can produce some unexpected behavior / error at runtime?

UIntN has unlabeled initializers taking all of the standard integer types, including itself. The literal type will therefore get defaulted to Int. The legal range of values for Int may not be a superset of the legal range of values for UIntN. If the literal is in the legal range for an Int but not for the target type, this might trap at runtime. Now, for a built-in integer type like UInt16, we will recognize that the coercion always traps and emit an error at compile-time, but this generally won't apply to other types.

John.

···

On Jun 2, 2016, at 1:56 PM, Vladimir.S <svabox@gmail.com> wrote:

On 02.06.2016 19:08, John McCall via swift-evolution wrote:

The official way to build a literal of a specific type is to write the
literal in an explicitly-typed context, like so:
   let x: UInt16 = 7
or
   let x = 7 as UInt16

Nonetheless, programmers often try the following:
   UInt16(7)

Unfortunately, this does /not/ attempt to construct the value using the
appropriate literal protocol; it instead performs overload resolution using
the standard rules, i.e. considering only single-argument unlabelled
initializers of a type which conforms to IntegerLiteralConvertible. Often
this leads to static ambiguities or, worse, causes the literal to be built
using a default type (such as Int); this may have semantically very
different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an
explicitly-typed literal is an obvious and natural choice with several
advantages over the "as" syntax. However, even if you disagree, it's clear
that programmers are going to continue to independently try to use it, so
it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

Given a function call expression of the form A(B) (that is, an
/expr-call/ with a single, unlabelled argument) where B is
an /expr-literal/ or /expr-collection/, if A has type T.Type for some type
T and there is a declared conformance of T to an appropriate literal
protocol for B, then the expression is always resolves as a literal
construction of type T (as if the expression were written "B as A") rather
than as a general initializer call.

Formally, this would be a special form of the argument conversion
constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by
wrapping the literal in parentheses. This might seem distasteful; it would
be easy enough to allow the form of B to include extra parentheses. It's
potentially useful to have a way to suppress this rule and get a normal
construction, but there are several other ways of getting that effect, such
as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the
generic arguments are known to not satisfy the conditional conformance.
This permits the applicability of the rule to be decided without having to
first decide the type arguments, which greatly simplifies the type-checking
problem (and may be necessary for soundness; I didn't explore this in
depth, but it certainly feels like a very nasty sort of dependence). We
could potentially weaken this for cases where A is a direct type reference
with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but
I think there's some benefit from having a simpler specification, both for
the implementation and for the explicability of the model.

John.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Waite) #18

Would it be possible to have a warning on usage if there is an ambiguity here?

Otherwise, if we want T(0) to work, shouldn't we change the initializer signatures for LiteralConvertibles to match the desired behavior, rather than make it a special case?

-DW

···

On Jun 2, 2016, at 1:57 PM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 2, 2016, at 10:49 AM, David Sweeris <davesweeris@mac.com <mailto:davesweeris@mac.com>> wrote:
I’m not entirely sure what an “expr-collection” is.

Collection literals, e.g. [x,y,z] and [a: x, b: y].

Does your proposal mean that in this code:
func foo() -> Int {...}
var w = 0
var x = T(foo())
var y = T(w)
var z = T(0)
different initializers would be used for `x`,`y`, and `z`?

z would be initialized using the literal initializer if T conforms to that protocol, yes.

If so, that seems a potential source of much subtler problems.

Note that this is only an issue for types that conform to the literal protocols.

I don’t disagree that you’ve identified a potential source of issues, but it’s conceivable that there might be circumstances where the "semantically very different results” are desired. I can’t think of any off the top of my head, but I’m not convinced that means they don’t exist.

I do not think that anybody writes UInt64(0) and *wants* the 0 to be built as an Int and then coerced to UInt64.

John.

So… I’m tentatively -1

- Dave Sweeris

On Jun 2, 2016, at 11:08 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The official way to build a literal of a specific type is to write the literal in an explicitly-typed context, like so:
    let x: UInt16 = 7
or
    let x = 7 as UInt16

Nonetheless, programmers often try the following:
    UInt16(7)

Unfortunately, this does not attempt to construct the value using the appropriate literal protocol; it instead performs overload resolution using the standard rules, i.e. considering only single-argument unlabelled initializers of a type which conforms to IntegerLiteralConvertible. Often this leads to static ambiguities or, worse, causes the literal to be built using a default type (such as Int); this may have semantically very different results which are only caught at runtime.

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

Therefore, I propose that we adopt the following typing rule:

  Given a function call expression of the form A(B) (that is, an expr-call with a single, unlabelled argument) where B is an expr-literal or expr-collection, if A has type T.Type for some type T and there is a declared conformance of T to an appropriate literal protocol for B, then the expression is always resolves as a literal construction of type T (as if the expression were written "B as A") rather than as a general initializer call.

Formally, this would be a special form of the argument conversion constraint, since the type of the expression A may not be immediately known.

Note that, as specified, it is possible to suppress this typing rule by wrapping the literal in parentheses. This might seem distasteful; it would be easy enough to allow the form of B to include extra parentheses. It's potentially useful to have a way to suppress this rule and get a normal construction, but there are several other ways of getting that effect, such as explicitly typing the literal argument (e.g. writing "A(Int(B))").

A conditional conformance counts as a declared conformance even if the generic arguments are known to not satisfy the conditional conformance. This permits the applicability of the rule to be decided without having to first decide the type arguments, which greatly simplifies the type-checking problem (and may be necessary for soundness; I didn't explore this in depth, but it certainly feels like a very nasty sort of dependence). We could potentially weaken this for cases where A is a direct type reference with bound parameters, e.g. Foo<Int>([]) or the same with a typealias, but I think there's some benefit from having a simpler specification, both for the implementation and for the explicability of the model.

John.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(John McCall) #19

In my opinion, using this initializer-call syntax to build an explicitly-typed literal is an obvious and natural choice with several advantages over the "as" syntax. However, even if you disagree, it's clear that programmers are going to continue to independently try to use it, so it's really unfortunate for it to be subtly wrong.

I've seen developers do this; in one memorable case, it resulted in Swift taking a ridiculously long time to typecheck an expression, since the seemingly pinned-down types of the literals had actually become *more* ambiguous, not less.

Yes, this would also tend to improve compile times, since currently we end up generating a massively-ambiguous constraint system which must be resolved by type defaulting. That's not really why I'm proposing this, though.

However, it's not difficult to teach developers to use `as`. Usually what's happening is that their mental model of the language is wrong: they think of `UInt16(foo)` as a cast to a primitive type, and are surprised to learn that it's actually an initializer on a struct and they're initializing an instance. Learning this helps them understand how the language works, what the difference is between initializers and `as`, and how they can write the same things they see in the standard library types.

So, you think that this syntax is enticing to new developers who naturally think that the feature works the way that I'm proposing it should work, and you think that the right solution is to make the syntax illegal so that you can more conveniently tell them it doesn't work that way? :slight_smile:

You can still tell them that it's a struct and you're calling an initializer on it; it's just that the initializer chosen is the special literal initializer because the argument is a literal.

John.

···

On Jun 2, 2016, at 2:43 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

I think *actually* turning this into magic would be counterproductive. The better solution is to make the compiler replace me in that story, by having it emit a warning with a fix-it. It keeps initializer calls meaning exactly what they say. (And it doesn't require an evolution proposal to do, since you can add a warning with a mere bug.)

  UInt16(42)
  ^~~~~~ ^~
  Use of initializer with integer literal does not cast '42' to 'UInt16'
  Fix-It: Replace with '42 as UInt16'

--
Brent Royal-Gordon
Architechies


(David Sweeris) #20

I’m not entirely sure what an “expr-collection” is.

Collection literals, e.g. [x,y,z] and [a: x, b: y].

Thought so, but I wasn’t sure. Thanks for clarifying :slight_smile:

Does your proposal mean that in this code:
func foo() -> Int {...}
var w = 0
var x = T(foo())
var y = T(w)
var z = T(0)
different initializers would be used for `x`,`y`, and `z`?

z would be initialized using the literal initializer if T conforms to that protocol, yes.

If so, that seems a potential source of much subtler problems.

Note that this is only an issue for types that conform to the literal protocols.

Oh, I know. The crux of my concern is that while the difference between `UInt16(7)` and `7 as UInt16` is subtle, it’s not that subtle… the literal convertible syntax doesn’t even look that much like a call to init, so it shouldn’t be that surprising if explicitly calling the init function might send you down a different code path. OTOH, this proposal silently (and invisibly) rewrites an explicit call to`init(_: Int)` to `init(integerLiteral: IntegerLiteralType)`, which seems worse. Again, I don’t disagree that there’s a subtlety here, but at least with the current behavior, the unexpected behavior comes from not paying attention to syntax.

I don’t disagree that you’ve identified a potential source of issues, but it’s conceivable that there might be circumstances where the "semantically very different results” are desired. I can’t think of any off the top of my head, but I’m not convinced that means they don’t exist.

I do not think that anybody writes UInt64(0) and *wants* the 0 to be built as an Int and then coerced to UInt64.

I can’t think of why anyone would either — all my *LiteralConvertible types just pass on the literal arguments to an init that takes an Int (or whatever) — but “a failure of imagination…”

I guess I’m just saying that with the way Swift treats literals, potential confusion is inevitable, and that it’s better to contain the subtleties to syntax which already involves some implicit behavior, rather than to start rewriting explicit code simply because we think the programmer doesn’t know what they’re doing.

- Dave Sweeris

···

On Jun 2, 2016, at 12:57 PM, John McCall <rjmccall@apple.com> wrote:

On Jun 2, 2016, at 10:49 AM, David Sweeris <davesweeris@mac.com <mailto:davesweeris@mac.com>> wrote: