Fixture 215

switch on wide selector

C · 5 functions · 4 lanes · 17 of 20 function-lanes behave identically

2 of 4 lanes have a function that returns a different result after decompilation: clang-O2 (3/5), gcc-O2 (4/5).

A switch whose selector is 64 bits wide, with case labels above 2^32.

WHY THE WIDTH MATTERS. Every part of dispatch recovery is 32-bit-shaped by habit: the guard that proves a range bound reads a 32-bit compare, the table index is a 32-bit register, and DispatchTracker::bounded maps a register to a u64 bound that is nevertheless only ever ESTABLISHED from a 32-bit comparison. A 64-bit selector changes three things at once — the compare is against a wide immediate (often itself from a literal pool), the index must be truncated before it can address a table, and a case label above 2^32 cannot be a table offset at all, so the compiler must emit a comparison tree for the high labels and may emit a table for a dense low cluster.

That mixture — a tree over clusters, with a table inside one cluster — is the shape where a recogniser that assumes "one dispatch per switch" loses the arms it did not expect.

Every existing switch fixture is 32-bit: 04_switch_shapes, 106_switch_shapes_dense_sparse, 154_wide_switch (wide in ARM COUNT, not in selector width), 186_defaultless_guarded_switch, 204_adjacent_dispatch_tables. None declares a uint64_t selector, and none has a label that does not fit in 32 bits.

115_enum_semantics covers the underlying type of an enum, which is a different question: an enum's constants are small.

tests/decompiler_fixtures/src/215_switch_on_wide_selector.c source
#include <stdint.h>

/* A `switch` whose selector is 64 bits wide, with case labels above 2^32.
 *
 * WHY THE WIDTH MATTERS. Every part of dispatch recovery is 32-bit-shaped by
 * habit: the guard that proves a range bound reads a 32-bit compare, the table
 * index is a 32-bit register, and `DispatchTracker::bounded` maps a register to
 * a `u64` bound that is nevertheless only ever ESTABLISHED from a 32-bit
 * comparison. A 64-bit selector changes three things at once — the compare is
 * against a wide immediate (often itself from a literal pool), the index must
 * be truncated before it can address a table, and a case label above 2^32
 * cannot be a table offset at all, so the compiler must emit a comparison tree
 * for the high labels and may emit a table for a dense low cluster.
 *
 * That mixture — a tree over clusters, with a table inside one cluster — is the
 * shape where a recogniser that assumes "one dispatch per switch" loses the
 * arms it did not expect.
 *
 * Every existing switch fixture is 32-bit: `04_switch_shapes`,
 * `106_switch_shapes_dense_sparse`, `154_wide_switch` (wide in ARM COUNT, not
 * in selector width), `186_defaultless_guarded_switch`,
 * `204_adjacent_dispatch_tables`. None declares a `uint64_t` selector, and none
 * has a label that does not fit in 32 bits.
 *
 * `115_enum_semantics` covers the underlying type of an enum, which is a
 * different question: an enum's constants are small.
 */

/* Dense 64-bit labels in the low range: a table is legal here, and the selector
 * must be truncated to index it. */
__attribute__((noinline)) int32_t wide_selector_dense(uint64_t op) {
    switch (op) {
    case 0:  return 10;
    case 1:  return 11;
    case 2:  return 12;
    case 3:  return 13;
    case 4:  return 14;
    case 5:  return 15;
    case 6:  return 16;
    case 7:  return 17;
    default: return -1;
    }
}

/* Labels ABOVE 2^32: no table can address these, so every target emits
 * comparisons against wide constants. */
__attribute__((noinline)) int32_t wide_selector_high_labels(uint64_t op) {
    switch (op) {
    case 0x100000000ull: return 20;
    case 0x100000001ull: return 21;
    case 0x200000000ull: return 22;
    case 0xFFFFFFFFFFFFFFFFull: return 23;
    default: return -1;
    }
}

/* A dense low cluster AND high labels in one switch — a comparison tree whose
 * leaves include a jump table. */
