On Wed, Aug 3, 2022 at 6:26 AM j <j...@lambda.is> wrote: > > I've proposed a patch [1] for condition coverage profiling in gcc, > implemented in the middle-end alongside the branch coverage. I've > written most of the tests for C and a few for C++ and finally got around > to try it with a toy example for D and go and noticed something odd > about Go's CFG construction. > > abc.c: > int fn (int a, int b, int c) { > if (a && (b || c)) > return a; > else > return b * c; > } > > abc.d: > int fn (int a, int b, int c) { > if (a && (b || c)) > return a; > else > return b * c; > } > > abc.go: > func fn (a int, b int, c int) int { > a_ := a != 0; > b_ := b != 0; > c_ := c != 0; > > if a_ && (b_ || c_) { > return 1; > } else { > return 0; > } > } > > All were built with gcc --coverage -fprofile-conditions (my patch, but > it does not affect this) and no optimization. The C and D programs > behaved as expected: > > gcov --conditions abc.d: > > 3: 3:int fn (int a, int b, int c) { > 3*: 4: if (a && (b || c)) > conditions outcomes covered 3/6 > condition 1 not covered (false) > condition 2 not covered (true) > condition 2 not covered (false) > 1: 5: return a; > -: 6: else > 2: 7: return b * c; > > > gcov --conditions abc.go: > 3: 5:func fn (a int, b int, c int) int { > 3: 6: a_ := a != 0; > 3: 7: b_ := b != 0; > 3: 8: c_ := c != 0; > -: 9: > 3*: 10: if a_ && (b_ || c_) { > condition outcomes covered 2/2 > condition outcomes covered 1/2 > condition 0 not covered (true) > condition outcomes covered 2/2 > 1: 11: return 1; > -: 12: } else { > 2: 13: return 0; > -: 14: } > -: 15:} > > So I dumped the gimple gcc -fdump-tree-gimple abc.go: > > int main.fn (int a, int b, int c) > { > int D.2725; > int $ret0; > > $ret0 = 0; > { > bool a_; > bool b_; > bool c_; > > a_ = a != 0; > b_ = b != 0; > c_ = c != 0; > { > { > GOTMP.0 = a_; > if (GOTMP.0 != 0) goto <D.2719>; else goto <D.2720>; > <D.2719>: > { > { > GOTMP.1 = b_; > _1 = ~GOTMP.1; > if (_1 != 0) goto <D.2721>; else goto <D.2722>; > <D.2721>: > { > GOTMP.1 = c_; > } > <D.2722>: > } > GOTMP.2 = GOTMP.1; > GOTMP.0 = GOTMP.2; > } > <D.2720>: > } > if (GOTMP.0 != 0) goto <D.2723>; else goto <D.2724>; > <D.2723>: > > > > { > { > $ret0 = 1; > D.2725 = $ret0; > // predicted unlikely by early return (on trees) predictor. > return D.2725; > } > } > <D.2724>: > { > { > $ret0 = 0; > D.2725 = $ret0; > // predicted unlikely by early return (on trees) predictor. > return D.2725; > } > } > } > } > } > > Where as D (and C) is more-or-less as you would expect: > > int fn (int a, int b, int c) > > > > { > int D.7895; > > if (a != 0) goto <D.7893>; else goto <D.7891>; > <D.7893>: > if (b != 0) goto <D.7892>; else goto <D.7894>; > <D.7894>: > if (c != 0) goto <D.7892>; else goto <D.7891>; > <D.7892>: > D.7895 = a; > // predicted unlikely by early return (on trees) predictor. > return D.7895; > <D.7891>: > D.7895 = b * c; > // predicted unlikely by early return (on trees) predictor. > return D.7895; > } > > Clearly the decision inference algorithm is unable to properly > instrument to Go program for condition coverage because of the use of > temporaries in the emitted GIMPLE. The question is: is this intentional > and/or required from Go's semantics or could it be considered a defect? > Is emitting the GIMPLE without the use of temporaries feasible at all?
The Go frontend converts && and || expressions into code that uses explicit if statements. This is done largely as an internal simplification. Go has rules about the order in which function calls and certain other kinds of expressions must be evaluated. Separating out the order of evaluation imposed by && and || simplifies the implementation of the other order of evaluation rules. Ian