www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Generating switch at Compile Time

reply Jesse Phillips <Jesse.K.Phillips+D gmail.com> writes:
I realize that this is likely really pushing the compile time 
generation but a recent change to the switch statement[1] is 
surfacing because of this usage.

uninitswitch2.d(13): Deprecation: 'switch' skips declaration of 
variable uninits
witch2.main.li at uninitswitch2.d(14)

---------------------
     import std.traits;
     import std.typecons;
     import std.meta;

     private static immutable list = AliasSeq!(
             tuple("a", "q"),
             tuple("b", "r"),
             );

     void main() {
         import std.stdio;
         string search;
         switch(search) {
--->        foreach(li; list) { // li initialization is skipped
                 mixin("case li[0]:");
                 mixin("writeln(li[1]);");
                 return;
             }
             default:
             break;
         }

         // Works
         mixin(genSwitch("search"));
     }
---------------------

I realize I can build out the entire switch and mix it in:

---------------------
     string genSwitch(string search) {
         auto ans = "switch(" ~ search ~ ") {\n";
             foreach(li; list) {
                 ans ~= "case \"" ~ li[0] ~ "\":\n";
                 ans ~= "writeln(\"" ~ li[1] ~ "\");\n";
                 ans ~= "return;\n";
             }
             ans ~= "default:\n";
             ans ~= "break;\n";
         ans ~= "}";

         return ans;
     }
---------------------

But I'm just wondering if the new initialization check should not 
be triggered from this utilization.

---------------------
     // Unrolled based on
     // 
https://wiki.dlang.org/User:Quickfur/Compile-time_vs._compile-time description

     version(none)
     void func2243(Tuple param0, Tuple param1) {
         {
             {
                 case param0[0]:
                 writeln(param0[1]);
                 return;
             }
             {
                 case param1[0]:
                 writeln(param1[1]);
                 return;
             }
         }
     }
---------------------

Thoughts?

1. https://issues.dlang.org/show_bug.cgi?id=14532
Apr 13 2017
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On 04/13/2017 11:06 PM, Jesse Phillips wrote:
 ---------------------
[...]
     private static immutable list = AliasSeq!(
             tuple("a", "q"),
             tuple("b", "r"),
             );
[...]
         switch(search) {
 --->        foreach(li; list) { // li initialization is skipped
                 mixin("case li[0]:");
                 mixin("writeln(li[1]);");
                 return;
             }
             default:
             break;
         }
[...]
     }
 ---------------------

 Thoughts?
That's not a static foreach. It's a normal run-time foreach. The switch jumps into the loop body without the loop head ever executing. The compiler is correct when it says that initialization of li is being skipped. Make `list` an enum or alias instead. Then the foreach is unrolled at compile time, you don't get a deprecation message, and it works correctly. By the way, in my opinion, `case li[0]:` shouldn't compile with the static immutable `list`. `list` and `li[0]` are dynamic values. The compiler only attempts (and succeeds) to evaluate them at compile time because they're typed as immutable. The way I see it, that only makes things more confusing.
Apr 13 2017
parent reply Jesse Phillips <Jesse.K.Phillips+D gmail.com> writes:
On Thursday, 13 April 2017 at 21:33:28 UTC, ag0aep6g wrote:
 That's not a static foreach. It's a normal run-time foreach. 
 The switch jumps into the loop body without the loop head ever 
 executing. The compiler is correct when it says that 
 initialization of li is being skipped.
Good good. I did miss that as I was trying different things.
 Make `list` an enum or alias instead. Then the foreach is 
 unrolled at compile time, you don't get a deprecation message, 
 and it works correctly.
Yes it did.
 By the way, in my opinion, `case li[0]:` shouldn't compile with 
 the static immutable `list`. `list` and `li[0]` are dynamic 
 values. The compiler only attempts (and succeeds) to evaluate 
 them at compile time because they're typed as immutable. The 
 way I see it, that only makes things more confusing.
This is very interesting. I wonder if the compiler is still unrolling the loop at compile time since it functions as expected; It certainly needs that deprecation though.
Apr 17 2017
parent ag0aep6g <anonymous example.com> writes:
On 04/17/2017 09:29 PM, Jesse Phillips wrote:
 On Thursday, 13 April 2017 at 21:33:28 UTC, ag0aep6g wrote:
[...]
 By the way, in my opinion, `case li[0]:` shouldn't compile with the
 static immutable `list`. `list` and `li[0]` are dynamic values. The
 compiler only attempts (and succeeds) to evaluate them at compile time
 because they're typed as immutable. The way I see it, that only makes
 things more confusing.