__attribute__((noinline)) int32_t wide_selector_mixed(uint64_t op) {
    switch (op) {
    case 0:  return 30;
    case 1:  return 31;
    case 2:  return 32;
    case 3:  return 33;
    case 4:  return 34;
    case 5:  return 35;
    case 0x100000000ull: return 36;
    case 0x8000000000000000ull: return 37;
    default: return -1;
    }
}

/* A SIGNED 64-bit selector with negative labels, so the guard is a signed
 * comparison and an unsigned range proof would be wrong. */
__attribute__((noinline)) int32_t signed_wide_selector(int64_t op) {
    switch (op) {
    case -3: return 40;
    case -2: return 41;
    case -1: return 42;
    case  0: return 43;
    case  1: return 44;
    case  2: return 45;
    default: return -1;
    }
}

/* CONTROL: the identical dense switch with a 32-bit selector. It must recover
 * at least as well as the 64-bit version; if the wide one regresses and this
 * does not, the width is the cause. */
__attribute__((noinline)) int32_t narrow_selector_control(uint32_t op) {
    switch (op) {
    case 0:  return 10;
    case 1:  return 11;
    case 2:  return 12;
    case 3:  return 13;
    case 4:  return 14;
    case 5:  return 15;
    case 6:  return 16;
    case 7:  return 17;
    default: return -1;
    }
}

Recovered C

Generated by glaurung decompile --style decbench at b47f6b43. baseline.json records the result after recompiling the C and calling it beside the original with seeded inputs.

clang -O2

3/5
narrow_selector_control pass 4 lines
// glaurung: narrow_selector_control @ 0x1200
int32_t narrow_selector_control(uint32_t arg0) {
    return (((unsigned long)((unsigned long)(arg0)) < (unsigned long)(8)) ? (unsigned long)((unsigned int)((arg0 + 10))) : 0xffffffff);
}
signed_wide_selector pass 6 lines
// glaurung: signed_wide_selector @ 0x11e0
int32_t signed_wide_selector(int64_t arg0) {
    long var0;
    var0 = (arg0 + 3);
    return (((unsigned long)(var0) < (unsigned long)(6)) ? (unsigned long)((unsigned int)((var0 + 40))) : 0xffffffff);
}
wide_selector_dense pass 4 lines
// glaurung: wide_selector_dense @ 0x1100
int32_t wide_selector_dense(uint64_t arg0) {
    return (((unsigned long)(arg0) < (unsigned long)(8)) ? (unsigned long)((unsigned int)((arg0 + 10))) : 0xffffffff);
}
wide_selector_high_labels fail 21 lines
// glaurung: wide_selector_high_labels @ 0x1110
int32_t wide_selector_high_labels(uint64_t arg0) {
    long var0;
    var0 = 0x100000000;
    if ((arg0 <= 0x100000000)) {
        if ((arg0 == -1)) {
            return 23;
        }
        if ((arg0 != var0)) {
            return 0xffffffff;
        }
        return 20;
    }
    if ((arg0 == 0x200000000)) {
        return 22;
    }
    if ((arg0 != 0x100000001)) {
        return 0xffffffff;
    }
    return 21;
}
wide_selector_mixed fail 13 lines
// glaurung: wide_selector_mixed @ 0x1170
int32_t wide_selector_mixed(uint64_t arg0) {
    if (((unsigned long)(arg0) <= (unsigned long)(5))) {
        /* unrecovered indirect jump through ((long)((int)(((arg0 == 0) ? 0xfffff1d8 : ((arg0 == 1) ? 0xfffff18b : ((arg0 == 2) ? 0xfffff1b5 : ((arg0 == 3) ? 0xfffff1bb : ((arg0 == 4) ? 0xfffff1c1 : ((arg0 == 5) ? 0xfffff1c7 : *(int *)((0x2000 + (arg0 * 4))))))))))) + 0x2000) */
    }
    if ((arg0 == (-0x7fffffffffffffffLL - 1LL))) {
        return 37;
    }
    if ((arg0 != 0x100000000)) {
        return 0xffffffff;
    }
    return 36;
}

