Thread overview
Generating switch at Compile Time
Apr 13, 2017
Jesse Phillips
Apr 13, 2017
ag0aep6g
Apr 17, 2017
Jesse Phillips
Apr 17, 2017
ag0aep6g
Apr 18, 2017
Stefan Koch
April 13, 2017
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
April 13, 2017
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.
April 17, 2017
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.
April 18, 2017
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.
April 18, 2017
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.