This is very interesting. I wonder if the compiler is still unrolling the loop at compile time since it functions as expected; It certainly needs that deprecation though.
It's really weird. I thought the loop would just not be unrolled at all. However, both `case`s seem to be generated as expected. So it behaves like a static foreach in that regard. But when you use `li` as a dynamic value (e.g. `writeln(li[1])`), it's suddenly empty. Seems that dmd can't decide what to do, so it does a little bit of both. Maybe I was wrong, and the loop in your code is a static foreach, but at some point there's a bug that makes dmd think it's dealing with run-time values. The behavior is also completely inconsistent. With ints, the program compiles and the assert passes: ---- alias AliasSeq(stuff ...) = stuff; immutable list = AliasSeq!(1, 2); void main() { switch(1) { foreach(li; list) { case li: enum e = li; assert(e == li); return; } default: } } ---- With strings, the program doesn't compile: ---- alias AliasSeq(stuff ...) = stuff; immutable list = AliasSeq!("foo", "bar"); void main() { switch("foo") { foreach(li; list) { case li: enum e = li; assert(e == li); return; /* "Error: case must be a string or an integral constant, not li" */ } default: } } ---- And with structs (your case), it compiles with a deprecation warning and behaves schizophrenically: ---- alias AliasSeq(stuff ...) = stuff; struct S { int x; } immutable list = AliasSeq!(S(1), S(2)); void main() { switch(1) /* Deprecation: 'switch' skips declaration of variable */ { foreach(li; list) { case li.x: enum e = li; assert(e == li); return; /* The assert fails. */ } default: } } ---- In my opinion, they should all simply be rejected. But I have no idea what's intended by the compiler writers. It's a mess. With enum/alias, they all compile and work as expected, of course.
Apr 17 2017
prev sibling parent Stefan Koch <uplink.coder googlemail.com> writes:
On Thursday, 13 April 2017 at 21:06:52 UTC, Jesse Phillips wrote:
 I realize that this is likely really pushing the compile time 
 generation but a recent change to the switch statement[1] is 
 surfacing because of this usage.

 uninitswitch2.d(13): Deprecation: 'switch' skips declaration of 
 variable uninits
 witch2.main.li at uninitswitch2.d(14)

 ---------------------
     import std.traits;
     import std.typecons;
     import std.meta;

     private static immutable list = AliasSeq!(
             tuple("a", "q"),
             tuple("b", "r"),
             );

     void main() {
         import std.stdio;
         string search;
         switch(search) {
 --->        foreach(li; list) { // li initialization is skipped
                 mixin("case li[0]:");
                 mixin("writeln(li[1]);");
                 return;
             }
             default:
             break;
         }

         // Works
         mixin(genSwitch("search"));
     }
 ---------------------

 I realize I can build out the entire switch and mix it in:

 ---------------------
     string genSwitch(string search) {
         auto ans = "switch(" ~ search ~ ") {\n";
             foreach(li; list) {
                 ans ~= "case \"" ~ li[0] ~ "\":\n";
                 ans ~= "writeln(\"" ~ li[1] ~ "\");\n";
                 ans ~= "return;\n";
             }
             ans ~= "default:\n";
             ans ~= "break;\n";
         ans ~= "}";

         return ans;
     }
 ---------------------

 But I'm just wondering if the new initialization check should 
 not be triggered from this utilization.

 ---------------------
     // Unrolled based on
     // 
 https://wiki.dlang.org/User:Quickfur/Compile-time_vs._compile-time description

     version(none)
     void func2243(Tuple param0, Tuple param1) {
         {
             {
                 case param0[0]:
                 writeln(param0[1]);
                 return;
             }
             {
                 case param1[0]:
                 writeln(param1[1]);
                 return;
             }
         }
     }
 ---------------------

 Thoughts?

 1. https://issues.dlang.org/show_bug.cgi?id=14532
This is what is generated by your example: switch (search) { unrolled { { // does not actually open a new scope immutable immutable(Tuple!(string, string)) li = __list_field_0; case "a": { } writeln(li.__expand_field_1); return 0; } { // same here we do not actually open a new scope. immutable immutable(Tuple!(string, string)) li = __list_field_1; case "b": { } writeln(li.__expand_field_1); return 0; } } default: { break; } } return 0; it should be clear to sww why li's initialisation is referred to as skipped.
Apr 17 2017