Closed jonathanvdc closed 8 years ago
[Edited] These should be illegal:
WriteLine(2.5 using int);
WriteLine(bignum using int);
Here's my original proposal:
// EC# introduces a new kind of cast operator, the "using" operator, to help you
// select an alias or interface to use. "using" is basically a kind of static
// assertion: it asserts that "t" can act as a T. It behaves like a cast, except
// that the implicit conversion to T must be legal. In other words, the compiler
// must know at compile time that the conversion is valid, or there is a compiler
// error. By itself, it can be used to
// - select an alias to use (including explicit aliases)
// - select an interface (for calling explicit interface implementations)
// - select a base class (for calling methods shadowed by 'new')
// - invoke an implicit conversion (but not an explicit conversion)
// Here's an example:
int three = (new[] { 1,2,3 } using IList).Count;
The explanation above seems to leave out part of my plan. In fact, I didn't think of using
as just a cast operator. So let me rephrase the proposal: my plan for using
was for it to be two separate pieces of functionality in one operator:
In fact I planned for item (1) to do something that standard C# couldn't do, as illustrated here:
struct Foo : ICloneable {
object ICloneable.Clone() { return this; }
}
void Example() { new Foo()(using ICloneable).Clone(); }
Ignoring for a moment that this code is silly, I envisioned that this would call Clone()
on the original struct value, not on a boxed copy of it.
I actually never verified that the CLR allows this, and C# certainly doesn't, but that's what I wanted :)
My thinking is a bit different now. Enhanced C# has an "escape hatch" - you can always quit using it and return to plain C#. That's supposed to be a key selling point. Therefore, a feature that doesn't work in plain C# is undesirable. So I think your interpretation of using
as a "conversion" is acceptable.
Maybe it would be a good idea that, when a user writes something like foo(using IBar).Baz()
and foo
is a struct, the compiler would warn that Baz
will run on a boxed copy of foo
rather than the original (which leads me to wonder... maybe there should be a warning like that for readonly
struct fields too...). On the other hand, if the user writes Baz(foo(using IBar))
, I guess they should already know that foo
will be boxed. Well, I guess this warning thing might be too much work.
Oh, I see. I've updated ecsc
to adhere to your explanation in this commit.
Which leaves us with the boxing problem. So your proposals for new Foo()(using ICloneable).Clone();
are:
Clone()
without boxing the Foo
instance. I'm fairly sure the CLR can handle this: the constrained. T
opcode does just that. But this feature doesn't really need CLR support, because the compiler can just emit a direct call to Foo.ICloneable.Clone
anyway. The only reason not to implement this first behavior is that it's a special case: it waives the normal rules for implicit conversions, and its semantics are inconsistent with ((ICloneable)new Foo()).Clone()
.struct
and don't issue a warning. This does get rid of the annoyance of warning the user when Foo
is an immutable struct. In that case, whether Foo
gets boxed or not doesn't matter in terms of run-time semantics.Personally, I'm leaning toward the first option.
If it's easy to implement without boxing, another option would be to do that but emit a warning that the code cannot be converted to plain C#.
The RangeEnumerator
struct in Loyc.Essentials illustrates the usefulness of this behavior:
/// <remarks>Although there is a ICloneable{R} constraint on R, it is currently
/// worthless due to a limitation of C#. Since <see cref="IFRange{T}"/> already
/// includes <c>ICloneable(IFRange(T))</c>, this structure cannot simply
/// invoke Clone() directly because the compiler complains that Clone() is
/// ambiguous. Consequently it is necessary to cast the range to <see
/// cref="ICloneable{R}"/> just to clone it; if R is a value type then it is
/// boxed, which defeats the entire performance advantage of calling
/// <c>ICloneable{R}.Clone()</c> instead of <c>ICloneable{IFRange{T}}.Clone</c>.
/// <para/>
/// Nevertheless, I have left the constraint in place, in the hope that EC# can
/// eventually eliminate this limitation thanks to its "using" cast. Once EC#
/// compiles directly to CIL, <c>range(using ICloneable<R>).Clone()</c> will
/// not perform boxing.
/// </remarks>
public struct RangeEnumerator<R, T> : IEnumerator<T> where R : IFRange<T>, ICloneable<R>
This is one of two problems I remember hitting when I tried to implement D-style ranges in C#. The other one was that C# doesn't support covariant return types, which makes Clone()
a massive hassle to implement...
// Since C# does not support covariant return types, implementing all the
// ICloneables can be quite a chore. Just copy and paste these, inserting
// an appropriate constructor for your range type:
IFRange<T> ICloneable<IFRange<T>>.Clone() { return Clone(); }
IBRange<T> ICloneable<IBRange<T>>.Clone() { return Clone(); }
IRange<T> ICloneable<IRange<T>> .Clone() { return Clone(); }
public MyType Clone() { return new MyType(...); }
EDIT: now I see three problems. The third is that PopFront(out bool fail)
is inconvenient to use due to its out
parameter. This can be solved with ref this
extension methods, or by allowing users to declare out
variables in-situ (as the C# team planned to do but mysteriously un-planned).
Good news, everyone. I've updated ecsc
, so calls to methods on using
cast-struct
values don't result in boxing any more. So this
using System;
public class A
{
public A()
{ }
public override string ToString()
{
return "A";
}
}
public class B : A
{
public B()
{ }
public override string ToString()
{
return "B";
}
}
public struct Foo : ICloneable
{
public int CloneCount { get; private set; }
public object Clone() { CloneCount++; return this; }
public override string ToString()
{
return CloneCount.ToString();
}
}
public static class Program
{
public static void Main()
{
B x = new B();
Console.WriteLine(x using A);
Console.WriteLine(x using B);
Console.WriteLine(2 using double);
var foo = default(Foo);
Console.WriteLine((foo using ICloneable).Clone());
}
}
prints the following output now:
B
B
2
1
This neat trick is accomplished by compiling
Console.WriteLine((foo using ICloneable).Clone());
as
IL_0028: ldloca.s 1
IL_002a: constrained. Foo
IL_0030: callvirt instance object class [mscorlib]System.ICloneable::Clone()
IL_0035: call void class [mscorlib]System.Console::WriteLine(object)
peverify
(well, monodis
, actually) seems to think that's okay, and both the Travis CI and the AppVeyor builds produce the expected result.
Compiling with the -pedantic
switch currently produces the warnings below.
$ ecsc UsingCast.ecs -platform clr -pedantic
UsingCast.ecs:42:27: warning: EC# extension: the 'using' cast operator is an EC# extension. [-Wecs-using-cast]
Console.WriteLine(x using A);
^~~~~~~~~
UsingCast.ecs:43:27: warning: EC# extension: the 'using' cast operator is an EC# extension. [-Wecs-using-cast]
Console.WriteLine(x using B);
^~~~~~~~~
UsingCast.ecs:44:27: warning: EC# extension: the 'using' cast operator is an EC# extension. [-Wecs-using-cast]
Console.WriteLine(2 using double);
^~~~~~~~~~~~~~
UsingCast.ecs:46:27: warning: EC# extension: the 'using' cast operator is an EC# extension. [-Wecs-using-cast]
Console.WriteLine((foo using ICloneable).Clone());
^~~~~~~~~~~~~~~~~~~~~~
I'm open to suggestions for the warning message, though. I was thinking of changing it to:
UsingCast.ecs:42:27: warning: EC# extension: 'using' casts cannot be converted to plain C#. [-Wecs-using-cast]
Console.WriteLine(x using A);
^~~~~~~~~
Well, it would be silly to warn about all the things that are "EC# extensions" (some of them your compiler could never even detect, like if I write ([#trivia_inParens] x)
instead of the equivalent (x)
. I think a warning should only be printed when using a non-public member of a struct; if you're not doing that then in fact the code is representable in plain C#. Foo(x using IFoo)
should not print a warning even if x
is a struct, since boxing was going to happen anyway so the plain C# code (IFoo)x
is equivalent.
Then again... it might be easiest to convert (x using IFoo).Foo(y)
to plain C# as ((IFoo)x).Foo(y)
even if Foo
is public and the cast wasn't necessary. How about this warning: "warning: this usage of using
cannot be translated faithfully to C#, because a cast requires boxing but using
does not [-Wecs-using-cast]". P.S. whoa, do you have a separate switch for every warning?
Great work, by the way!
Thanks! I've changed the warning to print the message you suggested, and made ecsc
display it only when a boxing conversion is elided.
The compiler's console output is now:
$ ecsc UsingCast.ecs -platform clr -pedantic
UsingCast.ecs:46:27: warning: EC# extension: this usage of 'using' cannot be translated faithfully to C#, because a cast requires boxing but 'using' does not. [-Wecs-using-cast]
Console.WriteLine((foo using ICloneable).Clone());
^~~~~~~~~~~~~~~~~~~~~~
Yeah, every warning has its own switch in ecsc
, just like in gcc
and clang
. Flame has support for warnings baked in, so adding a warning switch is actually sort of trivial. The following snippet of code is all it takes to define the -Wecs
warning group and the -Wecs-using-cast
warning.
/// <summary>
/// The -Wecs warning group.
/// </summary>
/// <remarks>
/// This is a -pedantic warning group.
/// </remarks>
public static readonly WarningDescription EcsExtensionWarningGroup =
new WarningDescription("ecs", Warnings.Instance.Pedantic);
/// <summary>
/// The -Wecs-using-cast warning.
/// </summary>
/// <remarks>
/// This is a -Wecs warning.
/// </remarks>
public static readonly WarningDescription EcsExtensionUsingCastWarning =
new WarningDescription("ecs-using-cast", EcsExtensionWarningGroup);
So -pedantic
implies -Wecs
, which in turn activates -Wecs-using-cast
. But you can also specifically turn -Wecs-using-cast
off by telling ecsc
to -Wno-ecs-using-cast
. I think that's lot more human-friendly than csc
and mcs
warning numbers, and gcc
-style warning groups make it easy to add new groups of off-by-default warnings. That scheme doesn't break existing code that treats warnings as errors.
You can clone and build ecsc
to play around with those warnings, if you want. ecsc
is still very experimental, but the tests/cs
contains some pure C# tests which compile successfully and correctly, under both -Og
and -O3
optimization levels.
Contributions would be most welcome, too. But I'd totally understand if you'd rather spend your spare time doing other things.
I'm afraid I'm well behind on everything - here's my excuse.
I'm still working on LES. I'd like to play with ecsc when I find some time.
That sounds like quite the Kafkaesque ordeal you're going through there. But it's good to hear that you've got a somewhat decent set-up now. :)
You really don't owe me an explanation, though. I'd be delighted if you found the time to play with ecsc
at some point, but please don't feel pressured to.
Speaking of LES, I'd like to add support for BigInteger
literals to LES. (#40) Do you think that's a good idea?
Anyway, I think my question about the run-time and compile-time semantics of using
conversions has been answered. Shall I mark this issue as closed?
Sure.
Hi there. I implemented the EC#
using
conversion inecsc
a while ago, and I'd just like to make sure that I got its semantics right.In my understanding,
x using T
does the exact same thing as(T)x
, except in the specific case where the(T)x
cast is resolved as a downcast, i.e. it is compiled as acastclass T
orunbox.any T
opcode, in which casex using T
results in a compile-time error.This is exactly what
ecsc
does right now. Note that the paragraph above implies that ifx using T
is resolved as an implicit or explicit user-defined conversion, then it will be compiled identically to(T)x
– there is no compile-time error – despite the fact that this may result in a run-time exception.Is that about right? Here's an overview of some
using
conversions. I've commented the illegalusing
conversion out.User-defined conversions haven't actually been implemented yet in
ecsc
, but I'd like to get a clear understanding of what theusing
conversion does first.