Compile-time generated dispatch
A dispatch table whose size is a compile-time parameter, generated by comptime for and
lowered to a single br_table. This is the payoff of pairing switch with
compile-time parameters: the arms are written once, the count is
a param, and the compiled module contains neither the loop nor the param — just the
table.
param OpCount: i32 = 6;
// One global per operation, named Op0..Op5 by pasting the loop variable.
comptime for i in 0..OpCount {
global [<Op $i>]: mut i32 = 0;
}
// `comptime for` inside the switch body generates one `case` arm per operation. Because
// case values must be compile-time-known, `$i` is a legal one -- that is exactly what
// makes this work.
func record(op: i32) -> i32 {
switch (op) {
comptime for i in 0..OpCount {
case $i: {
[<Op $i>] = [<Op $i>] + 1;
return $i;
}
}
// Required, and reached by anything outside 0..OpCount.
default: { return -1; }
}
}
func total() -> i32 {
var sum: i32 = 0;
comptime for i in 0..OpCount {
sum = sum + [<Op $i>];
}
return sum;
}
func check() -> i32 {
// `OpCount` is readable from ordinary expressions, so these probes stay correct
// for any OpCount >= 2 -- including under the `--define` override below.
if (record(0) != 0) { return 1; }
if (record(OpCount - 1) != OpCount - 1) { return 2; }
if (record(OpCount - 1) != OpCount - 1) { return 3; }
// Out of range, so nothing is recorded.
if (record(OpCount) != -1) { return 4; }
if (total() != 3) { return 5; }
if (Op0 != 1) { return 6; }
return 0;
}
export func check as "check";
export func record as "record";
check() returns 0.
What the compiler emits
With OpCount = 6, record becomes a single br_table over seven targets — six arms plus
the default — rather than six sequential comparisons:
;; switch (op)
(block $switch_exit_2
(local.set $switch_on_1 (local.get $op_0))
(block $switch_body_10
(block $switch_default_3
(block $switch_case5_9
(block $switch_case4_8
(block $switch_case3_7
(block $switch_case2_6
(block $switch_case1_5
(block $switch_case0_4
(br_table $switch_case0_4 $switch_case1_5 $switch_case2_6
$switch_case3_7 $switch_case4_8 $switch_case5_9
$switch_default_3
(local.get $switch_on_1)))
;; Op0 = Op0 + 1;
;; ... arm 0's body, then (br $switch_exit_2) ...
Note the block nesting runs opposite to arm order: the first arm is innermost, because
every br_table target must be in scope where the table appears, and each arm's body
follows the end of the block whose label the table names.
Changing the count without editing the source
--define overrides the param, so the same file produces a different table:
$ reedc build dispatch.reed --define OpCount=3 -o dispatch.wat
That emits three globals and a four-target table. Nothing else in the source changes, and
there is no trace of Op3..Op5.
When it is not a table
Widely-spaced case values would need an impractical table, so the compiler falls back to an
i32.eq comparison chain. The dispatch behaves identically; only the output size differs.
See switch for the rules, and
compile-time parameters for param/comptime for/[<...>].