benf.org :  other :  cfr :  Kotlin Switch (when) On String

I came for the Java!

I previously looked at how java 7+ compiles switch on string.

Kotlin

Kotlin has a very similar construct, (except that when is an expression-switch, though that's hopefully coming to java in JSR-325.)

fun whenSwitch(str  : String) = when (str) {
    "Aa", "BB" -> 111;
    "cc" -> 222;
    else -> 444;
}

I initially expected this to be compiled very similarly to the java, i.e. a code implementation of an open hash/separate chaining, with a second hash.

Interestingly (and perfectly reasonably!) Kotlin compiles this slightly differently....

Bytecode for above

  public static final int whenSwitch(java.lang.String);
    Code:
       0: aload_0
       1: ldc           #9                  // String str
       3: invokestatic  #15                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_0
       7: astore_1
       8: aload_1
       9: invokevirtual #21                 // Method java/lang/String.hashCode:()I
      12: lookupswitch  { // 2              LB : First, switch on hashcode.....
                  2112: 40
                  3168: 64
               default: 87
          }
      40: aload_1
      41: ldc           #23                 // String Aa - LB: Then, check the buckets for that hash
      43: invokevirtual #27                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      46: ifeq          52
      49: goto          76                  // LB: Then, branch directly to the target.......
      52: aload_1
      53: ldc           #29                 // String BB
      55: invokevirtual #27                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      58: ifeq          87
      61: goto          76
      64: aload_1
      65: ldc           #31                 // String cc
      67: invokevirtual #27                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      70: ifeq          87
      73: goto          81
      76: bipush        111
      78: goto          90
      81: sipush        222
      84: goto          90
      87: sipush        444                 // LB: With the default way down here.
      90: ireturn
}

So what's different?

It's interesting (normal caveat: to me!) - Instead of having the second switch statement, Kotlin's compiler has generated jumps directly to the target branches - same behaviour, but effectively bypassing the need for a second switch.

This is equivalent to

int res;
switch (str.hashCode()) {
 case 2112:
   if (str.equals("Aa")) goto LABEL-76;
   if (str.equals("BB")) goto LABEL-76;
   goto default;
 case 3168:
   if (str.equals("cc")) goto LABEL-81;
   goto default;
 LABEL-76:
   res = 111;
   break;
 LABEL-81:
   res = 222;
   break;
 default:
   res = 444;
}
return res;

This is kind of nice - the second switch in java has no real value. For me, that's the really interesting thing here - thinking again about why Javac generates it - I assume it's there because the string-switch to int-switch transformation was originally described as a pure Java source transform in the project Coin spec, and the implementors followed that description literally!

Unfortunately...

The above is of course, not valid Java at all, and can't be. (Which is why cfr < 129 blew up horribly on it!).

So rewrite it!

As of CFR 129, I pattern match for the above code, and re-introduce the secondary switch, allowing subsequent transforms to tidy up as normal - this means we generate reasonable java!

public final class WhenTest3Kt {
    public static final int whenSwitch(@NotNull String str) {
        int n;
        Intrinsics.checkParameterIsNotNull((Object)str, (String)"str");
        switch (str) {
            case "Aa": 
            case "BB": {
                n = 111;
                break;
            }
            case "cc": {
                n = 222;
                break;
            }
            default: {
                n = 444;
            }
        }
        return n;
    }
}

Hurrah! (ish)

Addendum - JSR 325 - Java Switch Expressions

Now CFR (141+) supports JSR 325 Switch expressions, we can also get them back when decompiling Kotlin bytecode! (in CFR 142+)

Note that because this is an experimental feature until java 13, you will need to specify --switchexpression true ... and you'll get:

public final class WhenTest3Kt {
    public static final int whenSwitch(@NotNull String str) {
        Intrinsics.checkParameterIsNotNull((Object)str, (String)"str");
        int n = switch (str) {
            "Aa", "BB" -> 111;
            "cc" -> 222;
            default -> 444;
        };
        return n;
    }
}

Last updated 05/2018