Re: Switch translation

2018-04-06 Thread Brian Goetz




    int index=-1;
    switch (s.hashCode()) {
        case 12345: if (!s.equals("Hello")) break; index = 1; break;
        case 6789: if (!s.equals("World")) break; index = 0; break;
        default: index = -1;
    }
    switch (index) {
        case 0: ...
    case 1: ...
        default: ...
    }

If there are hash collisions between the strings, the first switch 
must try all possible matching strings.


I see why you use this structure, because it fits a general paradigm 
of first mapping to an integer.


Or, "used", since this is the historical strategy which we're tossing 
over for the indy-based one.



I now suggest that a post-optimization might then turn this into:

  SUCCESS: {
    DEFAULT: {
      switch (s.hashCode()) {
        case 12345: if (s.equals("Hello")) { stmts1; break SUCCESS; } 
else if (s.equals(“Goodbye")) { stmts3; break SUCCESS; } else break 
DEFAULT;


Yes; the thing that pushed us to this translation was fallthrough and 
other weird control flow; by lowering the string-switch to an 
int-switch, the control structure is preserved, so any complex control 
flow comes along "for free" by existing int-switch translation.  Of 
course, it's not free; we pay with a pre-switch. (When we added strings 
in switch, it was part of "Project Coin", whose mandate was "small 
features", so it was preferable at the time to choose a simpler but less 
efficient desugaring.)






 Switches on enums

Switches on `enum` constants exploit the fact that enums have 
(usually dense) integral ordinal values. Unfortunately, because an 
ordinal value can change between compilation time and runtime, we 
cannot rely on this mapping directly, but instead need to do an extra 
layer of mapping.  Given a switch like:


    switch(color) {
    case RED: ...
    case GREEN: ...
    }

The compiler numbers the cases starting a 1 (as with string switch), 
and creates a synthetic class that maps the runtime values of the 
enum ordinals to the statically numbered cases:


Inconsistency: in the string example above, you actually numbered the 
cases 0 and 1, not 1 and 2.


The old way, where the compiler generated the transform table (Java 5 
and later) used 1-origin, for the reason you surmise.  The new, 
indy-based translation uses 0, like the String example.




Presumably for this example the chosen integers start with 1 rather 
than 0, so that if any element of the array is not explicitly 
initialized by Outer$0, its default 0 value will not be confused with 
an actual enum value.  This subtle point should be mentioned explicitly.


Yes, that's exactly why the historical approach did it this way. The new 
way (which is uniform with other indy-based switch types) takes care of 
this with pre-filling the array with the index that indicates "default" 
at linkage time.  From SwitchBootstraps::enumSwitch:


    ordinalMap = new int[enumClass.getEnumConstants().length];
    Arrays.fill(ordinalMap, enumNames.length);

    for (int i=0; i<enumNames.length; i++) {
    try {
    ordinalMap[E.valueOf(enumClass, 
enumNames[i]).ordinal()] = i;

    }
    catch (Exception e) {
    // allow non-existent labels, but never match them
    continue;
    }
    }
    }

The catch-and-continue is so that if an enum disappears, we don't fail 
linkage of the classifier site, its arm just becomes dead code (which is 
fine, since the enum constant no longer exists.)






 Explicit continue

An alternative to exposing guards is to expose an explicit `continue` 
statement in switch, which would have the effect of "keep matching at 
the next case."  Then guards could be expressed imperatively as:


    case P:
    if (!guard)
    continue;
    ...
    break;
    case Q: …


A nice idea, but careful: it is already meaningful to write:

while (…) { switch (…) { case 1: … case 2: if (foo) continue; … } }

and expect the `continue` to start a new iteration of the `while` 
loop.  Indeed, this fact was already exploited above under “### Guards”.


Yes.  One of the downsides of exposing `continue` is that currently the 
(switch, continue) entry in my table from "Disallowing break label and 
continue label inside expression switch" has a P instead of an X, 
meaning that continue is currently allowed in a switch if there's an 
enclosing continue-able context.  So this could be disambiguated as you 
say with "continue switch", or with requiring a label in some or all 
circumstances.





Re: Switch translation

2018-04-06 Thread Guy Steele
Very comprehensive.  Four groups of comments below (one at the very bottom).

> On Apr 6, 2018, at 11:51 AM, Brian Goetz <brian.go...@oracle.com> wrote:
> 
> The following outlines our story for translating improved switches, including 
> both the switch improvements coming as part of JEP 325, and follow-on work to 
> add pattern matching to switches.  Much of this has been discussed already 
> over the last year, but here it is in one place.
> 
> # Switch Translation
>  Maurizio Cimadamore and Brian Goetz 
>  April 2018
> 
> ## Part 1 -- constant switches
> 
> This part examines the current translation of `switch` constructs by `javac`, 
> and proposes a more general translation for switching on primitives, boxes, 
> strings, and enums, with the goals of: 
> 
>  - Unify the treatment of `switch` variants, simplifying the compiler 
> implementation and reducing the static footprint of generated code; 
>  - Move responsibility for target classification from compile time to run 
> time, allowing us to more freely update the logic without updating the 
> compiler.
> 
> ## Current translation 
> 
> Switches on `int` (and the smaller integer primitives) are translated in one 
> of two ways.  If the labels are relatively dense, we translate an `int` 
> switch to a `tableswitch`; if they are sparse, we translate to a 
> `lookupswitch`.  The current heuristic appears to be that we use a 
> `tableswitch` if it results   in a smaller bytecode than a `lookupswitch` 
> (which uses twice as many bytes per entry), which is a reasonable heuristic. 
> 
>  Switches on boxes 
> 
> Switches on primitive boxes are currently implemented as if they were 
> primitive switches, unconditionally unboxing the target before entry 
> (possibly throwing NPE). 
> 
>  Switches on strings 
> 
> Switches on strings are implemented as a two-step process, exploiting the 
> fact that strings cache their `hashCode()` and that hash codes are reasonably 
> spread out. Given a switch on strings like the one below: 
> 
> switch (s) { 
> case "Hello": ... 
> case "World": ... 
> default: ... 
> } 
> 
> The compiler desugar this into two separate switches, where the first switch 
> maps the input strings into a range of numbers [0..1], as shown below, which 
> can then be used in a subsequent plain switch on ints.  The generated code 
> unconditionally calls `hashCode()`, again possibly throwing NPE. 
> 
> int index=-1; 
> switch (s.hashCode()) { 
> case 12345: if (!s.equals("Hello")) break; index = 1; break; 
> case 6789: if (!s.equals("World")) break; index = 0; break; 
> default: index = -1; 
> } 
> switch (index) { 
> case 0: ... 
> case 1: ... 
> default: ... 
> } 
> 
> If there are hash collisions between the strings, the first switch must try 
> all possible matching strings. 

Minor point: unclear why the default case has to assign -1 to index, when it it 
already initialized to -1.

I see why you use this structure, because it fits a general paradigm of first 
mapping to an integer.

However, a post-optimization might be able to turn such a structure, where the 
assignments to “index” are all constants rather than the results of calling 
some opaque classifier method, into a control structure with a single switch 
statement and no use of the intermediate integer encoding.  I’ll show a more 
general example to give you the idea:

switch (s) { 
case "Hello": stmts1;
case "World": stmts2;
case “Goodbye": stmts3;
default: stmtsD;
} 

and suppose that “Hello” and “Goodbye” happen to have the same hashcode.  It 
might be transformed into:

int index=-1; 
switch (s.hashCode()) { 
case 12345: if (s.equals("Hello")) index = 0; else if 
(s.equals(“Goodbye")) index = 2; break; 
case 6789: if (s.equals("World")) index = 1; break; 
default: break; 
} 
switch (index) { 
case 0: stmts1;
case 1: stmts2;
case 2: stmts3;
default: stmtsD;
} 

I now suggest that a post-optimization might then turn this into:

  SUCCESS: {
DEFAULT: {
  switch (s.hashCode()) { 
case 12345: if (s.equals("Hello")) { stmts1; break SUCCESS; } else if 
(s.equals(“Goodbye")) { stmts3; break SUCCESS; } else break DEFAULT;
case 6789: if (s.equals("World")) { stmts2; break SUCCESS; } else break 
DEFAULT;
default: break DEFAULT; 
  }
  break SUCCESS;
}
do { stmtsD; } while(0);
  } 

where `SUCCESS` and `DEFAULT` are suitably generated fresh statement labels.  
(You might think that simp

Switch translation

2018-04-06 Thread Brian Goetz
The following outlines our story for translating improved switches, 
including both the switch improvements coming as part of JEP 325, and 
follow-on work to add pattern matching to switches.  Much of this has 
been discussed already over the last year, but here it is in one place.


# Switch Translation
 Maurizio Cimadamore and Brian Goetz
 April 2018

## Part 1 -- constant switches

This part examines the current translation of `switch` constructs by 
`javac`, and proposes a more general translation for switching on 
primitives, boxes, strings, and enums, with the goals of:


 - Unify the treatment of `switch` variants, simplifying the compiler 
implementation and reducing the static footprint of generated code;
 - Move responsibility for target classification from compile time to 
run time, allowing us to more freely update the logic without updating 
the compiler.


## Current translation

Switches on `int` (and the smaller integer primitives) are translated in 
one of two ways.  If the labels are relatively dense, we translate an 
`int` switch to a `tableswitch`; if they are sparse, we translate to a 
`lookupswitch`.  The current heuristic appears to be that we use a 
`tableswitch` if it results in a smaller bytecode than a `lookupswitch` 
(which uses twice as many bytes per entry), which is a reasonable 
heuristic.


 Switches on boxes

Switches on primitive boxes are currently implemented as if they were 
primitive switches, unconditionally unboxing the target before entry 
(possibly throwing NPE).


 Switches on strings

Switches on strings are implemented as a two-step process, exploiting 
the fact that strings cache their `hashCode()` and that hash codes are 
reasonably spread out. Given a switch on strings like the one below:


    switch (s) {
    case "Hello": ...
        case "World": ...
    default: ...
    }

The compiler desugar this into two separate switches, where the first 
switch maps the input strings into a range of numbers [0..1], as shown 
below, which can then be used in a subsequent plain switch on ints.  The 
generated code unconditionally calls `hashCode()`, again possibly 
throwing NPE.


    int index=-1;
    switch (s.hashCode()) {
        case 12345: if (!s.equals("Hello")) break; index = 1; break;
        case 6789: if (!s.equals("World")) break; index = 0; break;
        default: index = -1;
    }
    switch (index) {
        case 0: ...
    case 1: ...
        default: ...
    }

If there are hash collisions between the strings, the first switch must 
try all possible matching strings.


 Switches on enums

Switches on `enum` constants exploit the fact that enums have (usually 
dense) integral ordinal values.  Unfortunately, because an ordinal value 
can change between compilation time and runtime, we cannot rely on this 
mapping directly, but instead need to do an extra layer of mapping.  
Given a switch like:


    switch(color) {
    case RED: ...
    case GREEN: ...
    }

The compiler numbers the cases starting a 1 (as with string switch), and 
creates a synthetic class that maps the runtime values of the enum 
ordinals to the statically numbered cases:


    class Outer$0 {
    synthetic final int[] $EnumMap$Color = new 
int[Color.values().length];

        static {
            try { $EnumMap$Color[RED.ordinal()] = 1; } catch 
(NoSuchFieldError ex) {}
            try { $EnumMap$Color[GREEN.ordinal()] = 2; } catch 
(NoSuchFieldError ex) {}

    }
    }

Then, the switch is translated as follows:

    switch(Outer$0.$EnumMap$Color[color.ordinal()]) {
    case 1: stmt1;
        case 2: stmt2
    }

In other words, we construct an array whose size is the cardinality of 
the enum, and then the element at position *i* of such array will 
contain the case index corresponding to the enum constant with whose 
ordinal is *i*.


## A more general scheme

The handling of strings and enums give us a hint of how to create a more 
regular scheme; for `switch` targets more complex than `int`, we lower 
the `switch` to an `int` switch with consecutive `case` labels, and use 
a separate process to map the target into the range of synthetic case 
labels.


Now that we have `invokedynamic` in our toolbox, we can reduce all of 
the non-`int` cases to a single form, where we number the cases with 
consecutive integers, and perform case selection via an 
`invokedynamic`-based classifier function, whose static argument list 
receives a description of the actual targets, and which returns an `int` 
identifying what `case` to select.


This approach has several advantages:
 - Reduced compiler complexity -- all switches follow a common pattern;
 - Reduced static code size;
 - The classification function can select from a wide range of 
strategies (linear search, binary search, building a `HashMap`, 
constructing a perfect hash function, etc), which can vary over time or 
from situation to situation

Re: Switch translation, part 2

2018-01-02 Thread Remi Forax
Hi all, 
while the proposed translation is a good translation by default, when you can 
have fallthroughs or guards, if you have none of them, it's not the best 
translation. 

[CC John Rose because i may say something stupid] 

The problem is that the VM doesn't not prune never called cases in a switch 
while it does that for the branch of a if, so an if ... else can be faster than 
a switch in the case only some cases are exercise at runtime. 
Also note that a lot of tableswitch end up to be generated by the VM as if .. 
else if you take a look to the generated assembly code because jumping to a 
computed address is not free. 
So it seems a good idea in the case of an expression switch with no guard to 
not generate an indy + a tableswitch but just an indy. 

So instead of lowering: 

switch (x) { 
case T t: A 
case U u: B 
case V v: C 
} 

to 

int y = indy[bootstrap=typeSwitch(T.class, U.class, V.class)](x) 
switch (y) { 
case 0: A 
case 1: B 
case 2: C 
} 

I propose to lowering it to something similar to the lambda translation: 
var expr = indy[bootstrap=exprTypeSwich(1, T.class, U.class, V.class)(x); 
and let the bootstrap to do all the lifting. 
The first bootstrap argument is the number of the switch in the code, here 1. 

With A, B and C being desugared as static methods respectively to 
switch$1case$0, switch$1case$1 and switch$1case$2 (i.e. "switch$" + 
switchNumber + "case$" + caseNumber. 

The bootstrap method exprTypeSwich can work like an inlining cache if the 
number of branches actually visited is small, this is interesting because the 
performance of an expression switch will be in the same ball park as the 
performance of the corresponding virtual call, and if there is too many 
branches used, revert to use a new method handle combinator that does a 
tableswitch*. 

cheers, 
Rémi 

* the JSR 292 has talked several times to introduce such method handle 
combinator. 

> De: "Brian Goetz" <brian.go...@oracle.com>
> À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net>
> Envoyé: Lundi 11 Décembre 2017 22:37:57
> Objet: Switch translation, part 2

> # Switch Translation, Part 2 -- type test patterns and guards
>  Maurizio Cimadamore and Brian Goetz
>  December 2017

> This document examines possible translation of `switch` constructs involving
> `case` labels that include type-test patterns, potentially with guards. Part 3
> will address translation of destructuring patterns, nested patterns, and OR
> patterns.

> ## Type-test patterns

> Type-test patterns are notable because their applicability predicate is purely
> based on the type system, meaning that the compiler can directly reason about
> it both statically (using flow analysis, optimizing away dynamic type tests)
> and dynamically (with `instanceof`.) A switch involving type-tests:

> switch (x) {
> case String s: ...
> case Integer i: ...
> case Long l: ...
> }

> can (among other strategies) be translated into a chain of `if-else` using
> `instanceof` and casts:

> if (x instanceof String) { String s = (String) x; ... }
> else if (x instanceof Integer) { Integer i = (Integer) x; ... }
> else if (x instanceof Long) { Long l = (Long) x; ... }

>  Guards

> The `if-else` desugaring can also naturally handle guards:

> switch (x) {
> case String s
> where (s.length() > 0): ...
> case Integer i
> where (i > 0): ...
> case Long l
> where (l > 0L): ...
> }

> can be translated to:

> if (x instanceof String
> && ((String) x).length() > 0) { String s = (String) x; ... }
> else if (x instanceof Integer
> && ((Integer) x) > 0) { Integer i = (Integer) x; ... }
> else if (x instanceof Long
> && ((Long) x) > 0L) { Long l = (Long) x; ... }

>  Performance concerns

> The translation to `if-else` chains is simple (for switches without
> fallthrough), but is harder for the VM to optimize, because we've used a more
> general control flow mechanism. If the target is an empty `String`, which 
> means
> we'd pass the first `instanceof` but fail the guard, class-hierarchy analysis
> could tell us that it can't possibly be an `Integer` or a `Long`, and so
> there's no need to perform those tests. But generating code that takes
> advantage of this information is more complex.

> In the extreme case, where a switch consists entirely of type test patterns 
> for
> final classes, this could be performed as an O(1) operation by hashing. And
> this is a common case involving switches over alternatives in a sum (sealed)
> type. (We probably shouldn't rely on finality at compile time, as this can
> change between compile and run time, but we would like to take advantage of
> this at run time if we can.)

> Finally, the straightforward static translation may miss o

Switch translation, part 2

2017-12-11 Thread Brian Goetz

# Switch Translation, Part 2 -- type test patterns and guards
 Maurizio Cimadamore and Brian Goetz
 December 2017

This document examines possible translation of `switch` constructs 
involving `case` labels that include type-test patterns, potentially 
with guards.  Part 3 will address translation of destructuring patterns, 
nested patterns, and OR patterns.


## Type-test patterns

Type-test patterns are notable because their applicability predicate is 
purely based on the type system, meaning that the compiler can directly 
reason about it both statically (using flow analysis, optimizing away 
dynamic type tests) and dynamically (with `instanceof`.)  A switch 
involving type-tests:


    switch (x) {
    case String s: ...
    case Integer i: ...
    case Long l: ...
    }

can (among other strategies) be translated into a chain of `if-else` 
using `instanceof` and casts:


    if (x instanceof String) { String s = (String) x; ... }
    else if (x instanceof Integer) { Integer i = (Integer) x; ... }
    else if (x instanceof Long) { Long l = (Long) x; ... }

 Guards

The `if-else` desugaring can also naturally handle guards:

    switch (x) {
    case String s
    where (s.length() > 0): ...
    case Integer i
    where (i > 0): ...
    case Long l
    where (l > 0L): ...
    }

can be translated to:

    if (x instanceof String
    && ((String) x).length() > 0) { String s = (String) x; ... }
    else if (x instanceof Integer
 && ((Integer) x) > 0) { Integer i = (Integer) x; ... }
    else if (x instanceof Long
 && ((Long) x) > 0L) { Long l = (Long) x; ... }

 Performance concerns

The translation to `if-else` chains is simple (for switches without 
fallthrough), but is harder for the VM to optimize, because we've used a 
more general control flow mechanism.  If the target is an empty 
`String`, which means we'd pass the first `instanceof` but fail the 
guard, class-hierarchy analysis could tell us that it can't possibly be 
an `Integer` or a `Long`, and so there's no need to perform those tests. 
But generating code that takes advantage of this information is more 
complex.


In the extreme case, where a switch consists entirely of type test 
patterns for final classes, this could be performed as an O(1) operation 
by hashing.  And this is a common case involving switches over 
alternatives in a sum (sealed) type. (We probably shouldn't rely on 
finality at compile time, as this can change between compile and run 
time, but we would like to take advantage of this at run time if we can.)


Finally, the straightforward static translation may miss opportunities 
for optimization.  For example:


    switch (x) {
    case Point p
    where p.x > 0 && p.y > 0: A
    case Point p
    where p.x > 0 && p.y == 0: B
    }

Here, not only would we potentially test the target twice to see if it 
is a `Point`, but we then further extract the `x` component twice and 
perform the `p.x > 0` test twice.


 Optimization opportunities

The compiler can eliminate some redundant calculations through 
straightforward techniques.  The previous switch can be transformed to:


    switch (x) {
    case Point p:
    if (((Point) p).x > 0 && ((Point) p).y > 0) { A }
    else if (((Point) p).x > 0 && ((Point) p).y > 0) { B }

to eliminate the redundant `instanceof` (and could be further 
transformed to eliminate the downstream redundant computations.)


 Clause reordering

The above example was easy to transform because the two `case Point` 
clauses were adjacent.  But what if they are not?  In some cases, it is 
safe to reorder them.  For types `T` and `U`, it is safe to reorder 
`case T` and `case U` if the two types have no intersection; that there 
can be no types that are subtypes of them both.  This is true when `T` 
and `U` are classes and neither extends the other, or when one is a 
final class and the other is an interface that the class does not 
implement.


The compiler could then reorder case clauses so that all the ones whose 
first test is `case Point` are adjacent, and then coalesce them all into 
a single arm of the `if-else` chain.


A possible spoiler here is fallthrough; if case A falls into case B, 
then cases A and B have to be moved as a group.  (This is another reason 
to consider limiting fallthrough.)


 Summary of if-else translation

While the if-else translation at first looks pretty bad, we are able to 
extract a fair amount of redundancy through well-understood compiler 
transformations.  If an N-way switch has only M distinct types in it, in 
most cases we can reduce the cost from _O(N)_ to _O(M)_.  Sometimes _M 
== N_, so this doesn't help, but sometimes _M << N_ (and sometimes `N` 
is small, in which case _O(N)_ is fine.)


Reordering clau

Switch translation, part 1

2017-12-07 Thread Brian Goetz
This doc covers some groundwork on rationalizing existing translation of 
switch as we move towards pattern-enabled switches.



# Switch Translation, Part 1
 Maurizio Cimadamore and Brian Goetz
 December 2017

This document examines the current translation of `switch` constructs by 
`javac`, and proposes a more general translation for switching on 
primitives, boxes, strings, and enums, with the goals of:


 - Unify the treatment of `switch` variants, simplifying the compiler 
implementation and reducing the static footprint of generated code;
 - Move responsibility for target classification from compile time to 
run time, allowing us to more freely update the logic without updating 
the compiler;

 - Lay the groundwork for patterns in `switch`.

Part 2 of this document will focus on the challenges of translating 
pattern `switch`.


## Current translation

Switches on `int` (and the smaller integer primitives) are translated in 
one of two ways.  If the labels are relatively dense, we translate an 
`int` switch to a `tableswitch`; if they are sparse, we translate to a 
`lookupswitch`.  The current heuristic appears to be that we use a 
`tableswitch` if it results in a smaller bytecode than a `lookupswitch` 
(which uses twice as many bytes per entry), which is a reasonable 
heuristic.


 Switches on boxes

Switches on primitive boxes are currently implemented as if they were 
primitive switches, unconditionally unboxing the target before entry 
(possibly throwing NPE).


 Switches on strings

Switches on strings are implemented as a two-step process, exploiting 
the fact that strings cache their `hashCode()` and that hash codes are 
reasonably spread out. Given a switch on strings like the one below:


    switch (s) {
    case "Hello": ...
        case "World": ...
    default: ...
    }

The compiler desugar this into two separate switches, where the first 
switch maps the input strings into a range of numbers [0..1], as shown 
below, which can then be used in a subsequent plain switch on ints.  The 
generated code unconditionally calls `hashCode()`, again possibly 
throwing NPE.


    int index=-1;
    switch (s.hashCode()) {
        case 12345: if (!s.equals("Hello")) break; index = 1; break;
        case 6789: if (!s.equals("World")) break; index = 0; break;
        default: index = -1;
    }
    switch (index) {
        case 0: ...
    case 1: ...
        default: ...
    }

If there are hash collisions between the strings, the first switch must 
try all possible matching strings.


 Switches on enums

Switches on `enum` constants exploit the fact that enums have (usually 
dense) integral ordinal values.  Unfortunately, because an ordinal value 
can change between compilation time and runtime, we cannot rely on this 
mapping directly, but instead need to do an extra layer of mapping.  
Given a switch like:


    switch(color) {
    case RED: ...
    case GREEN: ...
    }

The compiler numbers the cases starting a 1 (as with string switch), and 
creates a synthetic class that maps the runtime values of the enum 
ordinals to the statically numbered cases:


    class Outer$0 {
    synthetic final int[] $EnumMap$Color = new 
int[Color.values().length];

        static {
            try { $EnumMap$Color[RED.ordinal()] = 1; } catch 
(NoSuchFieldError ex) {}
            try { $EnumMap$Color[GREEN.ordinal()] = 2; } catch 
(NoSuchFieldError ex) {}

    }
    }

Then, the switch is translated as follows:

    switch(Outer$0.$EnumMap$Color[color.ordinal()]) {
    case 1: stmt1;
        case 2: stmt2
    }

In other words, we construct an array whose size is the cardinality of 
the enum, and then the element at position *i* of such array will 
contain the case index corresponding to the enum constant with whose 
ordinal is *i*.


## A more general scheme

The handling of strings and enums give us a hint of how to create a more 
regular scheme; for `switch` targets more complex than `int`, we lower 
the `switch` to an `int` switch with consecutive `case` labels, and use 
a separate process to map the target into the range of synthetic case 
labels.


Now that we have `invokedynamic` in our toolbox, we can reduce all of 
the non-`int` cases to a single form, where we number the cases with 
consecutive integers, and perform case selection via an 
`invokedynamic`-based classifier function, whose static argument list 
receives a description of the actual targets, and which returns an `int` 
identifying what `case` to select.


This approach has several advantages:
 - Reduced compiler complexity -- all switches follow a common pattern;
 - Reduced static code size;
 - The classification function can select from a wide range of 
strategies (linear search, binary search, building a `HashMap`, 
constructing a perfect hash function, etc), which can vary over time or 
from situation to situation;
 - We are free to improve the