gcc -O2

4/5
narrow_selector_control pass 4 lines
// glaurung: narrow_selector_control @ 0x1260
int32_t narrow_selector_control(uint32_t arg0) {
    return (((unsigned long)(8) <= (unsigned long)((unsigned long)(arg0))) ? 0xffffffff : (unsigned long)((unsigned int)((arg0 + 10))));
}
signed_wide_selector pass 4 lines
// glaurung: signed_wide_selector @ 0x1240
int32_t signed_wide_selector(int64_t arg0) {
    return (((unsigned long)((arg0 + 3)) < (unsigned long)(6)) ? (unsigned long)((unsigned int)((arg0 + 43))) : 0xffffffff);
}
wide_selector_dense pass 4 lines
// glaurung: wide_selector_dense @ 0x1100
int32_t wide_selector_dense(uint64_t arg0) {
    return (((unsigned long)(8) <= (unsigned long)(arg0)) ? 0xffffffff : (unsigned long)((unsigned int)((arg0 + 10))));
}
wide_selector_high_labels pass 15 lines
// glaurung: wide_selector_high_labels @ 0x1120
int32_t wide_selector_high_labels(uint64_t arg0) {
    long ret;
    if ((arg0 == 0x200000000)) {
        return 22;
    }
    if (((unsigned long)(0x200000000) < (unsigned long)(arg0))) {
        return ((arg0 == -1) ? 23 : 0xffffffff);
    }
    ret = 20;
    if ((arg0 == 0x100000000)) {
        return ret;
    }
    return ((arg0 == 0x100000001) ? 21 : 0xffffffff);
}
wide_selector_mixed fail 10 lines
// glaurung: wide_selector_mixed @ 0x1190
int32_t wide_selector_mixed(uint64_t arg0) {
    if (((unsigned long)(arg0) <= (unsigned long)(5))) {
        /* unrecovered indirect jump through ((long)((int)(((arg0 == 0) ? 0xfffff1e0 : ((arg0 == 1) ? 0xfffff1f0 : ((arg0 == 2) ? 0xfffff200 : ((arg0 == 3) ? 0xfffff210 : ((arg0 == 4) ? 0xfffff220 : ((arg0 == 5) ? 0xfffff230 : *(int *)((0x2000 + (arg0 * 4))))))))))) + 0x2000) */
    }
    if ((arg0 != 0x100000000)) {
        return ((arg0 == (-0x7fffffffffffffffLL - 1LL)) ? 37 : 0xffffffff);
    }
    return 36;
}

clang -O0

