Thread overview
[Issue 17336] Implicit type conversion of size_t.init to int causes standard type compatibility test to break
Apr 20, 2017
Adam D. Ruppe
Apr 21, 2017
Adam D. Ruppe
Apr 21, 2017
Nick Treleaven
April 20, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

Adam D. Ruppe <destructionator@gmail.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |destructionator@gmail.com

--- Comment #1 from Adam D. Ruppe <destructionator@gmail.com> ---
I don't think this is a bug at all, it is exactly what VRP is supposed to do - if the value is guaranteed to be in range, allow the implicit cast. This is why `ubyte a = 0;` compiles at all; typeof(0) == int, but vrp proves it fits in ubyte so no cast required. The compiler could prove it in the case of `init` since it is statically known in the expression, but failed to prove it in the other case since VRP doesn't cross statements.

BTW also note that size_t *may be* `int` as well - that's the case on 32 bit builds. There's no error at all with -m32, so your test is buggy regardless IMO.


I do kinda agree though that if you do explicitly cast, the compiler shouldn't implicitly cast it right back, which it does (and the compiler likes to insert fake explicit casts into the AST as it goes, giving error messages saying casts exist that the coder didn't write, so it'd have to clean that up too), but I'm still hesitant to actually call it a bug since it is working exactly as designed.

--
April 20, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

--- Comment #2 from hsteoh@quickfur.ath.cx ---
On -m32, the test does exactly what it should: opBinary(int) *can* take size_t
when size_t is 32-bit.

The problem here is that T.init is the standard way to obtain an instance of T in generic code, because you don't know what T is and that's the only way to obtain an instance of T for type-compatibility testing purposes. You certainly cannot expect .max to exist for generic T, after all.

The code given here uses size_t.init explicitly because it was reduced from generic code.  The generic form of the code looks something like this:

-------
static if (is(typeof(A.init + B.init) : A)) { ... }
-------

There is no way you can know, in generic code, that B.init may magically implicitly convert to some other type C that passes the check, yet later on when you try to add an instance of B to A, it fails to compile.

Or perhaps this is an argument for not using .init in generic code at all, since with the recent language changes it has basically become worthless -- another problem is that if A or B has @disabled this(), then .init may actually fail even if A+B actually compiles.  Perhaps I should just accept that every time I need to do a type compatibility check I should write this instead:

-------
static if (is(typeof((A a, B b) => a + b) : A)) { ... }
-------

--
April 20, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

--- Comment #3 from hsteoh@quickfur.ath.cx ---
Oh, except that the latter check actually doesn't work, because the type of `(A a, B b) => a + b` is a delegate, not A.  So it'd have to be the nausea-inducing:

-------
static if (is(typeof((A a, B b) => a + b) : A delegate(A,B))) { ... }
-------

or something along those lines.  It quickly becomes completely ridiculous.

--
April 21, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

--- Comment #4 from Adam D. Ruppe <destructionator@gmail.com> ---
> if A or B has @disabled this(), then .init may actually fail even if A+B actually compiles.

That's not true, @disable this does not affect .init. .init WILL always compile. In fact, it is legal to do `Foo foo = Foo.init;` when Foo has a disabled default constructor; it still counts as explicit initialization.

But, .init is just a value. It doesn't represent a specific type, just the default value of that type, and as such is subject to implicit casting.

That's why I think your test is buggy: it should be checking something more
like `canAdd!(Foo, size_t)`, not `typeof(some_foo + cast(size_t) 0)`; iow,
working entirely on types instead of on values.

I understand that's a bit of a pain to write, but it is more accurate. The implementation of canAdd might be `__traits(compiles, () { A a = A.init; B b = B.init; auto c = a + b; });` ... though that's a bit fragile too, since vrp may someday cross statement boundaries and then the same thing comes up again. But it'd work for now at least...

--
April 21, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

Nick Treleaven <nick@geany.org> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |nick@geany.org

--- Comment #5 from Nick Treleaven <nick@geany.org> ---
(In reply to Adam D. Ruppe from comment #4)
> I understand that's a bit of a pain to write, but it is more accurate. The implementation of canAdd might be `__traits(compiles, () { A a = A.init; B b = B.init; auto c = a + b; });` ... though that's a bit fragile too, since vrp may someday cross statement boundaries and then the same thing comes up again. But it'd work for now at least...

Best to avoid init altogether:

__traits(compiles, (A a, B b) { auto c = a + b; })

--
April 21, 2017
https://issues.dlang.org/show_bug.cgi?id=17336

Steven Schveighoffer <schveiguy@yahoo.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
             Status|NEW                         |RESOLVED
                 CC|                            |schveiguy@yahoo.com
         Resolution|---                         |WONTFIX

--- Comment #6 from Steven Schveighoffer <schveiguy@yahoo.com> ---
The VRP is definitely not wrong. You are getting exactly what you asked for:
can you add size_t.init to B (yes, due to VRP), and is the result convertible
to B (yes).

The question is, what is the "right" way to do this. I understand that "I want an instance of a size_t for type purposes" generally is done with size_t.init. However, clearly this doesn't work out too well. I think using size_t.max is a good compromise, especially when you know the type being checked is a primitive and defined to have a max property.

Note that most of the cases are template constraints on a function, and you have at your disposal the function's parameter instead of reaching for T.init.

Think about what you are asking here, you want size_t.init NOT to convert via VRP to int (e.g. "let's break everyone's code"), OR you want it to not convert inside static tests (e.g. "let's break lots of code in a super-inconsistent way"). I think you can understand that we absolutely can't fix this. What has to change is the pattern used to check for adding a type.

Perhaps the best thing is to create a set of std.traits.canX (where X is add, multiply, etc) which checks for operations given two types in the verbose way. Feel free to file an enhancement for Phobos on that.

Little traps like this are very good to document and publish. I've had quite a
few is(typeof(...)) contraints/checks blow up on me.

--