5/5
narrow_selector_control pass 25 lines
// glaurung: narrow_selector_control @ 0x1410
int32_t narrow_selector_control(uint32_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case 0:
            return (unsigned int)(10);
        case 1:
            return (unsigned int)(11);
        case 2:
            return (unsigned int)(12);
        case 3:
            return (unsigned int)(13);
        case 4:
            return (unsigned int)(14);
        case 5:
            return (unsigned int)(15);
        case 6:
            return (unsigned int)(16);
        case 7:
            return (unsigned int)(17);
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}
signed_wide_selector pass 23 lines
// glaurung: signed_wide_selector @ 0x1380
int32_t signed_wide_selector(int64_t arg0) {
    long var1;
    // x86-64 prologue: save rbp
    var1 = (arg0 + 3);
    switch (var1) {
        case 0:
            return (unsigned int)(40);
        case 1:
            return (unsigned int)(41);
        case 2:
            return (unsigned int)(42);
        case 3:
            return (unsigned int)(43);
        case 4:
            return (unsigned int)(44);
        case 5:
            return (unsigned int)(45);
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}
wide_selector_dense pass 25 lines
// glaurung: wide_selector_dense @ 0x1100
int32_t wide_selector_dense(uint64_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case 0:
            return 10;
        case 1:
            return 11;
        case 2:
            return 12;
        case 3:
            return 13;
        case 4:
            return 14;
        case 5:
            return 15;
        case 6:
            return 16;
        case 7:
            return 17;
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}
wide_selector_high_labels pass 17 lines
// glaurung: wide_selector_high_labels @ 0x11a0
int32_t wide_selector_high_labels(uint64_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case -1:
            return 23;
        case 4294967296:
            return 20;
        case 4294967297:
            return 21;
        case 8589934592:
            return 22;
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}
wide_selector_mixed pass 25 lines
// glaurung: wide_selector_mixed @ 0x1250
int32_t wide_selector_mixed(uint64_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case -9223372036854775808:
            return 37;
        case 0:
            return 30;
        case 1:
            return 31;
        case 2:
            return 32;
        case 3:
            return 33;
        case 4:
            return 34;
        case 5:
            return 35;
        case 4294967296:
            return 36;
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}

gcc -O0

5/5
narrow_selector_control pass 25 lines
// glaurung: narrow_selector_control @ 0x1308
int32_t narrow_selector_control(uint32_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case 0:
            return 10;
        case 1:
            return 11;
        case 2:
            return 12;
        case 3:
            return 13;
        case 4:
            return 14;
        case 5:
            return 15;
        case 6:
            return 16;
        case 7:
            return 17;
        default:
            return 0xffffffff;
    }
    // x86-64 epilogue: restore rbp
}
signed_wide_selector pass 21 lines
// glaurung: signed_wide_selector @ 0x129c
int32_t signed_wide_selector(int64_t arg0) {
    // x86-64 prologue: save rbp
    switch ((arg0 + 3)) {
        case 0:
            return 40;
        case 1:
            return 41;
        case 2:
            return 42;
        case 3:
            return 43;
        case 4:
            return 44;
        case 5:
            return 45;
        default:
            return 0xffffffff;
    }
    // x86-64 epilogue: restore rbp
}
wide_selector_dense pass 25 lines
// glaurung: wide_selector_dense @ 0x10f9
int32_t wide_selector_dense(uint64_t arg0) {
    // x86-64 prologue: save rbp
    switch (arg0) {
        case 0:
            return 10;
        case 1:
            return 11;
        case 2:
            return 12;
        case 3:
            return 13;
        case 4:
            return 14;
        case 5:
            return 15;
        case 6:
            return 16;
        case 7:
            return 17;
        default:
            return 0xffffffff;
    }
    // x86-64 epilogue: restore rbp
}
wide_selector_high_labels pass 25 lines
// glaurung: wide_selector_high_labels @ 0x1170
int32_t wide_selector_high_labels(uint64_t arg0) {
    // x86-64 prologue: save rbp
    if ((arg0 == -1)) {
        return 23;
    } else {
        if ((arg0 == 0x200000000)) {
            return 22;
        } else {
            if (((unsigned long)(0x200000000) < (unsigned long)(arg0))) {
                // x86-64 epilogue: restore rbp
                return 0xffffffff;
            }
            if ((arg0 == 0x100000000)) {
                return 20;
            } else {
                if ((arg0 == 0x100000001)) {
                    return 21;
                } else {
                    return 0xffffffff;
                }
            }
        }
    }
}
wide_selector_mixed pass 36 lines
// glaurung: wide_selector_mixed @ 0x11e8
int32_t wide_selector_mixed(uint64_t arg0) {
    // x86-64 prologue: save rbp
    if ((arg0 == (-0x7fffffffffffffffLL - 1LL))) {
        return 37;
    }
    if (((unsigned long)((-0x7fffffffffffffffLL - 1LL)) < (unsigned long)(arg0))) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    if (((unsigned long)(5) < (unsigned long)(arg0))) {
        if ((arg0 == 0x100000000)) {
            return 36;
        } else {
            return 0xffffffff;
        }
    }
    switch (arg0) {
        case 0:
            return 30;
        case 1:
            return 31;
        case 2:
            return 32;
        case 3:
            return 33;
        case 4:
            return 34;
        case 5:
            return 35;
        default:
            // x86-64 epilogue: restore rbp
            return 0xffffffff;
    }
    // x86-64 epilogue: restore rbp
}

← 213 fixtures