From 27ae4848d543775d033d81f329897da42b56c2ca Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 13:06:03 +1100 Subject: [PATCH 01/14] Add a clang-oracle fuzzer, a compiler bug log and parked reproducers scripts/clang-oracle-fuzz.py generates whole C programs (statements, loops, switches, nested-block locals, structs, bit-fields, pointers, calls) and compares dcc's output with the host compiler's. The source is the same for both compilers: it uses fixed-width types only, the renderer inserts the casts that make it mean the same with a 16-bit int, and a host-only checked build under ASan and UBSan rejects any program whose result could legitimately differ. It also reduces a failing program to a small one. compiler-bugs.md logs what it found at 83dc7a91: 19 open defects in the shipped configuration (9 wrong results, 1 hang, 9 rejections of valid C), each filed upstream as #223-#241. Nothing is fixed here. tests/known-bugs/ holds one reproducer per open entry with a baseline taken from clang; scripts/run-known-bugs.py proves each baseline with the host compiler and reports whether dcc still fails it. They are outside tests/*.c, so the regression suite is unaffected. Three new suite tests cover shapes that upstream fixed in 34e35b3a while this was in progress: twcasgm (32-bit value compound-assigned to a narrower member, element or pointer target), tblkdiv (32-bit member divided by a nested-block 16-bit local) and tcastasg (narrowing cast of an assignment's value). Their baselines are clang's output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 474 ++++++ scripts/README.md | 76 + scripts/clang-oracle-fuzz.py | 779 +++++++++ scripts/clang_oracle_gen.py | 2096 +++++++++++++++++++++++++ scripts/run-known-bugs.py | 161 ++ tests/README.md | 9 + tests/baselines/tblkdiv.txt | 4 + tests/baselines/tcastasg.txt | 4 + tests/baselines/twcasgm.txt | 4 + tests/known-bugs/README.md | 52 + tests/known-bugs/baselines/bug002.txt | 4 + tests/known-bugs/baselines/bug003.txt | 12 + tests/known-bugs/baselines/bug004.txt | 4 + tests/known-bugs/baselines/bug006.txt | 4 + tests/known-bugs/baselines/bug007.txt | 3 + tests/known-bugs/baselines/bug008.txt | 2 + tests/known-bugs/baselines/bug009.txt | 3 + tests/known-bugs/baselines/bug010.txt | 4 + tests/known-bugs/baselines/bug011.txt | 4 + tests/known-bugs/baselines/bug012.txt | 2 + tests/known-bugs/baselines/bug013.txt | 13 + tests/known-bugs/baselines/bug014.txt | 4 + tests/known-bugs/baselines/bug015.txt | 4 + tests/known-bugs/baselines/bug016.txt | 9 + tests/known-bugs/baselines/bug017.txt | 2 + tests/known-bugs/baselines/bug018.txt | 4 + tests/known-bugs/baselines/bug019.txt | 7 + tests/known-bugs/baselines/bug020.txt | 6 + tests/known-bugs/baselines/bug021.txt | 4 + tests/known-bugs/bug002.c | 47 + tests/known-bugs/bug003.c | 54 + tests/known-bugs/bug004.c | 54 + tests/known-bugs/bug006.c | 41 + tests/known-bugs/bug007.c | 52 + tests/known-bugs/bug008.c | 35 + tests/known-bugs/bug009.c | 56 + tests/known-bugs/bug010.c | 47 + tests/known-bugs/bug011.c | 50 + tests/known-bugs/bug012.c | 27 + tests/known-bugs/bug013.c | 57 + tests/known-bugs/bug014.c | 64 + tests/known-bugs/bug015.c | 64 + tests/known-bugs/bug016.c | 84 + tests/known-bugs/bug017.c | 24 + tests/known-bugs/bug018.c | 56 + tests/known-bugs/bug019.c | 101 ++ tests/known-bugs/bug020.c | 82 + tests/known-bugs/bug021.c | 58 + tests/perf_baselines.csv | 3 + tests/tblkdiv.c | 65 + tests/tcastasg.c | 64 + tests/twcasgm.c | 60 + 52 files changed, 4999 insertions(+) create mode 100644 compiler-bugs.md create mode 100644 scripts/clang-oracle-fuzz.py create mode 100644 scripts/clang_oracle_gen.py create mode 100644 scripts/run-known-bugs.py create mode 100644 tests/baselines/tblkdiv.txt create mode 100644 tests/baselines/tcastasg.txt create mode 100644 tests/baselines/twcasgm.txt create mode 100644 tests/known-bugs/README.md create mode 100644 tests/known-bugs/baselines/bug002.txt create mode 100644 tests/known-bugs/baselines/bug003.txt create mode 100644 tests/known-bugs/baselines/bug004.txt create mode 100644 tests/known-bugs/baselines/bug006.txt create mode 100644 tests/known-bugs/baselines/bug007.txt create mode 100644 tests/known-bugs/baselines/bug008.txt create mode 100644 tests/known-bugs/baselines/bug009.txt create mode 100644 tests/known-bugs/baselines/bug010.txt create mode 100644 tests/known-bugs/baselines/bug011.txt create mode 100644 tests/known-bugs/baselines/bug012.txt create mode 100644 tests/known-bugs/baselines/bug013.txt create mode 100644 tests/known-bugs/baselines/bug014.txt create mode 100644 tests/known-bugs/baselines/bug015.txt create mode 100644 tests/known-bugs/baselines/bug016.txt create mode 100644 tests/known-bugs/baselines/bug017.txt create mode 100644 tests/known-bugs/baselines/bug018.txt create mode 100644 tests/known-bugs/baselines/bug019.txt create mode 100644 tests/known-bugs/baselines/bug020.txt create mode 100644 tests/known-bugs/baselines/bug021.txt create mode 100644 tests/known-bugs/bug002.c create mode 100644 tests/known-bugs/bug003.c create mode 100644 tests/known-bugs/bug004.c create mode 100644 tests/known-bugs/bug006.c create mode 100644 tests/known-bugs/bug007.c create mode 100644 tests/known-bugs/bug008.c create mode 100644 tests/known-bugs/bug009.c create mode 100644 tests/known-bugs/bug010.c create mode 100644 tests/known-bugs/bug011.c create mode 100644 tests/known-bugs/bug012.c create mode 100644 tests/known-bugs/bug013.c create mode 100644 tests/known-bugs/bug014.c create mode 100644 tests/known-bugs/bug015.c create mode 100644 tests/known-bugs/bug016.c create mode 100644 tests/known-bugs/bug017.c create mode 100644 tests/known-bugs/bug018.c create mode 100644 tests/known-bugs/bug019.c create mode 100644 tests/known-bugs/bug020.c create mode 100644 tests/known-bugs/bug021.c create mode 100644 tests/tblkdiv.c create mode 100644 tests/tcastasg.c create mode 100644 tests/twcasgm.c diff --git a/compiler-bugs.md b/compiler-bugs.md new file mode 100644 index 000000000..41e5ea949 --- /dev/null +++ b/compiler-bugs.md @@ -0,0 +1,474 @@ +# dcc compiler bugs + +Defects found by differential testing against the host C compiler (Apple +clang). Every open entry reproduces with the shipped configuration: no +diagnostic environment variables, with and without `dccpeep`. Nothing here +has been fixed by this work; this file is the log. + +- **Compiler under test:** `83dc7a91` (upstream `main` on 2026-11-13). +- **Reproducers:** one program per open entry, `tests/known-bugs/bugNNN.c`, + with the output it should print in `tests/known-bugs/baselines/bugNNN.txt`. + Each baseline is the host compiler's output, and the program also runs + clean under ASan and UBSan, so it is defined C whose result does not depend + on `int` being 16 or 32 bits. +- **Check them:** `python3 scripts/run-known-bugs.py` builds each reproducer + with clang and with dcc and reports `fails` or `FIXED`. +- **How they were found:** `scripts/clang-oracle-fuzz.py` (see + `scripts/README.md`), then narrowed by hand with one-statement probes. +- **Reported:** each open entry is an issue on `davidly/dcc` (the Issue + column below), with the full reproducer and expected and actual output. + +## Summary + +| ID | Issue | Kind | Defect | +|---|---|---|---| +| [BUG-002](#bug-002) | #223 | wrong result | `unsigned char` local `<<=` an `unsigned char` variable is not truncated on return | +| [BUG-003](#bug-003) | #224 | wrong result | `++`/`--` on a 32-bit array element is done in 16 bits | +| [BUG-004](#bug-004) | #225 | wrong result | 16-bit local set from `&&`/`\|\|`, stored to a 32-bit lvalue, leaves a garbage high word | +| [BUG-006](#bug-006) | #226 | wrong result | `/=` and `%=` on an unsigned bit-field | +| [BUG-007](#bug-007) | #227 | rejects valid C, or crashes | Indirect call with a nested-block local argument that needs converting | +| [BUG-008](#bug-008) | #228 | rejects valid C | Negative constant assigned to a 32-bit nested-block local | +| [BUG-009](#bug-009) | #229 | hang | Nested `do … while (d < n)` with a 32-bit `d` never exits | +| [BUG-010](#bug-010) | #230 | rejects valid C | Cast or narrowing return of a 32-bit post-increment or post-decrement | +| [BUG-011](#bug-011) | #231 | rejects valid C | Compound assignment to an element of a byte array inside a struct | +| [BUG-012](#bug-012) | #232 | rejects valid C | Unary `-` or `~` applied to `!` of a constant | +| [BUG-013](#bug-013) | #233 | wrong result | Unsigned bit-field is not promoted to `int` once it is an operand | +| [BUG-014](#bug-014) | #234 | rejects valid C | 32-bit local with two definitions, incremented, stored to a global | +| [BUG-015](#bug-015) | #235 | wrong result | Nested-block pointer to a 32-bit object, and nested-block `unsigned char`, lose their type | +| [BUG-016](#bug-016) | #236 | wrong result | A local takes the signedness of its initialiser, not of its declaration | +| [BUG-017](#bug-017) | #237 | wrong result | Parameter `x &= x` returned through a widening cast gives 0 | +| [BUG-018](#bug-018) | #238 | rejects valid C | 32-bit value compound-assigned to a bit-field or to an element of a 16-bit array member | +| [BUG-019](#bug-019) | #239 | rejects valid C | Unreachable nested-scope declaration after `return`, `break` or `goto` | +| [BUG-020](#bug-020) | #240 | rejects valid C | After a `switch`, a variable compared with `param % k` (32-bit parameter) | +| [BUG-021](#bug-021) | #241 | wrong result | `x++`/`x--` yields the new value for a 16-bit local assigned from a local array element | + +Nineteen are open: nine give wrong results with no diagnostic, one hangs, +and nine stop the compile (BUG-007 can instead compile and crash). BUG-003, +BUG-013 and BUG-016 are the ones most likely to be hit by ordinary code. + +Two more were found and then fixed upstream before they were reported; see +[Fixed upstream](#fixed-upstream). + +### History + +Every open entry also reproduces with compilers built from `32d3e46d` (before +PRs #219 and #220), `5f733050` (tag 3.8.0) and `5bcac74e` (28 October), so +none is a regression from the last two weeks. Two are regressions since +v3.6.0 (`20290235`); a first-parent bisect gives: + +- BUG-007: first bad `55fbb06f` (2026-09-06, "fix: correct MIR lowering and + harden structural verification (#183)"). +- BUG-008: first bad `d49e3d7f` (2026-09-14, "Merge #194: harden AST/MIR + correctness, ABI and proof infrastructure"). + +The compile failures BUG-010, 011, 012 and 014 are already present in v3.6.0. +The wrong-result bugs and the hang also show with the v3.6.0 compiler, but it +was only tried with today's runtime, which is not a reliable pairing. + +--- + +## BUG-002 + +**An `unsigned char` local shifted left by an `unsigned char` variable is not +truncated to 8 bits when returned from a function returning `unsigned char`.** + +```c +static uint8_t a1[4] = { 200, 2, 3, 4 }; +static uint8_t count = 5; +static uint8_t f(void) { uint8_t x = a1[0]; x <<= count; return x; } +/* f() is 0 (6400 truncated to 8 bits); dcc returns 6400 */ +``` + +- Wrong whether the count is a global or a local `unsigned char`, and + whether `x` starts from memory or a constant. +- Correct with a constant count, an `int16_t` count, `count & 7`, + `x = x << count`, a `signed char x`, or a `uint16_t` return type. +- Selected emitter for `f`: `spilled-scalar-cfg`. + +## BUG-003 + +**`++` and `--` on a 32-bit array element are carried out in 16 bits and the +result is sign-extended.** Values that stay inside −32768…32767 are +unaffected, which is why small counters work. + +```c +static int32_t a[4] = { 0x80000000, 65535, 32767, 70000 }; +++a[0]; /* want 80000001, dcc 00000001 */ +a[1]++; /* want 00010000, dcc 00000000 */ +a[1]--; /* (from 65535) want 0000fffe, dcc fffffffe */ +``` + +- Wrong for `int32_t` and `uint32_t`, prefix and postfix, constant and + variable index, and through a pointer (`++p[1]`). +- Correct: `a[1] += 1`, a 32-bit global scalar, a 32-bit struct member, + `++*p`. + +## BUG-004 + +**An unsigned 16-bit local initialised from `&&` or `||`, then stored to a +32-bit lvalue, leaves garbage in the high word.** + +```c +uint16_t l = one || zero; +s.m1 = l; /* uint32_t m1: want 00000001, dcc xxxx0001 (e.g. 09f90001) */ +``` + +- Wrong with variable or constant operands, `&&` or `||`, and a 32-bit + member, array element or global as the destination. +- Correct: `l = 1`; `l = one`; `s.m1 = one || zero` stored directly. +- Commit `f00e062e` ("Widen && and || results converted to long or float") + fixed a neighbouring shape. + +## BUG-006 + +**`/=` and `%=` on an unsigned bit-field store the wrong value.** + +```c +struct S { uint16_t m2 : 11; uint16_t m5 : 5; } s = { 1148, 18 }; +s.m5 %= 7; /* want 4, dcc 2 */ +s.m5 /= 21930; /* want 0, dcc 2 */ +s.m2 %= 21930; /* want 1148, dcc 296 */ +``` + +- Correct: `s.m5 = s.m5 % 7`; `+=`; `%=` on a signed bit-field. + +## BUG-007 + +**A call through a function pointer whose argument is a local declared in a +nested block or a `for` initialiser, and whose type differs from the +parameter's, produces invalid MIR.** The verifier prints +`MIR f: instruction N has incorrect argument ABI type`. Then either the +compile stops with `MIR verification failed before required emission`, or, +in other surrounding code, dcc exits 0 and the program crashes at the call. + +```c +static int16_t take(unsigned char x); +static int16_t (*take_p)(unsigned char) = take; +{ int32_t v = wide; (*take_p)(v); } /* narrowing */ +for (int32_t i = 3; i != 0; --i) (*take_p)(i); /* for-init local */ +``` + +- Fails for narrowing and widening conversions, `(*p)(v)` and `p(v)`. +- Correct: the direct call `take(v)`; `v` declared at function scope; an + explicit cast `(*take_p)((unsigned char)v)`; an argument of the parameter's + own type. +- PR #219 fixed the direct-call form of this. +- Regression since v3.6.0; first-parent bisect: first bad `55fbb06f`. + +## BUG-008 + +**Assigning a negative constant to a 32-bit local declared in a nested block +stops the compile.** `dcc: fatal: MIR emission is required`. + +```c +{ uint32_t b = 201; b = -3; } +``` + +- Fails for `int32_t` and `uint32_t`, with `-3` or `(int32_t)-3`. An + `int32_t` local also fails with a constant whose value has the top bit set: + `b = 0x80000000;`, `b = (uint32_t)0xfffffffe;`. +- Correct: the local at function scope; a positive constant; a 16-bit local; + `uint32_t b; b = 0xfffffffe;`. +- Regression since v3.6.0; first-parent bisect: first bad `d49e3d7f`. + +## BUG-009 + +**A `do … while (d < n)` loop nested inside another loop never exits when +`d` is 32 bits wide.** The comparison behaves as always true. + +```c +int32_t i, d = 0, n = 0; +for (i = 0; i < 4; i++) { + n = limit; + do { ++d; } while (d < n); /* never leaves */ +} +``` + +With `&& passes < 100` added to the condition the function returns 103 +instead of 6, which shows the comparison is the part that is wrong. + +- Hangs for `int32_t` and `uint32_t` `d`; with `n` a local, a global, 16 or + 32 bits; with a 16- or 32-bit outer counter; with an outer `for` or `while`; + with `++d` in the body or in the condition. +- Correct: the loop not nested; `while (d < n) { … }`; `if (d < n)`; + `d != n`; `n` set from a constant; a 16-bit `d`. + +## BUG-010 + +**A cast applied to a 32-bit post-increment or post-decrement is rejected, +and so is returning one from a function with a narrower return type.** +`DCC-E1002`, reported against the enclosing statement. + +```c +r = (int16_t)(l2++); /* int32_t l2 */ +r = (int32_t)(u2--); /* uint32_t u2 */ +take((int8_t)(u2++)); +uint8_t f(void) { int32_t l3 = 300; return l3++; } +``` + +- Accepted: `r = l2++` with a 32-bit or a 16-bit `r`; `(int16_t)(++l2)`; + `(int16_t)(l1++)` on a 16-bit variable. +- Upstream commit `34e35b3a` fixed the neighbouring "cast of an assignment" + rejection, which this entry originally included. + +## BUG-011 + +**Compound assignment to an element of a `char`, `signed char` or +`unsigned char` array that is a struct member is rejected.** `DCC-E1002`. + +```c +struct B { uint8_t b[4]; char name[4]; } s, *p; +s.b[2] ^= 1; +s.b[i] |= 0x20; +p->name[i] |= 0x80; +``` + +- Rejected for every operator tried (`^= += -= |= &= <<= *=`), constant and + variable indexes, a struct variable, a pointer to struct, and an element of + an array of structs. +- Accepted: `s.b[2] = s.b[2] ^ 1`; `s.b[2]++`; a byte array that is not a + struct member; a 16-bit array member. + +## BUG-012 + +**Unary `-` or `~` applied to `!` of a constant is rejected.** `DCC-E1002`. + +```c +r = -(!(-128)); +r = ~(!15); +r = -(!0); +``` + +- Accepted: `-(!p)` and `~(!p)` with a variable; `r = !(-128); r = -r;`. + +## BUG-013 + +**An unsigned bit-field narrower than `int` is not promoted to `int` once it +is an operand.** dcc promotes it for `bf - 1000` written directly (which is +what `tests/tbfprom.c` checks). But the result of unary `-` or `~`, of any +other binary operator, or of `?:` with a bit-field arm, is typed +`unsigned int`: it is zero-extended when widened and compared as unsigned. + +```c +struct S { uint16_t m0 : 9; uint16_t m2 : 11; } s = { 444, 346 }; +long r; +r = -s.m0; /* want -444, dcc 65092 */ +r = (s.m2 + 1) - 1000; /* want -653, dcc 64883 */ +r = 1 - (sel ? s.m2 : 1); /* want -345, dcc 65191 */ +if ((sel ? -21931 : s.m1) > 1) /* false; dcc takes the branch */ +``` + +- Wrong for `- ~ | ^ & + * / % >> <<` and `?:`. +- Correct: `s.m2 - 1000`; `sel ? s.m2 : -1` assigned directly. + +## BUG-014 + +**A signed 32-bit local that has two reaching definitions, is incremented by +one and is then stored to a global stops the compile.** +`dcc: fatal: missing direct MIR named-home offset`. + +```c +int32_t l = 1; +if (sel) l = 5; +l++; /* also ++l, l += 1, l = l + 1, g = ++l */ +g = l; +``` + +- The second definition can come from `if`, `switch` or a `goto`. +- Accepted: `--l`; `l += 2`; a `uint32_t` local; storing to an array element + or a local; a local with a single definition. + +## BUG-015 + +**Locals declared in a nested block lose their declared type in further +shapes.** Through a nested-block pointer to a 32-bit object, `*q *= k` is +computed in 16 bits and `c <= *q` is compared as signed. A nested-block +`unsigned char` is compared as `unsigned int`. + +```c +{ uint32_t *q = &a4[4]; *q *= 11; } /* 21930 * 11: want 0003ae4e, dcc ffffae4e */ +signed char c = -1; +{ uint32_t *q = &a4[0]; if (c <= *q) r = 7; } /* false (c converts to 0xffffffff); dcc takes it */ +signed char l2 = -66; +{ unsigned char b = 1; if ((b & s.m5) > l2) return 1; } /* true; dcc does not take it */ +``` + +- Each is correct when the pointer or the `unsigned char` is declared at + function scope. +- These are grouped because they share the nested-block condition; they may + have separate causes. See also BUG-008. + +## BUG-016 + +**A local keeps the signedness of its initialiser instead of its declared +type.** It shows when the local is later widened to 32 bits or used in a +32-bit division. + +```c +uint16_t l = gs; /* int16_t gs = -31256 */ +long r = l; /* want 34280, dcc -31256 */ + +int16_t l = gu; /* uint16_t gu = 50000 */ +long r = l; /* want -15536, dcc 50000 */ + +int32_t l = 1u | a3[1]; /* a3[1] = -3, so l = -3 */ +r = x / l; /* uint16_t x = 50000: want -16666, dcc 0 */ +``` + +- Wrong when the initialiser is a global, a struct member, a parameter, a + function result, or a negative constant (`unsigned short l = -100;` then + `long r = l;` gives -100, not 65436). +- A cast to the declared type in the initialiser does not help when it only + changes signedness: `unsigned short l = (uint16_t)p;` with `int16_t p`, or + `int32_t l = (int32_t)u;` with `uint32_t u`, are still wrong. +- Correct when the value is assigned in a separate statement + (`uint16_t l; l = gs;`), when the initialiser's cast also changes width + (`(uint16_t)(int32_t)p`), for 8-bit locals, and when the local is only used + at its own width (`l > 5`, `l / 2`). + +## BUG-017 + +**An `unsigned short` parameter and-assigned with itself and returned through +a widening cast returns 0.** + +```c +static int16_t f(unsigned short x) { x &= x; return (uint32_t)x; } /* f(1) is 1; dcc 0 */ +``` + +- With `dccpeep` a second form is also wrong: `x &= x; return (int32_t)x;` + returns garbage for 40000 (2112 or 5184, depending on build options). +- Correct: `return x;`; `x |= x`; `x &= 3`; a local copy of the parameter. +- Unlikely in hand-written code; logged because the result is silently wrong. + +## BUG-018 + +**Compound assignment of a 32-bit value to a bit-field, or to an element of +a 16-bit array that is a struct member, is rejected.** `DCC-E1002`. Upstream +commit `34e35b3a` made the same statements compile for plain members, array +elements and pointer targets; these two kinds of target are what remains. + +```c +struct S { uint16_t u : 9; int16_t s : 7; int16_t w[2]; } g, *p; +static uint32_t r = 5; +static int32_t big = 70003; +g.u |= r; /* bit-field */ +p->u -= r; +g.s >>= (r & 15); /* 32-bit shift count */ +g.w[1] += big; /* element of an array member */ +``` + +- Rejected for unsigned and signed bit-fields, directly, through a pointer + and in an element of an array of structs; for `|= += -= ^= &= *= /= %= <<= + >>=`; and for elements of `int16_t` and `uint16_t` array members with a + constant or variable index. +- Accepted: a 16-bit right operand (`g.u |= r16`, `g.w[1] += r16`); a plain + 16-bit member (`g.plain += big`); a 32-bit array member (`g.l[1] += big`); + `g.u = g.u | r`. +- Byte array members are rejected whatever the operand: see BUG-011. + +## BUG-019 + +**A declaration in a nested scope that is unreachable because a `return`, +`break` or `goto` precedes it in the same block stops the compile.** +`dcc: fatal: MIR emission is required`. The declaration can be a block-local +or a `for` initialiser. + +```c +if (sel) { + return 1; + { int16_t b = 1; low += b; } /* unreachable block with a declaration */ +} +if (sel) { + return 2; + for (uint8_t k = 3; k < 5; ++k) low++; /* unreachable for-init declaration */ +} +``` + +- Fails after `return`, `break` and `goto`, inside `if`, `else`, `for`, + `while` and `switch` bodies. +- Accepted: the same dead code at function scope (not inside a nested + block); dead code with no declaration (`return 1; { low += 1; }`); a + `for` loop whose counter is declared at function scope; a conditional jump + (`if (sel) continue;`). +- Dead code like this is rare in hand-written programs, so this is low + priority; it is still valid C. + +## BUG-020 + +**After a `switch` statement, a relational comparison between a variable and +the 32-bit product, quotient or remainder of a 32-bit parameter stops the +compile.** `dcc: fatal: MIR emission is required`. + +```c +static int32_t f(int32_t x) +{ + switch (sel) { case 5: hits++; } + if (g16 >= (x % 115)) /* also x / 115, x * 115, x % g16 */ + return 1; + return 0; +} +``` + +- Fails with `>=` and `<`; with the variable an 8-bit struct array element, + an 8-, 16- or 32-bit global, or `(int32_t)g16`; as an `if` or `while` + condition and as a value (`hits += (g16 >= (x % 115));`). +- Accepted: no `switch` before it (an `if` or a `for` loop before it is + fine); `==`; the operands the other way round (`(x % 115) <= g16`); a + constant on the left; `x + 115` or `x >> 3`; the parameter copied to a + local first; the result held in a temporary; a 32-bit global in place of + the parameter. +- Upstream commit `81937302` ("Don't pair a wide helper operand with an + unpushed parameter or constant") fixed `if (x < n / x)`, which fails with + the same message; this looks like a neighbouring shape. + +## BUG-021 + +**The value of a post-increment or post-decrement is the new value instead +of the old one**, for a signed 16-bit local that was assigned from an element +of a local unsigned 16-bit array, in a function that also has a `signed char` +local initialised from a narrowing cast. + +```c +static uint8_t a0[8] = { 16, 76, 156, 127, 255, 188, 0, 57 }; +static uint16_t f(void) +{ + signed char l1 = (int8_t)(1 - a0[1]); + int16_t l3 = 1; + uint16_t v6[2] = { 0xffde, 9029 }; + l3 = v6[1]; + return l3++; /* want 9029, dcc 9030 */ +} +``` + +- Wrong for `return l3++`, `return l3--` (9028) and `r = l3++; return r;`, + with the element read directly or through a pointer to the array. +- Correct: without `l1`, or with `l1` initialised from a constant, declared + `int16_t` or `int32_t`, or initialised from `g32 - a0[1]`; with the array + `static`; with `l3` assigned from a global; with `l3` declared `uint16_t`; + `return ++l3`. +- The trigger is narrow and involves three unrelated-looking locals, so the + cause is probably shared state (a slot or register), not the increment + itself. + +--- + +## Fixed upstream + +Found at `81937302` and fixed by upstream commit `34e35b3a` ("Give +assignments, compound assignments and ++/-- their stored values") before +they were reported. Their reproducers now pass. + +- **BUG-001:** compound assignment of a 32-bit value to an 8- or 16-bit + member, array element or pointer target (`p->w ^= r`, `a8[i] += r`, + `*q <<= r`) was rejected with `DCC-E1002`. Bit-field targets and elements + of array members are still rejected: BUG-018. +- **BUG-005:** `{ uint16_t b = 1; s.m3 /= b; }` with a 32-bit member `m3` and + a nested-block divisor zeroed the member's high word. + +## Not yet confirmed + +- **`a1` hangs with the regional emitter forced.** With + `DCC_MIR_PREFER_REGIONAL=1` the `a1` test's `6502fun` scenario + (`-x -a:400 6502FUN.HEX`) runs past 30 s in both peep modes. That variable + is diagnostic-only, so this is latent at most, and it has not been + investigated. It was observed at `aa9b2f1c` and not rechecked since. diff --git a/scripts/README.md b/scripts/README.md index 25cb0d7ca..da1d8302b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -62,6 +62,82 @@ A failing seed keeps `t.c`, `t.expected` and each mode's output under `build/typed-expr-fuzz/`; an output line is the case number times ten plus the input index, then the result as an `unsigned long`, both in hex. +## `clang-oracle-fuzz.py` + +Differential fuzzer whose expected output comes from the host C compiler. It +complements `typed-expr-fuzz.py`: that one checks single expressions against a +model; this one generates whole programs and lets clang say what they print. + +Each seed is one program of helper and test functions over fixed-width +integers, arrays, structs, bit-fields and pointers, with `for`/`while`/`do` +loops, `switch`, nested-block locals, `goto`, calls with mixed-width +arguments, bounded recursion, calls through function pointers, compound +assignment and `++`/`--` on every kind of lvalue, and assignments used as +values. The generator and renderers are in `clang_oracle_gen.py`. + +The same source is compiled by clang and by dcc. It can be, because it never +spells `int` or `long`, and the renderer inserts the casts and masks that +make an expression mean the same with a 16-bit and a 32-bit `int`. What a +cast cannot fix (signed overflow at 16 bits, division by zero, and so on) is +caught at run time: a second, host-only rendering routes every such operation +through a checking helper, and the driver repairs the operations that trip, +or drops the program. A program is used only when + +- the checked build, under ASan and UBSan, reports nothing; +- clang finds no unsequenced modification and no missing `return`; and +- `-O0`, `-O2`, the sanitized build and the checked build all print the same. + +So a difference from dcc is a dcc defect, not a porting difference. + +```sh +python3 scripts/clang-oracle-fuzz.py --seeds 1-300 --off known +python3 scripts/clang-oracle-fuzz.py --seeds 1-300 --off known --configs all +python3 scripts/clang-oracle-fuzz.py --reduce 17,42 --config nopeep --off known +python3 scripts/clang-oracle-fuzz.py --generate 17 prog.c --off known +python3 scripts/clang-oracle-fuzz.py --list-configs +``` + +- `--configs production` builds with and without `dccpeep` and with and + without the stack check. `default` adds `DCC_MIR_NO_EXACT=1`. `all` adds one + config per MIR candidate, forced with `DCC_MIR_SELECT_CANDIDATE` (or + `DCC_MIR_PREFER_REGIONAL=1` for the regional one); a failure only there is + latent, not shipped behaviour. +- `--off known` leaves out every form that hits a bug already logged in + `compiler-bugs.md`, so a run reports new failures instead of the same ones + again. `KNOWN_BUGS` in `clang_oracle_gen.py` maps each such form to its + entry; delete a line when its bug is fixed. `--off` also takes individual + generator features (`-h` lists them) to narrow a search by hand. +- Failures are reported as `COMPILE` (dcc rejects the program), `VERIFY` (dcc + prints a MIR verifier diagnostic but still exits 0), `MISMATCH`, `RUN-HANG` + or `BUILD-HANG`. A failing seed keeps `t.c`, `t.expected` and each failing + config's output under `build/clang-oracle-fuzz/`. An output line is a + call number and the returned value, or 0x4000 plus the call number and a + checksum of every global after that call. +- `--reduce` shrinks a failing seed to a small program that still fails the + same way and is still accepted by the host checks, written to + `build/clang-oracle-fuzz/-reduce-/reduced.c`. It refuses + edits that would turn the failure into a known one. Reduce a hang with a + short `--timeout`. + +A seed names a program only together with the generator version and +`--tests`, `--size`, `--depth` and `--off`; keep the reduced program, not the +seed. + +## `run-known-bugs.py` + +Checks the reproducers parked in `tests/known-bugs/`, one per open entry in +`compiler-bugs.md`. For each it proves the baseline with the host compiler +(same output, clean under ASan and UBSan, no unsequenced modification), then +builds and runs the program with dcc with and without `dccpeep` and reports +`fails` with the reason, or `FIXED`. See `tests/known-bugs/README.md` for what +to do with a fixed one. + +```sh +python3 scripts/run-known-bugs.py +python3 scripts/run-known-bugs.py bug003 --dcc path/to/another/dcc +python3 scripts/run-known-bugs.py --update bug022 # write a new baseline from clang +``` + ## `mir-migration-census.py` Measures generated MIR selection across `tests/*.c` and compares two snapshots diff --git a/scripts/clang-oracle-fuzz.py b/scripts/clang-oracle-fuzz.py new file mode 100644 index 000000000..c0a1bc1e5 --- /dev/null +++ b/scripts/clang-oracle-fuzz.py @@ -0,0 +1,779 @@ +#!/usr/bin/env python3 +"""Differential fuzzer for dcc whose expected output comes from the host C +compiler. + +Each seed generates one C program (scripts/clang_oracle_gen.py): helper and +test functions over fixed-width integers, arrays, structs, bit-fields and +pointers, with loops, switches, nested-block locals, gotos, calls, recursion +and calls through function pointers. The same source is compiled by the host +compiler and by dcc, and the two outputs are compared. + +A program is used only when a host build with the checking layer, ASan and +UBSan reports nothing that is undefined or that would differ with dcc's +16-bit int; tripped operations are repaired and the program is checked again. +So a mismatch is a dcc defect, not a porting difference. + + clang-oracle-fuzz.py --seeds 1-200 fuzz, production configs + clang-oracle-fuzz.py --seeds 1-200 --configs all also force every MIR candidate + clang-oracle-fuzz.py --reduce 17 --config nopeep shrink seed 17's failure + clang-oracle-fuzz.py --generate 17 prog.c only write seed 17's program + clang-oracle-fuzz.py --list-configs + +A failing seed keeps build/clang-oracle-fuzz// with t.c, t.expected and +each failing config's output. An output line is a call number and the returned +value, or 0x4000 + the call number and a checksum of every global after that +call. A seed names a program only together with --tests, --size, --depth and +--off; a reduced program is self-contained. +""" +from __future__ import annotations + +import argparse +import concurrent.futures +import copy +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import clang_oracle_gen as gen # noqa: E402 + +REPO = Path(__file__).resolve().parent.parent +HOST_FLAGS = ['-std=gnu99', '-w', '-fsigned-char', '-fno-common'] +SANITIZE = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all'] +CANDIDATES = ['homed', 'homed-lazy', 'hybrid', 'regional', 'spilled-baseline', 'spilled-all', + 'spilled-all-no-prepack', 'spilled-address-remat', 'spilled-boolean-phi-branch', + 'spilled-boolean-phi-branch-no-prepack', 'spilled-global-argument', + 'spilled-phi-slot', 'spilled-promoted-local-slot', 'spilled-rhs-forward', + 'spilled-stable-local', 'spilled-stable-pointer-argument', + 'spilled-stack-argument', 'spilled-store-address', 'spilled-wide-binary-lhs'] +DIAGNOSTIC_ENV = ('DCC_MIR_NO_EXACT', 'DCC_MIR_SELECT_CANDIDATE', 'DCC_MIR_PREFER_REGIONAL', + 'DCC_MIR_SELECT_FUNCTION', 'DCC_FORCE_STACK_CHECK') + + +class Config: + def __init__(self, name, env, peep, stack_check, production): + self.name, self.env, self.peep, self.stack_check = name, env, peep, stack_check + self.production = production + + +def all_configs(): + out = [] + for peep in (False, True): + tag = 'peep' if peep else 'nopeep' + out.append(Config(tag, {}, peep, True, True)) + out.append(Config(tag + '-nosc', {}, peep, False, True)) + for peep in (False, True): + tag = 'peep' if peep else 'nopeep' + out.append(Config('noexact-' + tag, {'DCC_MIR_NO_EXACT': '1'}, peep, True, False)) + for cand in CANDIDATES: + env = {'DCC_MIR_NO_EXACT': '1'} + if cand == 'regional': + env['DCC_MIR_PREFER_REGIONAL'] = '1' + else: + env['DCC_MIR_SELECT_CANDIDATE'] = cand + out.append(Config('%s-%s' % (cand, tag), env, peep, True, False)) + return out + + +CONFIGS = {c.name: c for c in all_configs()} + + +def select_configs(text): + if text == 'production': + return [c for c in CONFIGS.values() if c.production] + if text == 'default': + return [c for c in CONFIGS.values() if c.production or c.name.startswith('noexact-')] + if text == 'all': + return list(CONFIGS.values()) + return [CONFIGS[name] for name in text.split(',')] + + +# ------------------------------------------------------------- host oracle + +def host_build(source, exe, flags, timeout=120): + r = subprocess.run(['clang', *HOST_FLAGS, *flags, str(source), '-o', str(exe)], + capture_output=True, text=True, timeout=timeout) + return r.returncode == 0, r.stderr + + +def host_exec(exe, timeout): + try: + r = subprocess.run([str(exe)], capture_output=True, text=True, timeout=timeout, + stdin=subprocess.DEVNULL) + except subprocess.TimeoutExpired: + return None, '', 'timeout' + return r.returncode, r.stdout, r.stderr + + +def checked_run(prog, work, timeout, sanitize=True): + """(status, detail, output) of the checked build: ok, trips, or invalid.""" + src = work / 'check.c' + src.write_text(gen.render(prog, checked=True)) + flags = ['-O1', *SANITIZE] if sanitize else ['-O0'] + ok, err = host_build(src, work / 'check.host', flags) + if not ok: + return 'invalid', 'checked build does not compile: ' + err[:400], '' + code, out, err = host_exec(work / 'check.host', timeout) + if code is None: + return 'invalid', 'checked build timed out', '' + if code != 0: + return 'invalid', 'checked build failed: ' + err.strip()[-400:], '' + trips = [int(x) for x in re.findall(r'TRIP (\d+)', err)] + if trips: + return 'trips', trips, out + return 'ok', None, out + + +def host_oracle(prog, work, repair=True, strict=True, timeout=10, rounds=40): + """Repair prog until it is defined, then return (expected output, None), + or (None, reason) when it cannot be used. + + strict also requires -O0 and a sanitized build of the natural source to + agree with the -O2 build, and sanitizes the checked build; the checked + build must always agree. The reducer validates candidates without strict + and its result with it.""" + work.mkdir(parents=True, exist_ok=True) + for _ in range(rounds): + status, detail, checked_out = checked_run(prog, work, timeout, sanitize=strict) + if status == 'ok': + break + if status == 'invalid': + return None, detail + if not repair: + return None, 'trips' + left = gen.repair(prog, detail) + if left: + return None, 'unrepairable trips %s' % sorted(left) + else: + return None, 'still tripping after %d repairs' % rounds + src = work / 't.c' + src.write_text(gen.render(prog, title=', seed %s' % prog.seed)) + # clang proves at compile time what no run can: an object modified twice, + # or modified and read, with no sequence point between; and a value + # returning function that can fall off its end + lint = subprocess.run(['clang', '-std=gnu99', '-fsigned-char', '-fsyntax-only', + '-Wno-everything', '-Werror=unsequenced', '-Werror=return-type', + str(src)], capture_output=True, text=True) + if lint.returncode != 0: + return None, 'not well defined: ' + lint.stderr.strip().splitlines()[0][-160:] + builds = [('O2', ['-O2'])] + if strict: + builds += [('O0', ['-O0']), ('san', ['-O1', *SANITIZE])] + outputs = [] + for tag, flags in builds: + ok, err = host_build(src, work / ('t.%s.host' % tag), flags) + if not ok: + return None, 'natural build (%s) does not compile: %s' % (tag, err[:400]) + code, out, err = host_exec(work / ('t.%s.host' % tag), timeout) + if code != 0: + return None, 'natural build (%s) failed: %s' % (tag, (err or 'timeout')[-300:]) + outputs.append(out) + if any(o != outputs[0] for o in outputs) or checked_out != outputs[0]: + return None, 'host builds disagree' + (work / 't.expected').write_text(outputs[0]) + return outputs[0], None + + +# ----------------------------------------------------------------- dcc side + +def dcc_run(source, build, cfg, args): + """(kind, detail, output) of building and running source with dcc. + kind: ok, COMPILE, BUILD, BUILD-HANG, VERIFY or RUN-HANG.""" + shutil.rmtree(build, ignore_errors=True) + env = dict(os.environ) + for key in DIAGNOSTIC_ENV: + env.pop(key, None) + env.update(cfg.env) + cmd = [str(REPO / 'dccmake'), 'dcc-input=%s' % source, 'dcc-output=T', + 'dcc-build-dir=%s' % build, 'dcc-peep=%s' % ('true' if cfg.peep else 'false'), + 'dcc-stack-check=%s' % ('true' if cfg.stack_check else 'false'), + 'dcc-stack-bytes=%d' % args.stack, 'dcc-floatio=true', 'dcc-flongio=true', + 'ntvcm-tool=%s' % args.emulator] + try: + r = subprocess.run(cmd, cwd=REPO, env=env, stdin=subprocess.DEVNULL, + capture_output=True, text=True, timeout=args.build_timeout) + except subprocess.TimeoutExpired: + return 'BUILD-HANG', 'build ran past %d s' % args.build_timeout, '' + log = r.stdout + r.stderr + if r.returncode != 0: + lines = [ln for ln in log.splitlines() if not ln.startswith('+')] + errors = [ln for ln in lines if 'error' in ln.lower() or 'fatal' in ln.lower() + or 'MIR' in ln] + first = (errors or lines or ['build failed'])[0].strip() + first = first.replace(str(source) + ':', '') + if 'error DCC' in first or 'dcc: fatal' in first or 'MIR' in first: + return 'COMPILE', first[:200], log + return 'BUILD', first[:200], log + verify = re.search(r'^; MIR \S+: (instruction \d+ .*)$', log, re.M) + if verify: + # dcc reported invalid MIR and still produced a program + return 'VERIFY', re.sub(r'\d+', 'N', verify.group(1))[:120], log + try: + run = subprocess.run([args.emulator, '-s:0', 'T.COM'], cwd=build, + stdin=subprocess.DEVNULL, capture_output=True, timeout=args.timeout) + except subprocess.TimeoutExpired: + return 'RUN-HANG', 'ran past %d s' % args.timeout, '' + return 'ok', '', run.stdout.decode('latin-1').replace('\r', '') + + +def judge(kind, detail, output, expected): + if kind != 'ok': + return kind, detail + if output == expected: + return 'ok', '' + if 'stack overflow' in output: + return 'STACK', 'stack overflow' + want, got = expected.splitlines(), output.splitlines() + wrong = [w.split()[0] for w, g in zip(want, got) if w != g] + if len(got) != len(want): + return 'MISMATCH', '%d of %d lines printed, first wrong %s' % ( + len(got), len(want), wrong[0] if wrong else 'none') + return 'MISMATCH', 'lines %s' % ' '.join(wrong[:6]) + + +def signature(kind, detail): + """What a reduction must preserve.""" + if kind in ('COMPILE', 'BUILD', 'VERIFY'): + m = re.search(r'(DCC-[A-Z]\d+: .*?)(?: near |$)', detail) + if m: + return kind, m.group(1) + return kind, re.sub(r'\d+', 'N', re.sub(r'function \w+', 'function F', detail))[-70:] + return kind, '' + + +# --------------------------------------------------------------------- fuzz + +def make_program(seed, args): + return gen.generate(seed, tests=args.tests, size=args.size, depth=args.depth, + off=args.off) + + +def run_seed(seed, args, configs): + work = args.work / str(seed) + shutil.rmtree(work, ignore_errors=True) + work.mkdir(parents=True) + try: + prog = make_program(seed, args) + expected, reason = host_oracle(prog, work) + except Exception as exc: # a generator defect must not stop the campaign + return seed, 'GENERATOR', {'-': ('GENERATOR', repr(exc)[:200])} + if expected is None: + if not args.keep: + shutil.rmtree(work, ignore_errors=True) + return seed, 'INVALID', {'-': ('INVALID', reason)} + failures = {} + for cfg in configs: + build = work / cfg.name + kind, detail, output = dcc_run(work / 't.c', build, cfg, args) + kind, detail = judge(kind, detail, output, expected) + shutil.rmtree(build, ignore_errors=True) + if kind != 'ok': + failures[cfg.name] = (kind, detail) + if output: + (work / ('out-%s.txt' % cfg.name)).write_text(output) + if not failures and not args.keep: + shutil.rmtree(work, ignore_errors=True) + return seed, 'FAIL' if failures else 'ok', failures + + +def summarize(failures): + """Group a seed's failing configs by (kind, detail).""" + groups = {} + for name, (kind, detail) in failures.items(): + groups.setdefault((kind, detail), []).append(name) + parts = [] + for (kind, detail), names in sorted(groups.items()): + shown = ','.join(names[:4]) + (',+%d' % (len(names) - 4) if len(names) > 4 else '') + parts.append('%s [%s] %s' % (kind, shown, detail)) + return '; '.join(parts) + + +def fuzz(args): + configs = select_configs(args.configs) + seeds = parse_seeds(args.seeds) + counts = {} + failing = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool: + for seed, status, failures in pool.map(lambda s: run_seed(s, args, configs), seeds): + if status in ('INVALID', 'GENERATOR'): + counts[status] = counts.get(status, 0) + 1 + if args.verbose or status == 'GENERATOR': + print('seed %d: %s %s' % (seed, status, failures['-'][1])) + continue + real = {k: v for k, v in failures.items() if v[0] != 'STACK'} + if not real: + status = 'stack-only' if failures else 'ok' + counts[status] = counts.get(status, 0) + 1 + continue + counts['FAIL'] = counts.get('FAIL', 0) + 1 + failing.append(seed) + print('seed %d: %s' % (seed, summarize(real))) + sys.stdout.flush() + print('%d programs: %s' % (len(seeds), ', '.join('%s %d' % kv for kv in sorted(counts.items())))) + return 1 if failing else 0 + + +# ------------------------------------------------------------------- reduce + +def returns_value(prog, block): + """block is the body of a function that returns a value.""" + return any(f.body is block and f.ret is not None for f in prog.funcs) + + +def referenced(prog): + used = set() + for slot in gen.expr_slots(prog): + e = gen.slot_get(slot) + if e.op in ('var', 'idx', 'pidx', 'aend', 'addrof'): + used.add(e.a) + elif e.op == 'addr': + used.add(e.a[0]) + for b in gen.all_blocks(prog): + for s in b.stmts: + if s.op == 'for' and s.a is not None: + used.add(s.a) + for f in prog.funcs: + for args in f.inputs: + for a in args: + if a.op in ('var', 'idx', 'addrof'): + used.add(a.a) + elif a.op == 'addr': + used.add(a.a[0]) + return used + + +def parented_slots(prog): + """[(slot, parent expression or None, child index)] in a stable order. + Loop headers are left out: simplifying one usually makes the loop endless.""" + roots = [] + + def block(b): + for d in b.decls: + if isinstance(d.init, gen.E): + roots.append((d, 'init')) + for s in b.stmts: + if s.op not in ('for', 'while', 'do'): + roots.extend((s.e, i) for i, e in enumerate(s.e) if e is not None) + for sub in s.b: + if sub is not None: + block(sub) + + for f in prog.funcs: + block(f.body) + out = [] + stack = [(slot, None, 0) for slot in reversed(roots)] + while stack: + slot, parent, index = stack.pop() + out.append((slot, parent, index)) + e = gen.slot_get(slot) + for i in range(len(e.k) - 1, -1, -1): + stack.append(((e.k, i), e, i)) + return out + + +def is_lvalue_slot(parent, index): + if parent is None: + return False + if parent.op in ('asg', 'incdec') and index == 0: + return True + return parent.op in ('fld', 'deref') and index == 0 + + +class Reducer: + """Shrinks a failing program. Every pass is a list of candidate edits in + program order; candidates are tried in parallel and the first that keeps + the failure is applied.""" + + def __init__(self, prog, cfg, want, args, work): + self.prog, self.cfg, self.want, self.args, self.work = prog, cfg, want, args, work + self.tests = 0 + self.pool = concurrent.futures.ThreadPoolExecutor(max_workers=args.reduce_jobs) + # an edit may not turn this failure into one that is already logged + self.shapes = gen.known_shapes(prog) + + def still_fails(self, cand, lane=0): + """cand is defined on the host and dcc still fails the same way.""" + self.tests += 1 + work = self.work / ('try%d' % lane) + work.mkdir(parents=True, exist_ok=True) + src = work / 't.c' + try: + src.write_text(gen.render(cand)) + except Exception: + return False + expected = None + if self.want[0] not in ('COMPILE', 'BUILD', 'BUILD-HANG', 'VERIFY'): + ok, _ = host_build(src, work / 't.host', ['-O1'], timeout=60) + if not ok: + return False + code, expected, _ = host_exec(work / 't.host', 5) + if code != 0: + return False + kind, detail, output = dcc_run(src, work / 'dcc', self.cfg, self.args) + if kind == 'ok' and expected is None: + return False + kind, detail = judge(kind, detail, output, expected) + if signature(kind, detail) != self.want: + return False + checked, _ = host_oracle(copy.deepcopy(cand), work / 'host', repair=False, + strict=False, timeout=5) + return checked is not None and (expected is None or checked == expected) + + def sweep(self, candidates): + """candidates(prog) lists edits in program order; each returns False to + decline. Edits that survive alone are applied together, last first so + earlier positions stay valid, when the failure survives that too.""" + changed = False + pos = 0 + while True: + cands = candidates(self.prog) + if pos >= len(cands): + return changed + batch = cands[pos:pos + self.args.reduce_jobs] + + def apply(edits): + cand = copy.deepcopy(self.prog) + try: + for mutate in edits: + if mutate(cand) is False: + return None + if gen.known_shapes(cand) - self.shapes: + return None + except (IndexError, AttributeError, ValueError, KeyError, TypeError, + StopIteration): + return None + return cand + + def evaluate(item): + lane, mutate = item + cand = apply([mutate]) + return cand if cand is not None and self.still_fails(cand, lane) else None + + results = list(self.pool.map(evaluate, enumerate(batch))) + hits = [k for k, c in enumerate(results) if c is not None] + if not hits: + pos += len(batch) + continue + changed = True + # apply as many of the surviving edits together as still fail + n = len(hits) + while n > 1: + together = apply([batch[k] for k in reversed(hits[:n])]) + if together is not None and self.still_fails(together): + self.prog = together + break + n //= 2 + else: + self.prog = results[hits[0]] + pos += hits[0] + + # ---- candidate lists + + @staticmethod + def only_tests(prog): + names = [f.name for f in prog.tests] + if len(names) < 2: + return [] + + def only(keep): + def mutate(p): + p.funcs = [f for f in p.funcs if f.kind != 'test' or f.name == keep] + return mutate + return [only(n) for n in names] + + @staticmethod + def fewer_tests(prog): + out = [] + tests = prog.tests + if len(tests) > 1: + for f in tests: + def drop(p, name=f.name): + p.funcs = [g for g in p.funcs if g.name != name] + out.append(drop) + for f in tests: + if len(f.inputs) > 1: + for i in range(len(f.inputs)): + def drop_input(p, name=f.name, i=i): + g = next(x for x in p.funcs if x.name == name) + if len(g.inputs) < 2: + return False + del g.inputs[i] + out.append(drop_input) + return out + + @staticmethod + def fewer_statements(prog): + out = [] + for bi, b in enumerate(gen.all_blocks(prog)): + keeps = sum(1 for s in b.stmts if s.op == 'ret' and returns_value(prog, b)) + if len(b.stmts) - keeps > 1: + def clear(p, bi=bi): + blk = gen.all_blocks(p)[bi] + blk.stmts = [s for s in blk.stmts if s.op == 'ret' and returns_value(p, blk)] + out.append(clear) + for si, s in enumerate(b.stmts): + if s.op == 'ret' and returns_value(prog, b): + continue # a non-void function must still end in its return + + def drop(p, bi=bi, si=si): + del gen.all_blocks(p)[bi].stmts[si] + out.append(drop) + return out + + @staticmethod + def flatter(prog): + """Replace a compound statement by one of its blocks. A block that + jumps is only lifted to the end of its parent, so the edit never + leaves unreachable code behind it.""" + out = [] + for bi, b in enumerate(gen.all_blocks(prog)): + for si, s in enumerate(b.stmts): + for k, sub in enumerate(s.b): + if sub is None: + continue + if s.op == 'block' and sub.decls: + continue + jumps = any(x.op in ('ret', 'break', 'continue') for x in sub.stmts) + if jumps and si != len(b.stmts) - 1: + continue + + def lift(p, bi=bi, si=si, k=k): + blk = gen.all_blocks(p)[bi] + inner = blk.stmts[si].b[k] + if inner.decls: + blk.stmts[si] = gen.S('block', b=[inner]) + else: + blk.stmts[si:si + 1] = inner.stmts + out.append(lift) + return out + + @staticmethod + def simpler_expressions(prog): + out = [] + for i, (slot, parent, index) in enumerate(parented_slots(prog)): + node = gen.slot_get(slot) + t = gen.ty(node) + if not gen.is_int(t) or is_lvalue_slot(parent, index) or node.op == 'const': + continue + options = [] + # a cast is only ever dropped ('plain'): replacing it by a cast of + # its operand would be the same program again + if node.op not in ('idx', 'fld', 'pidx', 'deref', 'addr', 'cast'): + for k, kid in enumerate(node.k): + if node.op in ('asg', 'incdec') and k == 0: + continue + if gen.is_int(gen.ty(kid)): + options.append(('kid', k)) + options += [('const', 1), ('const', 0)] + if node.op == 'cast' or (node.op == 'asg' and node.a != '='): + options.append(('plain', 0)) + for what, k in options: + def mutate(p, i=i, what=what, k=k): + slot = parented_slots(p)[i][0] + node = gen.slot_get(slot) + t = gen.ty(node) + if what == 'kid': + new = node.k[k] + if gen.ty(new) is not t: + new = gen.E('cast', [new], t=t) + elif what == 'const': + ct = t if t.bits >= 16 else (gen.I16 if t.signed else gen.U16) + new = gen.mk_const(k, ct) + elif node.op == 'cast': + new = node.k[0] + else: + node.a = '=' + return None + gen.slot_set(slot, new) + out.append(mutate) + return out + + @staticmethod + def without_unused(prog): + def mutate(p): + used = referenced(p) + n = 0 + for b in gen.all_blocks(p): + keep = [d for d in b.decls if d in used] + n += len(b.decls) - len(keep) + b.decls = keep + keep = [g for g in p.globals if g in used] + n += len(p.globals) - len(keep) + p.globals = keep + called = set() + for slot in gen.expr_slots(p): + e = gen.slot_get(slot) + if e.op == 'call': + called.add(e.a) + keep = [f for f in p.funcs if f.kind == 'test' or f in called] + n += len(p.funcs) - len(keep) + p.funcs = keep + return None if n else False + return [mutate] + + @staticmethod + def fewer_params(prog): + out = [] + used = referenced(prog) + for f in prog.funcs: + if f.kind == 'rec': + continue + for pi, param in enumerate(f.params): + if param in used: + continue + + def drop(p, name=f.name, pi=pi): + g = next(x for x in p.funcs if x.name == name) + del g.params[pi] + for args in g.inputs: + del args[pi] + for slot in gen.expr_slots(p): + e = gen.slot_get(slot) + if e.op == 'call' and e.a is g: + del e.k[pi] + out.append(drop) + return out + + @staticmethod + def plainer_decls(prog): + """Initialize a local with a constant instead of an expression.""" + out = [] + for bi, b in enumerate(gen.all_blocks(prog)): + for di, d in enumerate(b.decls): + if isinstance(d.init, gen.E) and d.init.op != 'const' and gen.is_int(d.t): + def plain(p, bi=bi, di=di): + dd = gen.all_blocks(p)[bi].decls[di] + ct = dd.t if dd.t.bits >= 16 else (gen.I16 if dd.t.signed else gen.U16) + dd.init = gen.mk_const(1, ct) + out.append(plain) + return out + + def run(self): + if not self.still_fails(copy.deepcopy(self.prog)): + return False + passes = [self.only_tests, self.fewer_tests, self.fewer_statements, self.flatter, + self.without_unused, self.fewer_params, self.plainer_decls, + self.simpler_expressions, self.without_unused] + for round_ in range(8): + changed = False + for p in passes: + changed |= self.sweep(p) + if self.args.verbose: + print(' round %d %s: %d lines, %d tests run' % ( + round_, p.__name__, len(gen.render(self.prog).splitlines()), self.tests)) + sys.stdout.flush() + if not changed: + break + return True + + +def reduce_seed(seed, cfg_name, args): + cfg = CONFIGS[cfg_name] + work = args.work / ('%s-reduce-%s' % (seed, cfg_name)) + shutil.rmtree(work, ignore_errors=True) + work.mkdir(parents=True) + prog = make_program(seed, args) + expected, reason = host_oracle(prog, work / 'orig') + if expected is None: + return seed, cfg_name, None, 'program is not usable: %s' % reason + kind, detail, output = dcc_run(work / 'orig' / 't.c', work / 'orig' / 'dcc', cfg, args) + kind, detail = judge(kind, detail, output, expected) + if kind == 'ok': + return seed, cfg_name, None, 'does not fail under %s' % cfg_name + reducer = Reducer(prog, cfg, signature(kind, detail), args, work) + if not reducer.run(): + return seed, cfg_name, None, 'failure is not stable' + final = copy.deepcopy(reducer.prog) + expected, reason = host_oracle(final, work / 'final', repair=False) + if expected is None: + return seed, cfg_name, None, 'reduced program is not usable: %s' % reason + out = work / 'reduced.c' + out.write_text(gen.render(final, title=', seed %d reduced under %s' % (seed, cfg_name))) + (work / 'reduced.expected').write_text(expected) + kind2, detail2, output = dcc_run(out, work / 'final' / 'dcc', cfg, args) + (work / 'reduced.actual').write_text(output) + kind2, detail2 = judge(kind2, detail2, output, expected) + for sub in work.glob('try*'): + shutil.rmtree(sub, ignore_errors=True) + known = ' [already has: %s]' % ','.join(sorted(reducer.shapes)) if reducer.shapes else '' + return seed, cfg_name, out, '%s %s (%d lines, %d tests)%s' % ( + kind2, detail2, len(out.read_text().splitlines()), reducer.tests, known) + + +def parse_seeds(text): + seeds = [] + for part in text.split(','): + low, _, high = part.partition('-') + seeds.extend(range(int(low), int(high or low) + 1)) + return seeds + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--seeds', default='1-100', help='seeds to run, e.g. 1-100,250') + parser.add_argument('--configs', default='default', + help='production, default (production + no exact schedules), all, ' + 'or a comma-separated list of config names') + parser.add_argument('--tests', type=int, default=6, help='test functions per program') + parser.add_argument('--size', type=int, default=10, help='statements per test function') + parser.add_argument('--depth', type=int, default=3, help='statement nesting depth') + parser.add_argument('--off', default='', + help='comma-separated generator features to leave out; `known` ' + 'leaves out every form that hits a logged dcc bug. Features: %s' + % ', '.join(sorted(gen.FEATURES))) + parser.add_argument('--jobs', type=int, default=os.cpu_count() or 1) + parser.add_argument('--reduce-jobs', type=int, default=16, + help='candidate edits tried at once per reduction') + parser.add_argument('--work', type=Path, default=REPO / 'build' / 'clang-oracle-fuzz') + parser.add_argument('--emulator', default='ntvcm') + parser.add_argument('--stack', type=int, default=4096, help='target stack bytes') + parser.add_argument('--timeout', type=int, default=20, help='seconds to run a program') + parser.add_argument('--build-timeout', type=int, default=120) + parser.add_argument('--keep', action='store_true', help='keep passing seeds too') + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--list-configs', action='store_true') + parser.add_argument('--generate', nargs=2, metavar=('SEED', 'C'), + help='write one repaired program and FILE.expected beside it') + parser.add_argument('--reduce', help='seeds whose failure to reduce') + parser.add_argument('--config', default='nopeep', help='config to reduce under') + args = parser.parse_args() + args.off = frozenset(x for x in args.off.split(',') if x) + if 'known' in args.off: + args.off = (args.off - {'known'}) | frozenset(gen.KNOWN_BUGS) + unknown = args.off - gen.FEATURES + if unknown: + parser.error('unknown feature: %s' % ', '.join(sorted(unknown))) + if args.list_configs: + for c in CONFIGS.values(): + print('%-48s %s' % (c.name, ' '.join('%s=%s' % kv for kv in c.env.items()))) + return 0 + if args.generate: + seed, path = int(args.generate[0]), Path(args.generate[1]) + work = args.work / ('%d-generate' % seed) + prog = make_program(seed, args) + expected, reason = host_oracle(prog, work) + if expected is None: + print('seed %d is not usable: %s' % (seed, reason)) + return 1 + path.write_text(gen.render(prog, title=', seed %d' % seed)) + path.with_suffix('.expected').write_text(expected) + shutil.rmtree(work, ignore_errors=True) + return 0 + if args.reduce: + seeds = parse_seeds(args.reduce) + outer = max(1, args.jobs // args.reduce_jobs) + with concurrent.futures.ThreadPoolExecutor(max_workers=outer) as pool: + for seed, cfg, path, note in pool.map( + lambda s: reduce_seed(s, args.config, args), seeds): + print('seed %d %s: %s%s' % (seed, cfg, note, ' -> %s' % path if path else '')) + sys.stdout.flush() + return 0 + return fuzz(args) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py new file mode 100644 index 000000000..505397584 --- /dev/null +++ b/scripts/clang_oracle_gen.py @@ -0,0 +1,2096 @@ +#!/usr/bin/env python3 +"""Program model, generator and renderers for clang-oracle-fuzz.py. + +A generated program is an AST (structs, globals, helper functions, test +functions, a fixed harness) that is rendered two ways: + + natural the C source that both clang and dcc compile. It uses fixed-width + types only, so `int` is never spelled and host/target differ only + in the width integer promotion uses (32 bits against 16). + checked the same program for the host only, with every operation that + could be undefined, or could differ at 16-bit `int`, routed + through a ck_* helper that records the node's id when it trips. + +The renderer, not the generator, owns portability: for any well-typed tree it +inserts the casts and masks that make the natural source mean the same thing +with 16-bit and 32-bit `int`: + + * a result whose dcc type is `unsigned int` is cast to uint16_t, because the + host computes it exactly in `int` and does not wrap; + * `/`, `%` and comparisons whose dcc type is `unsigned int` get their signed + operand cast to uint16_t, because the host would compare signed values; + * array indexes and shift counts are masked into range; + * constants are spelled with casts, never with `L`/`U` suffixes, because + `long` is 64 bits on the host. + +What the renderer cannot make portable is undefined behaviour at 16 bits that +is defined at 32 (signed overflow of `int`, shifting into the sign bit) and +ordinary undefined behaviour (division by zero, 32-bit signed overflow). The +checked rendering detects those at run time; the driver then repairs the +tripped nodes or rejects the program. A program is used only when its checked +build reports no trips, so its natural source is defined for both compilers. +""" +from __future__ import annotations + +import random + +CMP = ('<', '>', '<=', '>=', '==', '!=') +LOGIC = ('&&', '||') +ARITH = ('+', '-', '*') +BITS = ('&', '|', '^') +DIV = ('/', '%') +SHIFT = ('<<', '>>') + + +class IntType: + __slots__ = ('name', 'bits', 'signed', 'cnames') + + def __init__(self, name, bits, signed, cnames): + self.name, self.bits, self.signed, self.cnames = name, bits, signed, cnames + + def __deepcopy__(self, memo): + return self + + def __copy__(self): + return self + + @property + def cname(self): + return self.cnames[0] + + @property + def lo(self): + return -(1 << (self.bits - 1)) if self.signed else 0 + + @property + def hi(self): + return (1 << (self.bits - 1)) - 1 if self.signed else (1 << self.bits) - 1 + + def wrap(self, v): + v &= (1 << self.bits) - 1 + if self.signed and v >> (self.bits - 1): + v -= 1 << self.bits + return v + + def __repr__(self): + return self.name + + +I8 = IntType('i8', 8, True, ('int8_t', 'signed char', 'char')) +U8 = IntType('u8', 8, False, ('uint8_t', 'unsigned char')) +I16 = IntType('i16', 16, True, ('int16_t', 'short')) +U16 = IntType('u16', 16, False, ('uint16_t', 'unsigned short')) +I32 = IntType('i32', 32, True, ('int32_t',)) +U32 = IntType('u32', 32, False, ('uint32_t',)) +INT_TYPES = (I8, U8, I16, U16, I32, U32) +UNSIGNED_OF = {I8: U8, I16: U16, I32: U32, U8: U8, U16: U16, U32: U32} + + +class PtrType: + __slots__ = ('to',) + + def __init__(self, to): + self.to = to + + +class Field: + __slots__ = ('name', 't', 'dims', 'bf') + + def __init__(self, name, t, dims=(), bf=0): + self.name, self.t, self.dims, self.bf = name, t, dims, bf + + +class StructType: + __slots__ = ('name', 'fields') + + def __init__(self, name, fields): + self.name, self.fields = name, fields + + @property + def cname(self): + return 'struct ' + self.name + + +def is_int(t): + return isinstance(t, IntType) + + +def promote(t): + """dcc's integer promotion: everything narrower than int becomes int.""" + if t.bits < 16: + return I16 + return t + + +def common(a, b): + """dcc's usual arithmetic conversions (int 16 bits, long 32 bits).""" + a, b = promote(a), promote(b) + if a.bits == 32 or b.bits == 32: + return U32 if U32 in (a, b) else I32 + return U16 if U16 in (a, b) else I16 + + +class Decl: + """A variable. storage: global, param, local (function top), block.""" + __slots__ = ('name', 't', 'dims', 'storage', 'init', 'cname', 'addr_taken', + 'valid', 'tgt_local', 'rng', 'static') + + def __init__(self, name, t, storage, dims=(), init=None, cname=None): + self.name, self.t, self.storage, self.dims, self.init = name, t, storage, dims, init + self.cname = cname or (t.cname if not isinstance(t, PtrType) else None) + self.addr_taken = False + self.valid = 1 # pointers: elements valid from the pointee + self.tgt_local = False # pointers: pointee is local to the function + self.rng = None # loop counters: largest value seen in the body + self.static = False + + +class E: + """Expression node. + + op k (children) a + const - value (t stored) + var - Decl + idx one index per dim array Decl + fld base [, index] Field + deref pointer expression - + pidx index pointer Decl + addr index (array Decl, elements needed) + aend - array Decl one past the end + addrof - Decl + un operand operator + bin left, right operator + cast operand - (t stored) + cond test, then, else - + call arguments Func + asg lvalue, value operator (= += ...) + incdec lvalue (operator, is_prefix) + """ + __slots__ = ('op', 't', 'k', 'a', 'id') + + def __init__(self, op, k=(), a=None, t=None, id=0): + self.op, self.k, self.a, self.t, self.id = op, list(k), a, t, id + + +class S: + """Statement node. + + op e (expressions) b (blocks) a + expr expression + if test then [, else] + for init, test, step body Decl for a C99 init decl + while test body + do test body + switch control one per case [(labels|None, has_break)] + break / continue + ret [value] + block body + goto test label + label label + """ + __slots__ = ('op', 'e', 'b', 'a') + + def __init__(self, op, e=(), b=(), a=None): + self.op, self.e, self.b, self.a = op, list(e), list(b), a + + +class Block: + __slots__ = ('decls', 'stmts') + + def __init__(self): + self.decls, self.stmts = [], [] + + +class Func: + __slots__ = ('name', 'ret', 'params', 'body', 'kind', 'static', 'inputs', 'via_ptr') + + def __init__(self, name, ret, params, kind): + self.name, self.ret, self.params, self.kind = name, ret, params, kind + self.body = Block() + self.static = True + self.inputs = [] + self.via_ptr = False + + +class Program: + __slots__ = ('seed', 'structs', 'globals', 'funcs', 'nid', 'off') + + def __init__(self, seed, off=frozenset()): + self.seed = seed + self.off = frozenset(off) # generator features left out; the renderer honours some + self.structs, self.globals, self.funcs = [], [], [] + self.nid = 1 + + @property + def tests(self): + return [f for f in self.funcs if f.kind == 'test'] + + +def ty(e): + """dcc type of an expression.""" + op = e.op + if op in ('const', 'cast'): + return e.t + if op in ('var', 'idx'): + return e.a.t + if op == 'fld': + return I16 if e.a.bf else e.a.t + if op == 'deref': + return ty(e.k[0]).to + if op == 'pidx': + return e.a.t.to + if op == 'addr': + return PtrType(e.a[0].t) + if op in ('aend', 'addrof'): + return PtrType(e.a.t) + if op == 'un': + return I16 if e.a == '!' else promote(ty(e.k[0])) + if op == 'bin': + if e.a in CMP or e.a in LOGIC: + return I16 + ta = ty(e.k[0]) + if e.a in SHIFT: + return promote(ta) + return common(ta, ty(e.k[1])) + if op == 'cond': + ta = ty(e.k[1]) + return common(ta, ty(e.k[2])) if is_int(ta) else ta + if op == 'call': + return e.a.ret + if op in ('asg', 'incdec'): + return ty(e.k[0]) + raise ValueError(op) + + +def clone(e): + """Copy an expression tree, sharing the declarations it refers to.""" + return E(e.op, [clone(k) for k in e.k], e.a, e.t, e.id) + + +def is_bitfield(e): + return e.op == 'fld' and e.a.bf + + +# ---------------------------------------------------------------- traversal + +def block_slots(block, out): + for d in block.decls: + if isinstance(d.init, E): + out.append((d, 'init')) + for s in block.stmts: + stmt_slots(s, out) + + +def stmt_slots(s, out): + if s.op == 'for' and s.a is not None and isinstance(s.a.init, E): + out.append((s.a, 'init')) + for i, e in enumerate(s.e): + if e is not None: + out.append((s.e, i)) + for b in s.b: + if b is not None: + block_slots(b, out) + + +def expr_slots(prog, funcs=None): + """Every (container, key) holding a root expression, then all sub-slots.""" + roots = [] + for f in (funcs if funcs is not None else prog.funcs): + block_slots(f.body, roots) + out = [] + stack = list(reversed(roots)) + while stack: + slot = stack.pop() + out.append(slot) + e = slot_get(slot) + for i in range(len(e.k) - 1, -1, -1): + stack.append((e.k, i)) + return out + + +def slot_get(slot): + c, k = slot + return getattr(c, k) if isinstance(k, str) else c[k] + + +def slot_set(slot, v): + c, k = slot + if isinstance(k, str): + setattr(c, k, v) + else: + c[k] = v + + +def all_blocks(prog): + out = [] + + def walk(b): + out.append(b) + for s in b.stmts: + for sub in s.b: + if sub is not None: + walk(sub) + + for f in prog.funcs: + walk(f.body) + return out + + +def init_keeps_type(init, t): + """init, as the initialiser of a local of integer type t, has exactly + that type already: a constant in t's range, or an expression of type t + that is not a cast from another type. Anything else is the BUG-016 shape, + where the local takes on the initialiser's signedness.""" + if init.op == 'const': + return t.lo <= init.a <= t.hi + if init.op == 'cast': + return ty(init.k[0]) is t + return ty(init) is t + + +def known_shapes(prog): + """The features in prog.off whose bug shape the program contains anyway. + + The generator steers round a logged bug when its feature is off, but the + reducer edits trees freely and could shrink a new failure into an old one. + It uses this to refuse an edit that introduces a shape the program did not + already have.""" + found = set() + + def stored(rhs, d): + if not is_int(d.t) or d.storage == 'global': + return + if d.t.bits <= 16 and rhs.op == 'bin' and rhs.a in LOGIC: + found.add('logicinit') + if d.storage == 'block' and d.t.bits == 32 and rhs.op == 'const' \ + and (rhs.a < 0 or (d.t is I32 and rhs.a >= 1 << 31)): + found.add('blockwideneg') + + def expr(e, root): + op = e.op + if op == 'asg': + lv, rhs = e.k + lt = ty(lv) + if is_int(lt) and e.a == '=': + if lv.op == 'var': + stored(rhs, lv.a) + elif is_int(lt): + local = lv.op == 'var' and lv.a.storage != 'global' + if lv.op != 'var' and lt.bits < 32 and ty(rhs).bits == 32: + found.add('widecassign') + if ty(rhs).bits == 32 and (is_bitfield(lv) or ( + lv.op == 'fld' and lv.a.dims and lt.bits == 16)): + found.add('widecasgbf') + if e.a == '<<=' and lt.bits == 8 and ty(rhs).bits == 8: + found.add('bytevarshl') + if e.a in ('/=', '%=') and is_bitfield(lv) and not lv.a.t.signed: + found.add('bfdivasg') + if e.a in ('/=', '%=') and lv.op == 'fld' and lt.bits == 32: + found.add('blockwidediv') + if lv.op == 'fld' and lv.a.dims and lt.bits == 8: + found.add('structbytecasg') + if lv.op == 'var' and rhs.op == 'var' and rhs.a is lv.a: + found.add('selfand') + if local and lt is I32 and e.a == '+=' and rhs.op == 'const' and rhs.a == 1: + found.add('widelocalinc') + elif op == 'incdec': + lv = e.k[0] + lt = ty(lv) + if is_int(lt) and lt.bits == 32: + if lv.op in ('idx', 'pidx') or (lv.op == 'fld' and lv.a.dims): + found.add('wideidxincdec') + if lv.op == 'var' and not e.a[1] and not root: + found.add('widepostinc') + if lv.op == 'var' and lt is I32 and lv.a.storage != 'global' and e.a[0] == '++': + found.add('widelocalinc') + elif op == 'un': + if e.a in ('-', '~') and e.k[0].op == 'un' and e.k[0].a == '!': + found.add('negnotconst') + elif op == 'call': + if e.a.via_ptr and any(is_int(p.t) and ty(a) is not p.t + for a, p in zip(e.k, e.a.params)): + found.add('ptrcallconv') + for k in e.k: + expr(k, False) + + def decl(d): + if isinstance(d.init, E): + if is_int(d.t): + stored(d.init, d) + if not init_keeps_type(d.init, d.t): + found.add('inittype') + expr(d.init, True) + + for b in all_blocks(prog): + for d in b.decls: + decl(d) + for s in b.stmts: + if s.op == 'for' and s.a is not None: + decl(s.a) + for e in s.e: + if e is not None: + expr(e, True) + return found & prog.off + + +# ------------------------------------------------------------------- repair + +def mk_const(v, t): + return E('const', a=t.wrap(v), t=t) + + +def wrap_unsigned(node): + """Rewrite a signed bin/un node in place as the same operation carried out + in the unsigned type of its width, converted back to the signed type.""" + t = ty(node) + u = UNSIGNED_OF[t] + kids = [E('cast', [k], t=u) for k in node.k] + if node.op == 'bin' and node.a in SHIFT: + kids[1] = node.k[1] + inner = E(node.op, kids, node.a, id=node.id) + node.op, node.k, node.a, node.t = 'cast', [inner], None, t + + +def safe_divisor(node): + b = node.k[1] + masked = E('bin', [b, mk_const(63, I16)], '&') + node.k[1] = E('bin', [masked, mk_const(1, I16)], '+') + + +def repair_node(node): + """Make a tripped node defined. Returns False when it cannot.""" + op = node.op + if op == 'bin': + if node.a in DIV: + if node.k[1].op == 'bin' and node.k[1].a == '+' and node.k[1].k[1].op == 'const': + node.k[1] = mk_const(3, I16) + else: + safe_divisor(node) + return True + if node.a == '*' and not ty(node).signed: + node.k[1] = E('bin', [node.k[1], mk_const(0x7f, I16)], '&') + return True + if node.a in ARITH or node.a == '<<': + if not ty(node).signed: + return False + wrap_unsigned(node) + return True + return False + if op == 'un' and node.a == '-': + wrap_unsigned(node) + return True + if op == 'asg' and node.a != '=': + lv, rhs = node.k + value = E('bin', [clone(lv), rhs], node.a[:-1], id=node.id) + if value.a in DIV: + safe_divisor(value) + elif value.a == '*' and not ty(value).signed: + value.k[1] = E('bin', [value.k[1], mk_const(0x7f, I16)], '&') + elif ty(value).signed and (value.a in ARITH or value.a == '<<'): + wrap_unsigned(value) + node.a, node.k = '=', [lv, value] + return True + if op == 'incdec': + lv = node.k[0] + t = ty(lv) + u = UNSIGNED_OF[t] + step = E('bin', [E('cast', [clone(lv)], t=u), mk_const(1, I16)], + '+' if node.a[0] == '++' else '-') + node.op, node.a, node.k = 'asg', '=', [lv, E('cast', [step], t=t)] + return True + return False + + +def repair(prog, trips): + """Repair every node whose id tripped. Returns the ids left unrepaired.""" + trips = set(trips) + left = set(trips) + for slot in expr_slots(prog): + node = slot_get(slot) + if node.id in trips and node.op != 'cast' and repair_node(node): + left.discard(node.id) + return left + + +# ---------------------------------------------------------------- rendering + +CHECK_PRELUDE = r''' +/* host-only checking layer: records operations that are undefined, or that + * would differ with a 16-bit int. */ +#include +static unsigned char ck_trip[%(nid)d + 1]; +static void ck_hit(int id) { ck_trip[id] = 1; } +static void ck_report(void) +{ + int i, any = 0; + for (i = 0; i <= %(nid)d; i++) + if (ck_trip[i]) { fprintf(stderr, "%%sTRIP %%d", any ? " " : "", i); any = 1; } + if (any) fprintf(stderr, "\n"); +} +static int16_t ck_s16(int id, int32_t v) { if (v < -32768 || v > 32767) ck_hit(id); return (int16_t)v; } +static int32_t ck_s32(int id, int64_t v) { if (v < INT32_MIN || v > INT32_MAX) ck_hit(id); return (int32_t)v; } +static int ck_cnt(int id, int64_t n, int bits) { if (n < 0 || n >= bits) ck_hit(id); return (int)(n & (bits - 1)); } +static int32_t ck_div32(int id, int32_t a, int32_t b) { if (b == 0 || (a == INT32_MIN && b == -1)) { ck_hit(id); return a; } return a / b; } +static int32_t ck_mod32(int id, int32_t a, int32_t b) { if (b == 0 || (a == INT32_MIN && b == -1)) { ck_hit(id); return 0; } return a %% b; } +static uint32_t ck_udiv32(int id, uint32_t a, uint32_t b) { if (b == 0) { ck_hit(id); return a; } return a / b; } +static uint32_t ck_umod32(int id, uint32_t a, uint32_t b) { if (b == 0) { ck_hit(id); return 0; } return a %% b; } +static int16_t ck_div16(int id, int32_t a, int32_t b) { if (b == 0 || (a == -32768 && b == -1)) { ck_hit(id); return (int16_t)a; } return ck_s16(id, a / b); } +static int16_t ck_mod16(int id, int32_t a, int32_t b) { if (b == 0 || (a == -32768 && b == -1)) { ck_hit(id); return 0; } return ck_s16(id, a %% b); } +static uint16_t ck_udiv16(int id, uint16_t a, uint16_t b) { if (b == 0) { ck_hit(id); return a; } return (uint16_t)(a / b); } +static uint16_t ck_umod16(int id, uint16_t a, uint16_t b) { if (b == 0) { ck_hit(id); return 0; } return (uint16_t)(a %% b); } +static uint16_t ck_umul16(int id, uint16_t a, uint16_t b) { uint32_t p = (uint32_t)a * b; if (p > (uint32_t)INT32_MAX) ck_hit(id); return (uint16_t)p; } +static int32_t ck_shl32(int id, int32_t a, int n) { if (a < 0 || (n && (a >> (31 - n)) != 0)) ck_hit(id); return (int32_t)((uint32_t)a << n); } +static int16_t ck_shl16(int id, int32_t a, int n) { if (a < 0 || ((int64_t)a << n) > 32767) ck_hit(id); return (int16_t)((uint32_t)a << n); } +static int16_t ck_post16(int id, int16_t *p, int d) { int16_t v = *p; *p = ck_s16(id, (int32_t)v + d); return v; } +static int32_t ck_post32(int id, int32_t *p, int d) { int32_t v = *p; *p = ck_s32(id, (int64_t)v + d); return v; } +''' + +HARNESS = r''' +static void hex4(uint16_t v) +{ + int8_t k; + for (k = 12; k >= 0; k -= 4) + putchar("0123456789abcdef"[(v >> k) & 15]); +} + +static void show(uint16_t id, uint32_t v) +{ + hex4(id); + putchar(' '); + hex4((uint16_t)(v >> 16)); + hex4((uint16_t)v); + putchar('\n'); +} + +static uint32_t ck; + +static void mix(uint32_t v) +{ + ck = ((ck << 5) | (ck >> 27)) ^ v; + ck = ck + (uint32_t)0x9e3779b1; +} +''' + + +def const_text(v, t): + if t is I16 or t.bits < 16: + if t is not I16: + return '((%s)%d)' % (t.cname, v) + if v == -32768: + return '(-32767 - 1)' + return str(v) if v >= 0 else '(%d)' % v + if t is U16: + return '((uint16_t)%d)' % v if v < 32768 else '((uint16_t)0x%x)' % v + if t is I32: + if v == -(1 << 31): + return '((int32_t)0x80000000)' + return '((int32_t)%d)' % v + if v >= 1 << 31: + return '((uint32_t)0x%x)' % v + return '((uint32_t)%d)' % v + + +def static_text(v, t): + """A constant for a static initializer.""" + if t.signed: + if v == -(1 << 31): + return '(-2147483647 - 1)' + return str(v) + return '0x%x' % v if v > 32767 else str(v) + + +def index_mask(n): + """Largest 2^k - 1 that is < n (n >= 1).""" + m = 1 + while m * 2 <= n: + m *= 2 + return m - 1 + + +class Renderer: + def __init__(self, checked=False, off=frozenset()): + self.checked = checked + self.off = off + + # ---- expressions + + def cast(self, cname, e): + """(cname)e. With `castsfx` off the value of an assignment or ++/-- + is passed through `+ 0` first, which changes nothing but keeps the + cast off the assignment itself.""" + s = self.ex(e) + if e.op in ('asg', 'incdec') and 'castsfx' in self.off and is_int(ty(e)): + s = '(%s + 0)' % s + return '((%s)%s)' % (cname, s) + + def index(self, e, n): + """An index into n elements.""" + if e.op == 'const' and 0 <= e.a < n: + return str(e.a) + if e.op == 'var' and e.a.rng is not None and e.a.rng < n and not self.checked: + return e.a.name + if e.op == 'var' and e.a.rng is not None and e.a.rng < n: + return 'ck_idx(%s, %d)' % (e.a.name, n) + return '%s & %d' % (self.ex(e), index_mask(n)) + + def u16(self, e): + return self.ex(e) if promote(ty(e)) is U16 else self.cast('uint16_t', e) + + def ex(self, e): + return getattr(self, 'ex_' + e.op)(e) + + def ex_const(self, e): + return const_text(e.a, e.t) + + def ex_var(self, e): + return e.a.name + + def ex_idx(self, e): + return e.a.name + ''.join('[%s]' % self.index(i, n) for i, n in zip(e.k, e.a.dims)) + + def ex_fld(self, e, raw=False): + base = e.k[0] + if base.op == 'deref' and base.k[0].op == 'var': + s = '%s->%s' % (base.k[0].a.name, e.a.name) + else: + s = '%s.%s' % (self.ex(base), e.a.name) + if e.a.dims: + s += '[%s]' % self.index(e.k[1], e.a.dims[0]) + if e.a.bf and not raw and 'bfpromo' in self.off: + # with `bfpromo` off a bit-field is read through a cast to the + # type it promotes to, so nothing depends on dcc promoting it + s = '((int16_t)%s)' % s + return s + + def lv(self, e): + """An lvalue: a bit-field is never read through a cast here.""" + return self.ex_fld(e, raw=True) if e.op == 'fld' else self.ex(e) + + def ex_deref(self, e): + return '(*%s)' % self.ex(e.k[0]) + + def ex_pidx(self, e): + return '%s[%s]' % (e.a.name, self.index(e.k[0], e.a.valid)) + + def ex_addr(self, e): + d, need = e.a + return '(&%s[%s])' % (d.name, self.index(e.k[0], d.dims[0] - need + 1)) + + def ex_aend(self, e): + return '(%s + %d)' % (e.a.name, e.a.dims[0]) + + def ex_addrof(self, e): + return '(&%s)' % e.a.name + + def ex_cast(self, e): + return self.cast(e.t.cname, e.k[0]) + + def ex_un(self, e): + a = self.ex(e.k[0]) + if e.a == '!': + return '(!%s)' % a + t = ty(e) + if e.a == '-' and self.checked and t.signed: + if t is I16: + return 'ck_s16(%d, -%s)' % (e.id, a) + return 'ck_s32(%d, -(int64_t)%s)' % (e.id, a) + s = '(%s%s)' % (e.a, a) + return '((uint16_t)%s)' % s if t is U16 else s + + def count(self, e, bits, node_id): + if e.op == 'const' and 0 <= e.a < bits: + s = str(e.a) + else: + s = '(%s & %d)' % (self.ex(e), bits - 1) + return 'ck_cnt(%d, %s, %d)' % (node_id, s, bits) if self.checked else s + + def ex_bin(self, e): + op = e.a + ka, kb = e.k + ta, tb = ty(ka), ty(kb) + if not is_int(ta): + return '(%s %s %s)' % (self.ex(ka), op, self.ex(kb)) + if op in LOGIC: + return '(%s %s %s)' % (self.ex(ka), op, self.ex(kb)) + if op in SHIFT: + t = promote(ta) + a, n = self.ex(ka), self.count(kb, t.bits, e.id) + if op == '<<' and t.signed and self.checked: + return 'ck_shl%d(%d, %s, %s)' % (t.bits, e.id, a, n) + s = '(%s %s %s)' % (a, op, n) + return '((uint16_t)%s)' % s if op == '<<' and t is U16 else s + ct = common(ta, tb) + if op in CMP: + if ct is U16: + return '(%s %s %s)' % (self.u16(ka), op, self.u16(kb)) + return '(%s %s %s)' % (self.ex(ka), op, self.ex(kb)) + if op in DIV: + name = 'div' if op == '/' else 'mod' + if ct is U16: + a, b = self.u16(ka), self.u16(kb) + if self.checked: + return 'ck_u%s16(%d, %s, %s)' % (name, e.id, a, b) + return '(%s %s %s)' % (a, op, b) + a, b = self.ex(ka), self.ex(kb) + if self.checked: + return 'ck_%s%s%d(%d, %s, %s)' % ('' if ct.signed else 'u', name, ct.bits, + e.id, a, b) + return '(%s %s %s)' % (a, op, b) + a, b = self.ex(ka), self.ex(kb) + if self.checked and op in ARITH: + if ct is I16: + return 'ck_s16(%d, %s %s %s)' % (e.id, a, op, b) + if ct is I32: + return 'ck_s32(%d, (int64_t)(int32_t)%s %s (int64_t)(int32_t)%s)' % ( + e.id, a, op, b) + if ct is U16 and op == '*' and promote(ta) is U16 and promote(tb) is U16: + return 'ck_umul16(%d, %s, %s)' % (e.id, a, b) + s = '(%s %s %s)' % (a, op, b) + return '((uint16_t)%s)' % s if ct is U16 else s + + def ex_cond(self, e): + s = '(%s ? %s : %s)' % (self.ex(e.k[0]), self.ex(e.k[1]), self.ex(e.k[2])) + t = ty(e) + return '((uint16_t)%s)' % s if t is U16 else s + + def ex_call(self, e): + f = e.a + name = '(*%s_p)' % f.name if f.via_ptr and e.id % 2 else f.name + return '%s(%s)' % (name, ', '.join(self.ex(k) for k in e.k)) + + def ex_asg(self, e): + lv, rhs = e.k + op = e.a + left, right = self.lv(lv), self.ex(rhs) + if op == '=': + return '(%s = %s)' % (left, right) + lt = ty(lv) + if not is_int(lt): + return '(%s %s %s)' % (left, op, right) + bop = op[:-1] + back = '' if is_bitfield(lv) else '(%s)' % lt.cname + if bop in SHIFT: + t = promote(lt) + n = self.count(rhs, t.bits, e.id) + if bop == '<<' and t.signed and self.checked: + return '(%s = %sck_shl%d(%d, %s, %s))' % (left, back, t.bits, e.id, left, n) + return '(%s %s %s)' % (left, op, n) + ct = common(lt, ty(rhs)) + if bop in DIV: + name = 'div' if bop == '/' else 'mod' + if ct is U16: + b = self.u16(rhs) + if promote(lt) is not U16: + # the target is converted to unsigned for the operation; + # a compound assignment cannot spell that on the host + a = '((uint16_t)%s)' % left + if self.checked: + return '(%s = %sck_u%s16(%d, %s, %s))' % (left, back, name, e.id, a, b) + return '(%s = %s(%s %s %s))' % (left, back, a, bop, b) + if self.checked: + return '(%s = %sck_u%s16(%d, %s, %s))' % (left, back, name, e.id, left, b) + return '(%s %s %s)' % (left, op, b) + if self.checked: + return '(%s = %sck_%s%s%d(%d, %s, %s))' % ( + left, back, '' if ct.signed else 'u', name, ct.bits, e.id, left, right) + return '(%s %s %s)' % (left, op, right) + if self.checked and bop in ARITH: + if ct is I16: + return '(%s = %sck_s16(%d, %s %s %s))' % (left, back, e.id, left, bop, right) + if ct is I32: + return '(%s = %sck_s32(%d, (int64_t)(int32_t)%s %s (int64_t)(int32_t)%s))' % ( + left, back, e.id, left, bop, right) + if ct is U16 and bop == '*' and promote(lt) is U16 and promote(ty(rhs)) is U16: + return '(%s = %sck_umul16(%d, %s, %s))' % (left, back, e.id, left, right) + return '(%s %s %s)' % (left, op, right) + + def ex_incdec(self, e): + lv = e.k[0] + op, prefix = e.a + left = self.lv(lv) + lt = ty(lv) + if self.checked and is_int(lt) and lt in (I16, I32) and not is_bitfield(lv): + d = 1 if op == '++' else -1 + if prefix: + if lt is I16: + return '(%s = ck_s16(%d, %s + %d))' % (left, e.id, left, d) + return '(%s = ck_s32(%d, (int64_t)%s + %d))' % (left, e.id, left, d) + return 'ck_post%d(%d, &%s, %d)' % (lt.bits, e.id, left, d) + return '(%s%s)' % (op, left) if prefix else '(%s%s)' % (left, op) + + # ---- statements + + def top(self, e): + """An expression statement or controlling expression, unparenthesized.""" + s = self.ex(e) + if s.startswith('(') and s.endswith(')'): + depth = 0 + for i, ch in enumerate(s): + depth += ch == '(' + depth -= ch == ')' + if depth == 0 and i < len(s) - 1: + return s + return s[1:-1] + return s + + def decl(self, d): + if isinstance(d.t, PtrType): + s = '%s *%s' % (d.t.to.cname, d.name) + else: + s = '%s %s%s' % (d.cname, d.name, ''.join('[%d]' % n for n in d.dims)) + if d.static: + s = 'static ' + s + if isinstance(d.init, E): + s += ' = ' + self.top(d.init) + elif d.init is not None: + s += ' = ' + self.init(d.init, d.t, d.dims) + return s + + def init(self, v, t, dims): + if dims: + return '{ %s }' % ', '.join(self.init(x, t, dims[1:]) for x in v) + if isinstance(t, StructType): + return '{ %s }' % ', '.join(self.init(x, f.t, f.dims) for x, f in zip(v, t.fields)) + return static_text(v, t) + + def block(self, b, out, ind): + for d in b.decls: + out.append('%s%s;' % (ind, self.decl(d))) + for s in b.stmts: + self.stmt(s, out, ind) + + def braced(self, b, out, ind, head, tail=''): + out.append('%s%s {' % (ind, head)) + self.block(b, out, ind + ' ') + out.append('%s}%s' % (ind, tail)) + + def stmt(self, s, out, ind): + op = s.op + if op == 'expr': + out.append('%s%s;' % (ind, self.top(s.e[0]))) + elif op == 'if': + self.braced(s.b[0], out, ind, 'if (%s)' % self.top(s.e[0])) + if len(s.b) > 1 and s.b[1] is not None: + out[-1] += ' else {' + self.block(s.b[1], out, ind + ' ') + out.append(ind + '}') + elif op == 'for': + if s.a is not None: + init = self.decl(s.a) + else: + init = self.top(s.e[0]) if s.e[0] is not None else '' + test = self.top(s.e[1]) if s.e[1] is not None else '' + step = self.top(s.e[2]) if s.e[2] is not None else '' + self.braced(s.b[0], out, ind, 'for (%s; %s; %s)' % (init, test, step)) + elif op == 'while': + self.braced(s.b[0], out, ind, 'while (%s)' % self.top(s.e[0])) + elif op == 'do': + self.braced(s.b[0], out, ind, 'do', ' while (%s);' % self.top(s.e[0])) + elif op == 'switch': + out.append('%sswitch (%s) {' % (ind, self.top(s.e[0]))) + for (labels, has_break), body in zip(s.a, s.b): + if labels is None: + out.append(ind + 'default:') + else: + for v in labels: + out.append('%scase %d:' % (ind, v)) + self.block(body, out, ind + ' ') + if has_break: + out.append(ind + ' break;') + out.append(ind + '}') + elif op in ('break', 'continue'): + out.append('%s%s;' % (ind, op)) + elif op == 'ret': + out.append('%sreturn%s;' % (ind, ' ' + self.top(s.e[0]) if s.e else '')) + elif op == 'block': + out.append(ind + '{') + self.block(s.b[0], out, ind + ' ') + out.append(ind + '}') + elif op == 'goto': + out.append('%sif (%s) goto %s;' % (ind, self.top(s.e[0]), s.a)) + elif op == 'label': + out.append('%s:;' % s.a) + else: + raise ValueError(op) + + # ---- program + + def signature(self, f): + ret = 'void' if f.ret is None else f.ret.cname + params = ', '.join(self.decl(p) for p in f.params) or 'void' + return '%s%s %s(%s)' % ('static ' if f.static else '', ret, f.name, params) + + def state(self, prog, out): + out.append('static uint32_t state(void)') + out.append('{') + out.append(' uint8_t i, j;') + out.append(' ck = 0;') + + def leaf(text, t, dims, ind, var): + if dims: + out.append('%sfor (%s = 0; %s < %d; %s++)' % (ind, var, var, dims[0], var)) + leaf('%s[%s]' % (text, var), t, dims[1:], ind + ' ', 'j') + elif isinstance(t, StructType): + out.append(ind + '{') + for f in t.fields: + leaf('%s.%s' % (text, f.name), f.t, f.dims, ind + ' ', 'j') + out.append(ind + '}') + else: + out.append('%smix((uint32_t)%s);' % (ind, text)) + + for g in prog.globals: + if isinstance(g.t, PtrType): + continue + leaf(g.name, g.t, g.dims, ' ', 'i') + out.append(' return ck;') + out.append('}') + + def program(self, prog, title=''): + out = ['/* generated by clang-oracle-fuzz.py%s */' % title, + '#include ', '#include '] + if self.checked: + out.append(CHECK_PRELUDE % {'nid': prog.nid}) + out.append('#define ck_idx(v, n) ((v) < 0 || (v) >= (n) ? (abort(), 0) : (v))') + out.append('') + for st in prog.structs: + out.append('struct %s {' % st.name) + for f in st.fields: + dims = ''.join('[%d]' % n for n in f.dims) + bits = ' : %d' % f.bf if f.bf else '' + out.append(' %s %s%s%s;' % (f.t.cname, f.name, dims, bits)) + out.append('};') + out.append('') + for g in prog.globals: + out.append('%s;' % self.decl(g)) + out.append(HARNESS) + self.state(prog, out) + out.append('') + for f in prog.funcs: + if f.kind == 'rec' or f.via_ptr: + out.append(self.signature(f) + ';') + for f in prog.funcs: + if f.via_ptr: + ret = 'void' if f.ret is None else f.ret.cname + params = ', '.join(p.cname for p in f.params) or 'void' + out.append('static %s (*%s_p)(%s) = %s;' % (ret, f.name, params, f.name)) + for f in prog.funcs: + out.append('') + out.append(self.signature(f)) + out.append('{') + self.block(f.body, out, ' ') + out.append('}') + out.append('') + out.append('int main(void)') + out.append('{') + n = 0 + for f in prog.tests: + for args in f.inputs: + n += 1 + call = '%s(%s)' % (f.name, ', '.join(self.ex(a) for a in args)) + if is_int(f.ret): + out.append(' show(%d, (uint32_t)%s);' % (n, call)) + else: + out.append(' %s;' % call) + out.append(' show(%d, state());' % (n + 0x4000)) + if self.checked: + out.append(' ck_report();') + out.append(' return 0;') + out.append('}') + return '\n'.join(out) + '\n' + + +# --------------------------------------------------------------- generation + +POOL = [0, 1, 2, 3, 5, 7, 8, 15, 16, 31, 63, 100, 127, 128, 129, 255, 256, 257, 1000, + 0x7ffe, 0x7fff, 0x8000, 0x8001, 0xfffe, 0xffff, 0x10000, 0x10001, 0x12345, + 0x7fffffff, 0x80000000, 0x80000001, 0xfffffffe, 0xffffffff, 0x00ff00ff, + 0xdeadbeef, 0x55aa, 0xaa55, -1, -2, -3, -100, -128, -129, -32768, -32769] + + +class Ctx: + def __init__(self, fn, pure): + self.fn = fn + self.pure = pure + self.ints, self.arrs, self.sarrs, self.svars = [], [], [], [] + self.ptrs, self.sptrs, self.walk = [], [], [] + self.counters = [] + self.prot = set() + self.used, self.banned = set(), set() + self.loop = 0 + self.nosfx = 0 + self.names = 0 + self.labels = 0 + + def mark(self): + return [len(x) for x in (self.ints, self.arrs, self.sarrs, self.svars, self.ptrs, + self.sptrs, self.walk, self.counters)] + + def release(self, mark): + for lst, n in zip((self.ints, self.arrs, self.sarrs, self.svars, self.ptrs, + self.sptrs, self.walk, self.counters), mark): + del lst[n:] + + def can_write(self, d): + """Whether d (or, for a pointer, what it points at) may be stored to.""" + if isinstance(d.t, PtrType): + return d.tgt_local or not self.pure + if d in self.prot or d in self.banned: + return False + return d.storage != 'global' or not self.pure + + def name(self, prefix): + self.names += 1 + return '%s%d' % (prefix, self.names) + + def fresh(self): + self.used, self.banned = set(), set() + + +FEATURES = frozenset(( + 'bitfield', 'nested', 'arrfield', 'struct', 'structval', 'ptr', 'arr2d', 'localarr', + 'if', 'for', 'while', 'do', 'switch', 'block', 'call', 'scopy', 'walk', 'brk', 'ret', + 'goto', 'c99for', 'blockdecl', 'static', 'varbound', + 'logic', 'un', 'cast', 'cond', 'shift', 'div', 'pcall', 'sfx', 'fnptr', 'rec', + 'cassign', 'incdec', 'i8', 'i32', + # forms that hit a logged dcc defect; see KNOWN_BUGS + 'widecassign', 'bytevarshl', 'castsfx', 'widepostinc', 'structbytecasg', 'negnotconst', + 'bfdivasg', 'wideidxincdec', 'logicinit', 'blockwidediv', 'ptrcallconv', + 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', + 'selfand', 'widecasgbf', 'widehelperparam')) + +# Features switched off by `--off known`, with the entry in compiler-bugs.md +# that each one would otherwise keep rediscovering. Remove a feature from this +# table when its bug is fixed. +KNOWN_BUGS = { + 'bytevarshl': 'BUG-002', + 'wideidxincdec': 'BUG-003', + 'logicinit': 'BUG-004', + 'bfdivasg': 'BUG-006', + 'ptrcallconv': 'BUG-007', + 'blockwideneg': 'BUG-008', + 'nestedwideless': 'BUG-009', + 'widepostinc': 'BUG-010', + 'structbytecasg': 'BUG-011', + 'negnotconst': 'BUG-012', + 'bfpromo': 'BUG-013', + 'widelocalinc': 'BUG-014', + 'blockptr': 'BUG-015', + 'inittype': 'BUG-016', + 'selfand': 'BUG-017', + 'widecasgbf': 'BUG-018', + 'widehelperparam': 'BUG-020', +} + + +class Gen: + def __init__(self, seed, tests=6, size=10, depth=3, off=frozenset()): + self.rng = random.Random(seed) + self.prog = Program(seed, off) + self.n_tests, self.size, self.depth = tests, size, depth + self.off = frozenset(off) + self.pure_fns, self.impure_fns, self.struct_fns = [], [], [] + + def on(self, feature): + return feature not in self.off + + # ---- small helpers + + def mk(self, op, k=(), a=None, t=None): + self.prog.nid += 1 + return E(op, k, a, t, self.prog.nid) + + def pick(self, weighted): + total = sum(w for _, w in weighted) + x = self.rng.random() * total + for item, w in weighted: + x -= w + if x < 0: + return item + return weighted[-1][0] + + def int_type(self): + return self.pick([(I8, 3 if self.on('i8') else 0), (U8, 4 if self.on('i8') else 0), + (I16, 5), (U16, 5), (I32, 5 if self.on('i32') else 0), + (U32, 4 if self.on('i32') else 0)]) + + def value(self, t): + r = self.rng + if r.random() < 0.7: + return t.wrap(r.choice(POOL)) + return t.wrap(r.getrandbits(32) if r.random() < 0.5 else r.randint(-300, 300)) + + def const(self, t=None): + if t is None: + t = self.pick([(I16, 8), (U16, 3), (I32, 5 if self.on('i32') else 0), + (U32, 4 if self.on('i32') else 0)]) + if t.bits < 16: + t = I16 if t.signed else U16 + return self.mk('const', a=self.rng.randint(0, 127), t=t) + return self.mk('const', a=self.value(t), t=t) + + def small(self, lo, hi): + return self.mk('const', a=self.rng.randint(lo, hi), t=I16) + + def spelled(self, t): + return self.rng.choice(t.cnames) + + # ---- program skeleton + + def gen_structs(self): + r = self.rng + for n in range(2): + fields = [] + for i in range(r.randint(3, 6)): + name = 'm%d' % i + kind = r.random() + if kind < 0.2 and self.on('bitfield'): + t = r.choice((I16, U16)) + width = r.randint(2 if t.signed else 1, 15) + fields.append(Field(name, t, bf=width)) + elif kind < 0.35 and self.on('arrfield'): + fields.append(Field(name, self.int_type(), dims=(r.choice((2, 4)),))) + elif kind < 0.45 and n == 1 and self.on('nested'): + fields.append(Field(name, self.prog.structs[0])) + else: + fields.append(Field(name, self.int_type())) + self.prog.structs.append(StructType('S%d' % n, fields)) + + def init_value(self, t, dims): + if dims: + return [self.init_value(t, dims[1:]) for _ in range(dims[0])] + if isinstance(t, StructType): + return [self.field_value(f) for f in t.fields] + return self.value(t) + + def field_value(self, f): + if f.bf: + lo = -(1 << (f.bf - 1)) if f.t.signed else 0 + hi = (1 << (f.bf - 1)) - 1 if f.t.signed else (1 << f.bf) - 1 + return self.rng.randint(lo, hi) + return self.init_value(f.t, f.dims) + + def gen_globals(self): + r = self.rng + g = self.prog.globals + n = 0 + allowed = lambda t: self.on('i8' if t.bits == 8 else 'i32' if t.bits == 32 else '') + for t in INT_TYPES: + if not allowed(t): + continue + for _ in range(r.randint(1, 2)): + d = Decl('g%d' % n, t, 'global', init=self.value(t), cname=self.spelled(t)) + d.static = r.random() < 0.6 + g.append(d) + n += 1 + for i, (t, dims) in enumerate([(U8, (8,)), (I16, (8,)), (U16, (4,)), (I32, (4,)), + (U32, (8,)), (r.choice((U8, I16)), (4, 4)), + (I8, (8,))]): + if not allowed(t) or (len(dims) > 1 and not self.on('arr2d')): + continue + d = Decl('a%d' % i, t, 'global', dims=dims, init=self.init_value(t, dims)) + d.static = r.random() < 0.6 + g.append(d) + if not self.on('struct'): + return + s0, s1 = self.prog.structs + for i, (t, dims) in enumerate([(s0, ()), (s1, ()), (r.choice((s0, s1)), (4,))]): + d = Decl('s%d' % i, t, 'global', dims=dims, init=self.init_value(t, dims)) + d.static = True + g.append(d) + + def ctx(self, fn, pure): + c = Ctx(fn, pure) + for d in self.prog.globals: + self.expose(c, d) + return c + + def expose(self, c, d): + if isinstance(d.t, PtrType): + (c.sptrs if isinstance(d.t.to, StructType) else c.ptrs).append(d) + elif isinstance(d.t, StructType): + (c.sarrs if d.dims else c.svars).append(d) + elif d.dims: + c.arrs.append(d) + else: + c.ints.append(d) + + # ---- expressions + + def gen_index(self, c, depth, n): + r = self.rng + counters = [d for d in c.counters if d.rng is not None and d.rng < n] + x = r.random() + if counters and x < 0.45: + d = r.choice(counters) + c.used.add(d) + return self.mk('var', a=d) + if x < 0.6: + return self.small(0, n - 1) + return self.gen_expr(c, min(depth - 1, 1)) + + def struct_lvalue(self, c, depth, write): + r = self.rng + opts = [('var', d) for d in c.svars if not write or c.can_write(d)] + opts += [('arr', d) for d in c.sarrs if not write or c.can_write(d)] + opts += [('ptr', d) for d in c.sptrs if not write or c.can_write(d)] + if not opts: + return None + kind, d = r.choice(opts) + if kind == 'var': + return self.mk('var', a=d) + if kind == 'arr': + return self.mk('idx', [self.gen_index(c, depth, d.dims[0])], a=d) + return self.mk('deref', [self.mk('var', a=d)]) + + def field_of(self, c, depth, base, st, nest=True): + r = self.rng + fields = [f for f in st.fields if nest or not isinstance(f.t, StructType)] + f = r.choice(fields) + node = self.mk('fld', [base], a=f) + if isinstance(f.t, StructType): + return self.field_of(c, depth, node, f.t, nest=False) + if f.dims: + node.k.append(self.gen_index(c, depth, f.dims[0])) + return node + + def gen_lvalue(self, c, depth, write=False): + """An integer lvalue. One that will be stored to has no side effects + in its index expressions: a repair may duplicate the lvalue + (`x op= e` becomes `x = x op e`), and a duplicated `i++` would be an + unsequenced modification.""" + if not write: + return self.pick_lvalue(c, depth, False) + c.nosfx += 1 + try: + return self.pick_lvalue(c, depth, True) + finally: + c.nosfx -= 1 + + def pick_lvalue(self, c, depth, write): + r = self.rng + ints = [d for d in c.ints if d not in c.banned and (not write or c.can_write(d))] + arrs = [d for d in c.arrs if not write or c.can_write(d)] + ptrs = [d for d in c.ptrs + c.walk if not write or c.can_write(d)] + has_struct = any(not write or c.can_write(d) for d in c.svars + c.sarrs + c.sptrs) + kind = self.pick([('var', 6 if ints else 0), ('idx', 3 if arrs else 0), + ('fld', 3 if has_struct else 0), ('ptr', 2 if ptrs else 0)]) + if kind == 'var' and ints: + if not write: + locals_ = [d for d in ints if d.storage != 'global'] + if locals_ and r.random() < 0.6: + ints = locals_ + d = r.choice(ints) + c.used.add(d) + return self.mk('var', a=d) + if kind == 'idx' and arrs: + d = r.choice(arrs) + return self.mk('idx', [self.gen_index(c, depth, n) for n in d.dims], a=d) + if kind == 'fld': + base = self.struct_lvalue(c, depth, write) + if base is not None: + return self.field_of(c, depth, base, ty(base)) + if kind == 'ptr' and ptrs: + d = r.choice(ptrs) + if d in c.walk or r.random() < 0.4: + return self.mk('deref', [self.mk('var', a=d)]) + return self.mk('pidx', [self.gen_index(c, depth, d.valid)], a=d) + if ints: + d = r.choice(ints) + c.used.add(d) + return self.mk('var', a=d) + return None + + def gen_leaf(self, c, depth): + if self.rng.random() < 0.22: + return self.const() + return self.gen_lvalue(c, depth) or self.const() + + def victim(self, c): + cands = [d for d in c.ints if d.storage != 'global' and not d.addr_taken + and d not in c.prot and d not in c.used and d not in c.banned] + if not cands: + return None + d = self.rng.choice(cands) + c.used.add(d) + c.banned.add(d) + return d + + def gen_expr(self, c, depth): + r = self.rng + if depth <= 0 or r.random() < 0.14: + return self.gen_leaf(c, depth) + w = lambda feature, weight: weight if self.on(feature) else 0 + kind = self.pick([('arith', 30), ('bits', 14), ('cmp', 10), ('logic', w('logic', 5)), + ('un', w('un', 7)), ('cast', w('cast', 12)), ('cond', w('cond', 7)), + ('shift', w('shift', 9)), ('div', w('div', 6)), + ('call', w('pcall', 9) if self.pure_fns else 0), + ('sfx', w('sfx', 5) if not c.nosfx else 0)]) + sub = lambda: self.gen_expr(c, depth - 1) + if kind == 'arith': + return self.mk('bin', [sub(), sub()], r.choice(('+', '+', '-', '-', '*'))) + if kind == 'bits': + return self.mk('bin', [sub(), sub()], r.choice(BITS)) + if kind == 'cmp': + return self.mk('bin', [sub(), sub()], r.choice(CMP)) + if kind == 'logic': + return self.mk('bin', [sub(), sub()], r.choice(LOGIC)) + if kind == 'shift': + count = self.small(0, 17) if r.random() < 0.6 else sub() + return self.mk('bin', [sub(), count], r.choice(SHIFT)) + if kind == 'div': + x = r.random() + if x < 0.4: + divisor = self.small(1, 200) if r.random() < 0.8 else self.const() + elif x < 0.7: + divisor = self.mk('bin', [sub(), self.small(1, 1)], '|') + else: + divisor = sub() + return self.mk('bin', [sub(), divisor], r.choice(DIV)) + if kind == 'un': + op = r.choice(('-', '~', '!')) + operand = sub() + if op in ('-', '~') and operand.op == 'un' and operand.a == '!' \ + and not self.on('negnotconst'): + return operand + return self.mk('un', [operand], op) + if kind == 'cast': + return self.mk('cast', [sub()], t=self.int_type()) + if kind == 'cond': + return self.mk('cond', [self.gen_cond(c, depth - 1), sub(), sub()]) + if kind == 'call': + f = r.choice(self.pure_fns) + args = [sub() for _ in f.params] + if f.via_ptr and not self.on('ptrcallconv'): + args = [self.mk('cast', [a], t=p.t) for a, p in zip(args, f.params)] + return self.mk('call', args, a=f) + d = self.victim(c) + if d is None: + return self.gen_leaf(c, depth) + lv = self.mk('var', a=d) + x = r.random() + if x < 0.45: + # with `widepostinc` off a 32-bit victim is only pre-incremented: + # its old value could reach a narrower type + prefix = r.random() < 0.5 or (d.t.bits == 32 and not self.on('widepostinc')) + op = r.choice(('++', '--')) + if d.t is I32 and not self.on('widelocalinc'): + op = '--' + return self.mk('incdec', [lv], a=(op, prefix)) + if x < 0.75: + return self.mk('asg', [lv, sub()], '=') + return self.compound(lv, sub(), r.choice(('+=', '-=', '^=', '|=', '&=', '>>='))) + + def gen_cond(self, c, depth): + x = self.rng.random() + if x < 0.65 or depth < -1: + return self.mk('bin', [self.gen_expr(c, depth), self.gen_expr(c, depth)], + self.rng.choice(CMP)) + if x < 0.8: + return self.mk('bin', [self.gen_cond(c, depth - 1), self.gen_cond(c, depth - 1)], + self.rng.choice(LOGIC)) + if x < 0.88: + return self.mk('un', [self.gen_expr(c, depth)], '!') + return self.gen_expr(c, depth) + + # ---- statements + + def new_local(self, c, prefix, t, init, top=True): + d = Decl(c.name(prefix), t, 'local', init=init, cname=self.spelled(t)) + if top: + c.fn.body.decls.append(d) + return d + + def gen_block(self, c, depth, n, decls=0): + b = Block() + mark = c.mark() + if not self.on('blockdecl'): + decls = 0 + for _ in range(decls): + self.block_decl(c, b, depth) + for _ in range(n): + self.gen_stmt(c, depth, b) + c.release(mark) + return b + + def block_decl(self, c, b, depth): + t = self.int_type() + c.fresh() + init, later = self.initialiser(c, t, self.gen_expr(c, min(depth, 2)), 'block') + d = Decl(c.name('b'), t, 'block', init=init, cname=self.spelled(t)) + b.decls.append(d) + self.assign_after(b, d, later) + c.ints.append(d) + + def gen_stmt(self, c, depth, b): + r = self.rng + c.fresh() + nest = depth > 0 + w = lambda feature, weight: weight if self.on(feature) else 0 + kind = self.pick([ + ('assign', 30), ('cassign', w('cassign', 12)), ('incdec', w('incdec', 5)), + ('if', w('if', 13) if nest else 0), ('for', w('for', 8) if nest else 0), + ('while', w('while', 3) if nest else 0), ('do', w('do', 3) if nest else 0), + ('switch', w('switch', 5) if nest else 0), ('block', w('block', 5) if nest else 0), + ('call', w('call', 7) if self.impure_fns and not c.pure else 0), + ('scopy', w('scopy', 4)), ('walk', w('walk', 3) if nest and self.on('ptr') else 0), + ('brk', w('brk', 4) if c.loop else 0), ('ret', w('ret', 2)), + ('goto', w('goto', 2) if nest else 0), + ('ptr', 2 if c.ptrs or c.sptrs else 0)]) + getattr(self, 'st_' + kind)(c, depth, b) + + def stored(self, rhs, t, storage, init=False): + """rhs as the value stored to a local of type t (init: as its + initialiser), steered round logged bugs whose feature is off.""" + if storage == 'global' or not is_int(t): + return rhs + if not self.on('logicinit') and t.bits <= 16 and rhs.op == 'bin' and rhs.a in LOGIC: + rhs.a = '|' if rhs.a == '||' else '&' + if not self.on('blockwideneg') and storage == 'block' and t.bits == 32 \ + and rhs.op == 'const' and (rhs.a < 0 or (t is I32 and rhs.a >= 1 << 31)): + rhs.a = -rhs.a if rhs.a < 0 and rhs.a != rhs.t.lo else 1 + return rhs + + def initialiser(self, c, t, expr, storage): + """(initialiser, statement or None) for a new local of type t that + starts as expr. With `inittype` off, an expr that is not already of + exactly type t is assigned by a following statement instead.""" + expr = self.stored(expr, t, storage) + if self.on('inittype') or init_keeps_type(expr, t): + return expr, None + return mk_const(0, t if t.bits >= 16 else I16), expr + + def assign_after(self, block, d, value): + if value is not None: + block.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=d), value], '=')])) + + def st_assign(self, c, depth, b): + lv = self.gen_lvalue(c, depth, write=True) + if lv is None: + return + rhs = self.gen_expr(c, depth) + if lv.op == 'var': + rhs = self.stored(rhs, lv.a.t, lv.a.storage) + b.stmts.append(S('expr', [self.mk('asg', [lv, rhs], '=')])) + + def compound(self, lv, rhs, op): + """lv op= rhs. A feature that is off names a form dcc is known to + reject or miscompile (see KNOWN_BUGS); the operand is converted so the + statement stays out of it and fuzzing can reach other code.""" + lt = ty(lv) + if op != '=' and is_int(lt): + if not self.on('selfand') and lv.op == 'var' and rhs.op == 'var' and rhs.a is lv.a: + rhs = self.small(1, 7) + if not self.on('widelocalinc') and lv.op == 'var' and lt is I32 \ + and lv.a.storage != 'global' and op == '+=' and rhs.op == 'const' \ + and rhs.a == 1: + rhs = self.small(2, 2) + if not self.on('structbytecasg') and lv.op == 'fld' and lv.a.dims \ + and lt.bits == 8: + op = '=' + return self.mk('asg', [lv, rhs], op) + if not self.on('bfdivasg') and is_bitfield(lv) and op in ('/=', '%=') \ + and not lv.a.t.signed: + rhs = self.mk('bin', [clone(lv), rhs], op[:-1]) + return self.mk('asg', [lv, rhs], '=') + if not self.on('blockwidediv') and lv.op == 'fld' and lt.bits == 32 \ + and op in ('/=', '%='): + rhs = self.mk('bin', [clone(lv), rhs], op[:-1]) + return self.mk('asg', [lv, rhs], '=') + if not self.on('widecassign') and lv.op != 'var' and lt.bits < 32 \ + and ty(rhs).bits == 32: + rhs = self.mk('cast', [rhs], t=lt) + if not self.on('widecasgbf') and ty(rhs).bits == 32 \ + and (is_bitfield(lv) or (lv.op == 'fld' and lv.a.dims and lt.bits == 16)): + rhs = self.mk('cast', [rhs], t=lt) + if not self.on('bytevarshl') and op == '<<=' and lt.bits == 8 \ + and ty(rhs).bits == 8: + rhs = self.mk('cast', [rhs], t=I16) + return self.mk('asg', [lv, rhs], op) + + def st_cassign(self, c, depth, b): + lv = self.gen_lvalue(c, depth, write=True) + if lv is None: + return + op = self.rng.choice(('+=', '+=', '-=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', + '<<=', '>>=')) + if op in ('<<=', '>>=') and self.rng.random() < 0.6: + rhs = self.small(0, 17) + elif op in ('/=', '%=') and self.rng.random() < 0.6: + rhs = self.small(1, 50) + else: + rhs = self.gen_expr(c, depth - 1) + b.stmts.append(S('expr', [self.compound(lv, rhs, op)])) + + def st_incdec(self, c, depth, b): + lv = self.gen_lvalue(c, depth, write=True) + if lv is None: + return + op = self.rng.choice(('++', '--')) + if not self.on('widelocalinc') and lv.op == 'var' and ty(lv) is I32 \ + and lv.a.storage != 'global': + op = '--' + indexed = lv.op in ('idx', 'pidx') or (lv.op == 'fld' and lv.a.dims) + if not self.on('wideidxincdec') and indexed and ty(lv).bits == 32: + b.stmts.append(S('expr', [self.mk('asg', [lv, self.small(1, 1)], op[0] + '=')])) + return + b.stmts.append(S('expr', [self.mk('incdec', [lv], a=(op, self.rng.random() < 0.5))])) + + def st_if(self, c, depth, b): + r = self.rng + cond = self.gen_cond(c, min(depth, 2)) + then = self.gen_block(c, depth - 1, r.randint(1, 3), decls=r.random() < 0.3) + blocks = [then] + if r.random() < 0.45: + blocks.append(self.gen_block(c, depth - 1, r.randint(1, 2), decls=r.random() < 0.2)) + b.stmts.append(S('if', [cond], blocks)) + + def loop_bound(self, c, b, n): + """(bound, slack): a constant n, or a protected local set to a value in + n..n+slack just before the loop.""" + r = self.rng + if r.random() < 0.5 or not self.on('varbound'): + return self.small(n, n), 0 + t = self.int_type() + d = self.new_local(c, 'n', t, self.small(0, 0)) + c.prot.add(d) + c.fresh() + base = self.gen_expr(c, 1) + value = self.mk('bin', [self.mk('bin', [base, self.small(3, 3)], '&'), + self.small(n, n)], '+') + b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=d), value], '=')])) + c.ints.append(d) + return self.mk('var', a=d), 3 + + def loop_body(self, c, depth, counter=None, hi=None): + r = self.rng + mark = c.mark() + if counter is not None: + counter.rng = hi + c.ints.append(counter) + c.counters.append(counter) + c.loop += 1 + body = self.gen_block(c, depth - 1, r.randint(1, 3), decls=r.random() < 0.3) + c.loop -= 1 + c.release(mark) + return body + + def st_for(self, c, depth, b): + r = self.rng + t = self.int_type() + lo = r.randint(0, 3) + n = r.randint(1, 5) + hi = lo + n + c99 = self.on('c99for') and r.random() < 0.35 + down = r.random() < 0.35 + if c99: + d = Decl(c.name('i'), t, 'block', cname=self.spelled(t)) + else: + d = self.new_local(c, 'i', t, self.small(0, 0)) + c.prot.add(d) + var = lambda: self.mk('var', a=d) + if not down: + bound, slack = self.loop_bound(c, b, hi) + start = self.small(lo, lo) + x = r.random() + if x < 0.6: + test = self.mk('bin', [var(), bound], '<') + elif x < 0.8: + test = self.mk('bin', [var(), bound], '!=') + elif bound.op == 'const': + bound.a -= 1 + test = self.mk('bin', [var(), bound], '<=') + else: + test = self.mk('bin', [var(), bound], '<') + step = r.choice([ + self.mk('incdec', [var()], a=('++', False)), + self.mk('incdec', [var()], a=('++', True)), + self.mk('asg', [var(), self.small(1, 1)], '+='), + self.mk('asg', [var(), self.mk('bin', [var(), self.small(1, 1)], '+')], '=')]) + top = hi - 1 + slack + else: + start = self.small(hi, hi) + x = r.random() + if x < 0.5 or not t.signed: + test = self.mk('bin', [var(), self.small(lo, lo)], '>' if x < 0.4 else '!=') + top = hi + else: + test = self.mk('bin', [var(), self.small(lo, lo)], '>=') + top = hi + step = r.choice([ + self.mk('incdec', [var()], a=('--', False)), + self.mk('incdec', [var()], a=('--', True)), + self.mk('asg', [var(), self.small(1, 1)], '-=')]) + if c99: + d.init = start + init = None + else: + init = self.mk('asg', [var(), start], '=') + body = self.loop_body(c, depth, d, top) + b.stmts.append(S('for', [init, test, step], [body], a=d if c99 else None)) + + def counter_type(self): + """Type for a loop counter stepped inside the loop condition. With + `nestedwideless` off it is never 32 bits wide.""" + t = self.int_type() + while t.bits == 32 and not self.on('nestedwideless'): + t = self.int_type() + return t + + def st_while(self, c, depth, b): + r = self.rng + t = self.counter_type() + n = r.randint(1, 5) + d = self.new_local(c, 'w', t, self.small(0, 0)) + c.prot.add(d) + var = lambda: self.mk('var', a=d) + if r.random() < 0.5: + b.stmts.append(S('expr', [self.mk('asg', [var(), self.small(n, n)], '=')])) + post = self.mk('incdec', [var()], a=('--', False)) + test = self.mk('bin', [post, self.small(0, 0)], '>' if r.random() < 0.5 else '!=') + body = self.loop_body(c, depth) + else: + b.stmts.append(S('expr', [self.mk('asg', [var(), self.small(0, 0)], '=')])) + bound, _ = self.loop_bound(c, b, n) + post = self.mk('incdec', [var()], a=('++', False)) + test = self.mk('bin', [post, bound], '<') + body = self.loop_body(c, depth) + b.stmts.append(S('while', [test], [body])) + + def st_do(self, c, depth, b): + r = self.rng + t = self.counter_type() + n = r.randint(1, 5) + d = self.new_local(c, 'd', t, self.small(0, 0)) + c.prot.add(d) + var = lambda: self.mk('var', a=d) + b.stmts.append(S('expr', [self.mk('asg', [var(), self.small(0, 0)], '=')])) + bound, slack = self.loop_bound(c, b, n) + pre = self.mk('incdec', [var()], a=('++', True)) + test = self.mk('bin', [pre, bound], '<') + body = self.loop_body(c, depth, d, n - 1 + slack) + b.stmts.append(S('do', [test], [body])) + + def st_switch(self, c, depth, b): + r = self.rng + x = r.random() + base = self.gen_expr(c, min(depth, 2)) + if x < 0.5: + mask = r.choice((3, 7, 15)) + ctrl = self.mk('bin', [base, self.small(mask, mask)], '&') + values = list(range(mask + 1)) + elif x < 0.8: + m = r.randint(3, 7) + ctrl = self.mk('bin', [base, self.small(m, m)], '%') + values = list(range(-(m - 1), m)) + else: + ctrl = self.mk('cast', [base], t=r.choice((I8, U8))) + values = [-128, -2, -1, 0, 1, 2, 3, 10, 100, 127, 128, 200, 255] + values = [v for v in values if ctrl.t.lo <= v <= ctrl.t.hi] + r.shuffle(values) + ncase = r.randint(2, min(5, len(values))) + cases, blocks = [], [] + pos = 0 + for _ in range(ncase): + k = 1 if r.random() < 0.8 else 2 + labels = values[pos:pos + k] + pos += k + if not labels: + break + cases.append((labels, r.random() < 0.75)) + blocks.append(self.gen_block(c, depth - 1, r.randint(1, 2))) + if r.random() < 0.7: + at = r.randint(0, len(cases)) + cases.insert(at, (None, r.random() < 0.75)) + blocks.insert(at, self.gen_block(c, depth - 1, r.randint(1, 2))) + b.stmts.append(S('switch', [ctrl], blocks, a=cases)) + + def st_block(self, c, depth, b): + r = self.rng + b.stmts.append(S('block', b=[self.gen_block(c, depth - 1, r.randint(1, 3), + decls=r.randint(1, 2))])) + + def st_brk(self, c, depth, b): + cond = self.gen_cond(c, 1) + inner = Block() + inner.stmts.append(S(self.rng.choice(('break', 'continue')))) + b.stmts.append(S('if', [cond], [inner])) + + def st_ret(self, c, depth, b): + if c.fn.ret is None or not is_int(c.fn.ret): + return + cond = self.gen_cond(c, 1) + inner = Block() + c.fresh() + inner.stmts.append(S('ret', [self.gen_expr(c, min(depth, 2))])) + b.stmts.append(S('if', [cond], [inner])) + + def st_goto(self, c, depth, b): + c.labels += 1 + label = 'L%d' % c.labels + b.stmts.append(S('goto', [self.gen_cond(c, 1)], a=label)) + for _ in range(self.rng.randint(1, 2)): + self.st_assign(c, depth - 1, b) + b.stmts.append(S('label', a=label)) + + def st_scopy(self, c, depth, b): + dst = self.struct_lvalue(c, depth, write=True) + if dst is None: + return self.st_assign(c, depth, b) + st = ty(dst) + srcs = [f for f in self.struct_fns if f.ret is st] + args = None + if srcs and not c.pure and dst.op == 'var' and self.rng.random() < 0.6: + # the callee may store to globals, so the destination must need no + # evaluation of its own + f = self.rng.choice(srcs) + args = self.call_args(c, f, depth) + if args is not None: + src = self.mk('call', args, a=f) + else: + src = None + for _ in range(8): + cand = self.struct_lvalue(c, depth, write=False) + if cand is not None and ty(cand) is st: + src = cand + break + if src is None: + return self.st_assign(c, depth, b) + b.stmts.append(S('expr', [self.mk('asg', [dst, src], '=')])) + + def st_walk(self, c, depth, b): + r = self.rng + arrs = [d for d in c.arrs if len(d.dims) == 1 and (not c.pure or d.storage != 'global') + and (d.t.bits < 32 or self.on('blockptr'))] + if not arrs: + return self.st_assign(c, depth, b) + arr = r.choice(arrs) + blk = Block() + q = Decl(c.name('q'), PtrType(arr.t), 'block', + init=self.mk('addr', [self.small(0, 0)], a=(arr, arr.dims[0]))) + q.tgt_local = arr.storage != 'global' + end = Decl(c.name('e'), PtrType(arr.t), 'block', init=self.mk('aend', a=arr)) + blk.decls += [q, end] + c.prot.update((q, end)) + test = self.mk('bin', [self.mk('var', a=q), self.mk('var', a=end)], '!=') + targets = [d for d in c.ints if c.can_write(d) and d.storage != 'global' + and not d.addr_taken] + if targets and r.random() < 0.5: + acc = r.choice(targets) + bump = self.mk('incdec', [self.mk('var', a=q)], a=('++', False)) + op = r.choice(('+=', '^=', '|=', '-=')) + body = Block() + body.stmts.append(S('expr', [self.mk('asg', [ + self.mk('var', a=acc), self.mk('deref', [bump])], op)])) + blk.stmts.append(S('while', [test], [body])) + else: + mark = c.mark() + c.walk.append(q) + c.loop += 1 + body = self.gen_block(c, depth - 1, r.randint(1, 2)) + c.loop -= 1 + c.release(mark) + step = self.mk('incdec', [self.mk('var', a=q)], a=('++', r.random() < 0.5)) + blk.stmts.append(S('for', [None, test, step], [body])) + b.stmts.append(S('block', b=[blk])) + + def st_ptr(self, c, depth, b): + """Write through a pointer.""" + r = self.rng + ptrs = [d for d in c.ptrs if c.can_write(d)] + sptrs = [d for d in c.sptrs if c.can_write(d)] + if ptrs and (not sptrs or r.random() < 0.6): + d = r.choice(ptrs) + if r.random() < 0.5: + lv = self.mk('deref', [self.mk('var', a=d)]) + else: + lv = self.mk('pidx', [self.gen_index(c, depth, d.valid)], a=d) + elif sptrs: + d = r.choice(sptrs) + lv = self.field_of(c, depth, self.mk('deref', [self.mk('var', a=d)]), d.t.to) + else: + return self.st_assign(c, depth, b) + op = r.choice(('=', '=', '+=', '^=', '-=')) + b.stmts.append(S('expr', [self.compound(lv, self.gen_expr(c, depth), op)])) + + def call_args(self, c, f, depth): + args = [] + for p in f.params: + arg = None + if isinstance(p.t, PtrType): + arg = self.ptr_arg(c, p) + elif isinstance(p.t, StructType): + for _ in range(8): + cand = self.struct_lvalue(c, depth, write=False) + if cand is not None and ty(cand) is p.t: + arg = cand + break + else: + arg = self.gen_expr(c, min(depth, 2)) + if arg is None: + return None + args.append(arg) + return args + + def ptr_arg(self, c, p, constant=False): + """A pointer to global storage with at least p.valid elements.""" + r = self.rng + to = p.t.to + index = (lambda n: self.small(0, n - 1)) if constant \ + else (lambda n: self.gen_index(c, 1, n)) + if isinstance(to, StructType): + opts = [d for d in c.svars if d.t is to and d.storage == 'global'] + arrs = [d for d in c.sarrs if d.t is to and d.storage == 'global'] + if arrs and (not opts or r.random() < 0.5): + d = r.choice(arrs) + return self.mk('addr', [index(d.dims[0])], a=(d, 1)) + if opts: + return self.mk('addrof', a=r.choice(opts)) + return None + arrs = [d for d in c.arrs if d.t is to and len(d.dims) == 1 and d.dims[0] >= p.valid + and d.storage == 'global'] + if not arrs: + return None + d = r.choice(arrs) + return self.mk('addr', [index(d.dims[0] - p.valid + 1)], a=(d, p.valid)) + + def st_call(self, c, depth, b): + r = self.rng + f = r.choice(self.impure_fns) + args = self.call_args(c, f, depth) + if args is None: + return self.st_assign(c, depth, b) + call = self.mk('call', args, a=f) + if f.ret is None or not is_int(f.ret): + if f.ret is None: + b.stmts.append(S('expr', [call])) + else: + dsts = [d for d in c.svars if d.t is f.ret and c.can_write(d)] + if not dsts: + return self.st_assign(c, depth, b) + b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=r.choice(dsts)), + call], '=')])) + return + simple = [d for d in c.ints if c.can_write(d) and d not in c.used] + local = [d for d in simple if d.storage != 'global' and not d.addr_taken] + x = r.random() + if x < 0.5 and simple: + b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=r.choice(simple)), + call], '=')])) + elif x < 0.7 and local: + op = r.choice(('+=', '^=', '-=', '|=')) + b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=r.choice(local)), + call], op)])) + elif x < 0.9: + other = self.mk('var', a=r.choice(local)) if local and r.random() < 0.5 \ + else self.const() + cond = self.mk('bin', [call, other], r.choice(CMP)) + b.stmts.append(S('if', [cond], [self.gen_block(c, depth - 1, r.randint(1, 2))])) + else: + b.stmts.append(S('expr', [call])) + + # ---- functions + + def locals_for(self, c, fn, n): + r = self.rng + for _ in range(n): + t = self.int_type() + c.fresh() + init, later = self.initialiser(c, t, self.gen_expr(c, 1), 'local') + d = self.new_local(c, 'l', t, init) + # a pure helper keeps no state, so calls to it can be reordered + d.static = not c.pure and self.on('static') and r.random() < 0.06 + if d.static: + d.init = self.value(t) + else: + self.assign_after(fn.body, d, later) + c.ints.append(d) + if r.random() < 0.5 and self.on('localarr'): + t = self.int_type() + n = r.choice((2, 4, 8)) + d = Decl(c.name('v'), t, 'local', dims=(n,), init=self.init_value(t, (n,))) + fn.body.decls.append(d) + c.arrs.append(d) + if r.random() < 0.6 and self.on('ptr'): + valid = r.choice((1, 2)) if n == 2 else r.choice((1, 2, 4)) + p = Decl(c.name('p'), PtrType(t), 'local', + init=self.mk('addr', [self.small(0, n - valid)], a=(d, valid))) + p.valid, p.tgt_local = valid, True + c.prot.add(p) + fn.body.decls.append(p) + c.ptrs.append(p) + if r.random() < 0.35 and self.on('struct'): + st = r.choice(self.prog.structs) + d = Decl(c.name('t'), st, 'local', init=self.init_value(st, ())) + fn.body.decls.append(d) + c.svars.append(d) + if not c.pure and r.random() < 0.5 and self.on('ptr'): + arrs = [g for g in self.prog.globals if g.dims and len(g.dims) == 1 + and is_int(g.t)] + g = r.choice(arrs) + valid = r.choice((1, 2, 4)) + p = Decl(c.name('p'), PtrType(g.t), 'local', + init=self.mk('addr', [self.gen_index(c, 1, g.dims[0] - valid + 1)], + a=(g, valid))) + p.valid = valid + c.prot.add(p) + fn.body.decls.append(p) + c.ptrs.append(p) + + def params_for(self, c, fn, n, pointers): + r = self.rng + targets = sorted({g.t for g in self.prog.globals + if is_int(g.t) and len(g.dims) == 1 and g.dims[0] >= 4}, + key=lambda t: t.name) + ptr = pointers and self.on('ptr') + sptr = ptr and self.on('struct') + sval = pointers and self.on('struct') and self.on('structval') + for i in range(n): + x = r.random() + if ptr and x < 0.12: + t = r.choice(targets) + d = Decl('x%d' % i, PtrType(t), 'param') + d.valid = r.choice((1, 2, 4)) + c.prot.add(d) + c.ptrs.append(d) + elif sptr and 0.12 <= x < 0.2: + d = Decl('x%d' % i, PtrType(r.choice(self.prog.structs)), 'param') + c.prot.add(d) + c.sptrs.append(d) + elif sval and 0.2 <= x < 0.27: + st = r.choice(self.prog.structs) + d = Decl('x%d' % i, st, 'param') + c.svars.append(d) + else: + t = self.int_type() + d = Decl('x%d' % i, t, 'param', cname=self.spelled(t)) + if t.bits == 32 and not self.on('widehelperparam'): + # with this off a 32-bit parameter is only read through a + # local copy (BUG-020 needs the parameter itself) + copy_ = Decl('y%d' % i, t, 'local', init=self.mk('var', a=d), + cname=self.spelled(t)) + fn.body.decls.append(copy_) + c.ints.append(copy_) + else: + c.ints.append(d) + fn.params.append(d) + + def finish(self, c, fn): + if fn.ret is not None and is_int(fn.ret): + c.fresh() + fn.body.stmts.append(S('ret', [self.gen_expr(c, 2)])) + + def gen_pure(self, n): + r = self.rng + fn = Func('h%d' % n, self.int_type(), [], 'pure') + c = self.ctx(fn, True) + self.params_for(c, fn, r.randint(1, 4), False) + self.locals_for(c, fn, r.randint(0, 2)) + for _ in range(r.randint(0, 3)): + self.gen_stmt(c, 1, fn.body) + self.finish(c, fn) + fn.via_ptr = self.on('fnptr') and r.random() < 0.25 + return fn + + def gen_rec(self, n): + """Bounded recursion: depth is at most 7 whatever the argument.""" + r = self.rng + t = self.int_type() + fn = Func('r%d' % n, t, [], 'rec') + c = self.ctx(fn, True) + cnt = Decl('x0', U8, 'param') + acc = Decl('x1', t, 'param', cname=self.spelled(t)) + fn.params += [cnt, acc] + c.ints += [cnt, acc] + c.prot.add(cnt) + var = lambda d: self.mk('var', a=d) + low = lambda: self.mk('bin', [var(cnt), self.small(7, 7)], '&') + stop = Block() + stop.stmts.append(S('ret', [var(acc)])) + fn.body.stmts.append(S('if', [self.mk('bin', [low(), self.small(0, 0)], '==')], [stop])) + c.fresh() + nxt = self.mk('cast', [self.mk('bin', [low(), self.small(1, 1)], '-')], t=U8) + fn.body.stmts.append(S('ret', [self.mk('call', [nxt, self.gen_expr(c, 2)], a=fn)])) + return fn + + def gen_impure(self, n): + r = self.rng + x = r.random() + if x < 0.15: + ret = None + elif x < 0.3 and self.on('struct') and self.on('structval'): + ret = r.choice(self.prog.structs) + else: + ret = self.int_type() + fn = Func('k%d' % n, ret, [], 'impure') + c = self.ctx(fn, False) + self.params_for(c, fn, r.randint(1, 4), True) + self.locals_for(c, fn, r.randint(0, 2)) + for _ in range(r.randint(1, 4)): + self.gen_stmt(c, 1, fn.body) + if ret is not None and not is_int(ret): + srcs = [d for d in c.svars if d.t is ret] + if not srcs: + d = Decl(c.name('t'), ret, 'local', init=self.init_value(ret, ())) + fn.body.decls.append(d) + srcs = [d] + fn.body.stmts.append(S('ret', [self.mk('var', a=r.choice(srcs))])) + else: + self.finish(c, fn) + return fn + + def gen_test(self, n): + r = self.rng + fn = Func('f%d' % n, self.int_type(), [], 'test') + fn.static = r.random() < 0.7 + c = self.ctx(fn, False) + self.params_for(c, fn, r.randint(0, 5), True) + self.locals_for(c, fn, r.randint(2, 6)) + for _ in range(r.randint(max(2, self.size // 2), self.size)): + self.gen_stmt(c, self.depth, fn.body) + self.finish(c, fn) + for _ in range(3): + args = [] + for p in fn.params: + if isinstance(p.t, PtrType): + args.append(self.ptr_arg(c, p, constant=True)) + elif isinstance(p.t, StructType): + cands = [g for g in self.prog.globals if g.t is p.t] + g = r.choice(cands) + if g.dims: + args.append(self.mk('idx', [self.small(0, g.dims[0] - 1)], a=g)) + else: + args.append(self.mk('var', a=g)) + else: + t = p.t if p.t.bits >= 16 else (I16 if p.t.signed else U16) + v = p.t.wrap(self.value(p.t)) + args.append(self.mk('const', a=t.wrap(v), t=t)) + fn.inputs.append(args) + return fn + + def generate(self): + r = self.rng + self.gen_structs() + self.gen_globals() + funcs = self.prog.funcs + for n in range(r.randint(2, 4)): + fn = self.gen_pure(n) if n or r.random() < 0.6 or not self.on('rec') \ + else self.gen_rec(n) + funcs.append(fn) + self.pure_fns.append(fn) + for n in range(r.randint(1, 3)): + fn = self.gen_impure(n) + funcs.append(fn) + (self.struct_fns if isinstance(fn.ret, StructType) else self.impure_fns).append(fn) + if isinstance(fn.ret, StructType): + self.impure_fns.append(fn) + for n in range(self.n_tests): + funcs.append(self.gen_test(n)) + return self.prog + + +def generate(seed, tests=6, size=10, depth=3, off=frozenset()): + return Gen(seed, tests, size, depth, off).generate() + + +def render(prog, checked=False, title=''): + return Renderer(checked, prog.off).program(prog, title) diff --git a/scripts/run-known-bugs.py b/scripts/run-known-bugs.py new file mode 100644 index 000000000..30f228c6c --- /dev/null +++ b/scripts/run-known-bugs.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Check the parked reproducers in tests/known-bugs/. + +Each tests/known-bugs/.c reproduces one entry in compiler-bugs.md and +has the output it should produce in tests/known-bugs/baselines/.txt. +They are parked there, outside the suite scripts/runall.py runs, because dcc +currently fails them. + +For every reproducer this script: + + 1. builds it with the host compiler (the flags validate-unit-test.ps1 uses) + and requires the output to equal the baseline, and requires a build with + ASan and UBSan to agree, so the baseline is proven and the test defined; + 2. builds and runs it with dcc, with and without dccpeep, and reports + whether dcc still fails it. + + run-known-bugs.py check every reproducer + run-known-bugs.py bug003 check some + run-known-bugs.py --update rewrite baselines from the host compiler + +A reproducer reported FIXED passes in both modes: move it and its baseline to +tests/ and tests/baselines/, add a tests/perf_baselines.csv row, and close the +entry in compiler-bugs.md. The exit status is 1 only when a baseline is not +proven by the host compiler. +""" +from __future__ import annotations + +import argparse +import concurrent.futures +import os +import shutil +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +BUGS = REPO / 'tests' / 'known-bugs' +HOST_FLAGS = ['-std=gnu99', '-w', '-fsigned-char', '-fno-common'] +SANITIZE = ['-O1', '-fsanitize=address,undefined', '-fno-sanitize-recover=all'] + + +def host_output(source, work, flags, timeout): + exe = work / 'host' + r = subprocess.run(['clang', *HOST_FLAGS, *flags, str(source), '-o', str(exe)], + capture_output=True, text=True) + if r.returncode: + return None, 'host build failed: ' + r.stderr.strip()[:200] + try: + r = subprocess.run([str(exe)], capture_output=True, text=True, timeout=timeout, + stdin=subprocess.DEVNULL) + except subprocess.TimeoutExpired: + return None, 'host run timed out' + if r.returncode: + return None, 'host run failed: ' + r.stderr.strip()[-200:] + return r.stdout, None + + +def dcc_verdict(source, work, peep, expected, args): + build = work / ('peep' if peep else 'nopeep') + shutil.rmtree(build, ignore_errors=True) + cmd = [str(REPO / 'dccmake'), 'dcc-input=%s' % source, 'dcc-output=T', + 'dcc-build-dir=%s' % build, 'dcc-peep=%s' % ('true' if peep else 'false'), + 'dcc-stack-check=true', 'dcc-floatio=true', 'dcc-flongio=true', + 'ntvcm-tool=%s' % args.emulator] + if args.dcc: + cmd.append('dcc-tool=%s' % args.dcc.resolve()) + r = subprocess.run(cmd, cwd=REPO, stdin=subprocess.DEVNULL, capture_output=True, text=True) + log = r.stdout + r.stderr + notes = [ln.strip() for ln in log.splitlines() + if not ln.startswith('+') + and ('error DCC' in ln or 'dcc: fatal' in ln or ln.startswith('; MIR '))] + if r.returncode: + text = notes[0] if notes else 'build failed' + return 'does not compile: ' + text.replace(str(source) + ':', 'line ')[:110] + try: + run = subprocess.run([args.emulator, '-s:0', 'T.COM'], cwd=build, + stdin=subprocess.DEVNULL, capture_output=True, timeout=args.timeout) + except subprocess.TimeoutExpired: + return 'hangs (over %d s)' % args.timeout + output = run.stdout.decode('latin-1').replace('\r', '') + if output == expected: + return 'pass' + want, got = expected.splitlines(), output.splitlines() + wrong = sum(1 for w, g in zip(want, got) if w != g) + abs(len(want) - len(got)) + note = '; dcc reported: ' + notes[0][:60] if notes else '' + return 'wrong output (%d of %d lines)%s' % (wrong, len(want), note) + + +def check(name, args): + source = BUGS / (name + '.c') + baseline = BUGS / 'baselines' / (name + '.txt') + work = args.work / name + shutil.rmtree(work, ignore_errors=True) + work.mkdir(parents=True) + out, err = host_output(source, work, ['-O2'], args.timeout) + if out is not None: + # unsequenced modifications and missing returns are undefined but + # invisible to the sanitizers; clang diagnoses them at compile time + lint = subprocess.run(['clang', '-std=gnu99', '-fsigned-char', '-fsyntax-only', + '-Wno-everything', '-Werror=unsequenced', '-Werror=return-type', + str(source)], capture_output=True, text=True) + if lint.returncode: + out, err = None, 'not well defined: ' + lint.stderr.strip().splitlines()[0][-150:] + if out is not None: + sanitized, err = host_output(source, work, SANITIZE, args.timeout) + if sanitized is not None and sanitized != out: + out, err = None, 'sanitized host build prints different output' + elif sanitized is None: + out = None + if out is None: + return name, 'UNPROVEN', err, [] + if args.update: + baseline.parent.mkdir(exist_ok=True) + baseline.write_text(out) + if not baseline.exists(): + return name, 'UNPROVEN', 'no baseline (run with --update)', [] + if baseline.read_text() != out: + return name, 'UNPROVEN', 'baseline differs from the host compiler output', [] + verdicts = [dcc_verdict(source, work, peep, out, args) for peep in (False, True)] + shutil.rmtree(work, ignore_errors=True) + if all(v == 'pass' for v in verdicts): + return name, 'FIXED', '', verdicts + return name, 'fails', '', verdicts + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('names', nargs='*', help='reproducers to check (default: all)') + parser.add_argument('--update', action='store_true', + help='rewrite baselines from the host compiler output') + parser.add_argument('--emulator', default='ntvcm') + parser.add_argument('--dcc', type=Path, + help='another dcc build to check instead of the one in the repo root') + parser.add_argument('--timeout', type=int, default=20, help='seconds to run a program') + parser.add_argument('--work', type=Path, default=REPO / 'build' / 'known-bugs') + parser.add_argument('--jobs', type=int, default=os.cpu_count() or 1) + args = parser.parse_args() + names = args.names or sorted(p.stem for p in BUGS.glob('*.c')) + missing = [n for n in names if not (BUGS / (n + '.c')).exists()] + if missing: + parser.error('no such reproducer: ' + ', '.join(missing)) + counts = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool: + for name, status, note, verdicts in pool.map(lambda n: check(n, args), names): + counts[status] = counts.get(status, 0) + 1 + if status == 'UNPROVEN': + print('%-10s UNPROVEN %s' % (name, note)) + elif status == 'FIXED': + print('%-10s FIXED passes with and without dccpeep' % name) + elif verdicts[0] == verdicts[1]: + print('%-10s fails %s' % (name, verdicts[0])) + else: + print('%-10s fails nopeep: %s; peep: %s' % (name, verdicts[0], verdicts[1])) + print('%d reproducers: %s' % (len(names), ', '.join( + '%d %s' % (n, s) for s, n in sorted(counts.items())))) + return 1 if counts.get('UNPROVEN') else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/README.md b/tests/README.md index 5898b1005..1da439926 100644 --- a/tests/README.md +++ b/tests/README.md @@ -19,8 +19,17 @@ tests/ .txt # expected normalized dcc stderr/stdout extended-tests/ c-testsuite/ # imported c89/c99/c11 single-exec corpus + known-bugs/ + bugNNN.c # reproducer for an open compiler bug; expected to fail + baselines/ + bugNNN.txt # what it should print (host compiler output) ``` +`known-bugs/` is not part of the suite: the runners only build `tests/*.c`. +Run `python3 scripts/run-known-bugs.py` to check those reproducers, and see +[known-bugs/README.md](known-bugs/README.md) and +[compiler-bugs.md](../compiler-bugs.md). + ## How tests and baselines relate Each test program `tests/.c` has a **matching baseline file** diff --git a/tests/baselines/tblkdiv.txt b/tests/baselines/tblkdiv.txt new file mode 100644 index 000000000..5504ce025 --- /dev/null +++ b/tests/baselines/tblkdiv.txt @@ -0,0 +1,4 @@ +member / 1 610804097 +element member / 1 610804097 +pointer member / 3 203601365 +pointer member % 1000 -97 diff --git a/tests/baselines/tcastasg.txt b/tests/baselines/tcastasg.txt new file mode 100644 index 000000000..2696f33e2 --- /dev/null +++ b/tests/baselines/tcastasg.txt @@ -0,0 +1,4 @@ +value 75124 +condition 1484 +switch 12 +comparison 1 diff --git a/tests/baselines/twcasgm.txt b/tests/baselines/twcasgm.txt new file mode 100644 index 000000000..649022c42 --- /dev/null +++ b/tests/baselines/twcasgm.txt @@ -0,0 +1,4 @@ +member via pointer 205 297 +member 0 60 +array 59 -4167 205 4767 +pointer 7 3 diff --git a/tests/known-bugs/README.md b/tests/known-bugs/README.md new file mode 100644 index 000000000..d699241bb --- /dev/null +++ b/tests/known-bugs/README.md @@ -0,0 +1,52 @@ +# Known-bug reproducers + +Each `bugNNN.c` here reproduces one open entry in +[compiler-bugs.md](../../compiler-bugs.md) and is expected to **fail** with the +current compiler: it is rejected, hangs, or prints the wrong output. They are +kept out of `tests/*.c` so the regression suite (`scripts/runall.py`, +`scripts/runall.ps1`) stays green; those runners only look at `tests/*.c`. + +`baselines/bugNNN.txt` is the output the program should print. It is the host +compiler's output, not dcc's. + +## Checking them + +```sh +python3 scripts/run-known-bugs.py # every reproducer +python3 scripts/run-known-bugs.py bug003 # one +``` + +For each reproducer the script first proves the baseline: the program must +build with the host compiler (the flags `validate-unit-test.ps1` uses), print +exactly the baseline, and print the same under ASan and UBSan. It then builds +and runs the program with dcc, with and without `dccpeep`, and reports +`fails` with the reason, or `FIXED`. + +`--dcc PATH` checks another compiler build, for example an older one when +dating a regression. + +## When a bug is fixed + +A reproducer reported `FIXED` passes in both modes. Promote it to a regular +test: + +1. Move `bugNNN.c` to `tests/.c` and `baselines/bugNNN.txt` to + `tests/baselines/.txt`, with a name that says what it covers, and + rewrite the header comment to describe the coverage instead of the defect. +2. Add its row to `tests/perf_baselines.csv`. +3. Confirm with `pwsh ./scripts/validate-unit-test.ps1 -App ` and + `python3 ./scripts/runall.py -apps -mode=full`. +4. Close the entry in `compiler-bugs.md`, and remove the bug's feature from + `KNOWN_BUGS` in `scripts/clang_oracle_gen.py` so the fuzzer generates that + form again. + +`tests/twcasgm.c`, `tests/tblkdiv.c` and `tests/tcastasg.c` were promoted this +way. + +## Adding one + +Write a small program that prints one line per shape of the bug, using +fixed-width types (``) so the result does not depend on `int` being +16 or 32 bits, then run `python3 scripts/run-known-bugs.py --update bugNNN` to +write the baseline from the host compiler. The script refuses a program that +ASan or UBSan objects to. diff --git a/tests/known-bugs/baselines/bug002.txt b/tests/known-bugs/baselines/bug002.txt new file mode 100644 index 000000000..dbec25526 --- /dev/null +++ b/tests/known-bugs/baselines/bug002.txt @@ -0,0 +1,4 @@ +from array 0 +from constant 0 +after add 160 +local count 0 diff --git a/tests/known-bugs/baselines/bug003.txt b/tests/known-bugs/baselines/bug003.txt new file mode 100644 index 000000000..b41905ef3 --- /dev/null +++ b/tests/known-bugs/baselines/bug003.txt @@ -0,0 +1,12 @@ +signed ++ 80000001 +signed ++ 00010000 +signed ++ 00008000 +signed ++ 00011171 +unsigned ++ 80000001 +unsigned ++ 00010000 +unsigned ++ 00008000 +unsigned ++ 00011171 +pointer -- 80000000 +pointer -- 0000fffe +pointer -- ffffffff +pointer -- 0000ffff diff --git a/tests/known-bugs/baselines/bug004.txt b/tests/known-bugs/baselines/bug004.txt new file mode 100644 index 000000000..612ee8e1d --- /dev/null +++ b/tests/known-bugs/baselines/bug004.txt @@ -0,0 +1,4 @@ +|| to member 00000001 +&& to element 00000001 +|| to global 00000000 +constant || 00000001 diff --git a/tests/known-bugs/baselines/bug006.txt b/tests/known-bugs/baselines/bug006.txt new file mode 100644 index 000000000..b6b255fe3 --- /dev/null +++ b/tests/known-bugs/baselines/bug006.txt @@ -0,0 +1,4 @@ +%= constant 4 48 +%= variable 18 1148 +/= 0 164 +neighbours 33407 -11 33407 -11 diff --git a/tests/known-bugs/baselines/bug007.txt b/tests/known-bugs/baselines/bug007.txt new file mode 100644 index 000000000..d536ffb8f --- /dev/null +++ b/tests/known-bugs/baselines/bug007.txt @@ -0,0 +1,3 @@ +narrowing 3 +widening 6 +for-init 15 diff --git a/tests/known-bugs/baselines/bug008.txt b/tests/known-bugs/baselines/bug008.txt new file mode 100644 index 000000000..1d0c34a7a --- /dev/null +++ b/tests/known-bugs/baselines/bug008.txt @@ -0,0 +1,2 @@ +unsigned 1 65533 +signed -70000 diff --git a/tests/known-bugs/baselines/bug009.txt b/tests/known-bugs/baselines/bug009.txt new file mode 100644 index 000000000..142dc5d0c --- /dev/null +++ b/tests/known-bugs/baselines/bug009.txt @@ -0,0 +1,3 @@ +signed 6 6 +unsigned 6 12 +global bound 6 18 diff --git a/tests/known-bugs/baselines/bug010.txt b/tests/known-bugs/baselines/bug010.txt new file mode 100644 index 000000000..b712462b0 --- /dev/null +++ b/tests/known-bugs/baselines/bug010.txt @@ -0,0 +1,4 @@ +cast of post-increment 74465 +cast of post-decrement 139999 +argument 345 +returned 44 diff --git a/tests/known-bugs/baselines/bug011.txt b/tests/known-bugs/baselines/bug011.txt new file mode 100644 index 000000000..52c4862cf --- /dev/null +++ b/tests/known-bugs/baselines/bug011.txt @@ -0,0 +1,4 @@ +5 1 97 | 4 1 | 4 1 97 +6 2 66 | 5 1 | 5 2 98 +38 -2 99 | 7 3 | 18 6 227 +14 4 0 | 7 4 | 7 4 0 diff --git a/tests/known-bugs/baselines/bug012.txt b/tests/known-bugs/baselines/bug012.txt new file mode 100644 index 000000000..a73a090c6 --- /dev/null +++ b/tests/known-bugs/baselines/bug012.txt @@ -0,0 +1,2 @@ +negated -1 +complemented -3 diff --git a/tests/known-bugs/baselines/bug013.txt b/tests/known-bugs/baselines/bug013.txt new file mode 100644 index 000000000..c7814faf6 --- /dev/null +++ b/tests/known-bugs/baselines/bug013.txt @@ -0,0 +1,13 @@ +unary -444 -445 0 +binary 0 -653 +binary 1 -653 +binary 2 -28 +binary 3 -653 +binary 4 -308 +binary 5 -827 +binary 6 -997 +binary 7 -827 +binary 8 -308 +conditional 0 -345 +conditional 1 -654 +conditional 2 9 diff --git a/tests/known-bugs/baselines/bug014.txt b/tests/known-bugs/baselines/bug014.txt new file mode 100644 index 000000000..44b9bd35d --- /dev/null +++ b/tests/known-bugs/baselines/bug014.txt @@ -0,0 +1,4 @@ +post-increment 70001 70001 +add one 6 6 +assigned pre-increment 8 8 +after goto 10 10 diff --git a/tests/known-bugs/baselines/bug015.txt b/tests/known-bugs/baselines/bug015.txt new file mode 100644 index 000000000..a7d669298 --- /dev/null +++ b/tests/known-bugs/baselines/bug015.txt @@ -0,0 +1,4 @@ +multiply 0003ae4e +compare char 0 +compare int 0 +byte local 1 diff --git a/tests/known-bugs/baselines/bug016.txt b/tests/known-bugs/baselines/bug016.txt new file mode 100644 index 000000000..c1de8091e --- /dev/null +++ b/tests/known-bugs/baselines/bug016.txt @@ -0,0 +1,9 @@ +unsigned from signed 34280 +signed from unsigned -15536 +compared 1 +from cast 1 +remainder 14 +divided -16666 +from constant 65436 +same-width cast 34280 +same-width cast long -3 diff --git a/tests/known-bugs/baselines/bug017.txt b/tests/known-bugs/baselines/bug017.txt new file mode 100644 index 000000000..4e8fc004f --- /dev/null +++ b/tests/known-bugs/baselines/bug017.txt @@ -0,0 +1,2 @@ +and self 1 1234 +and self long 40000 diff --git a/tests/known-bugs/baselines/bug018.txt b/tests/known-bugs/baselines/bug018.txt new file mode 100644 index 000000000..1ce3af3ef --- /dev/null +++ b/tests/known-bugs/baselines/bug018.txt @@ -0,0 +1,4 @@ +bit-field 384 -1 +bit-field via pointer 29 +array member 300 4162 35533 34816 +element of struct array 4767 304 diff --git a/tests/known-bugs/baselines/bug019.txt b/tests/known-bugs/baselines/bug019.txt new file mode 100644 index 000000000..8e6086d2d --- /dev/null +++ b/tests/known-bugs/baselines/bug019.txt @@ -0,0 +1,7 @@ +after return 1 +for-init after return 2 +after break 1 +after goto 4 +in switch 5 +in while 6 +dead code ran 0 times diff --git a/tests/known-bugs/baselines/bug020.txt b/tests/known-bugs/baselines/bug020.txt new file mode 100644 index 000000000..614209dc7 --- /dev/null +++ b/tests/known-bugs/baselines/bug020.txt @@ -0,0 +1,6 @@ +remainder >= 0 1 +quotient < 1 0 +product >= 0 1 +member >= 1 1 +as value 0 1 +switch arms taken 8 diff --git a/tests/known-bugs/baselines/bug021.txt b/tests/known-bugs/baselines/bug021.txt new file mode 100644 index 000000000..360a62077 --- /dev/null +++ b/tests/known-bugs/baselines/bug021.txt @@ -0,0 +1,4 @@ +returned x++ 9029 +returned x-- 9029 +assigned x++ 9029 +through pointer 9029 diff --git a/tests/known-bugs/bug002.c b/tests/known-bugs/bug002.c new file mode 100644 index 000000000..62bfff00d --- /dev/null +++ b/tests/known-bugs/bug002.c @@ -0,0 +1,47 @@ +/* BUG-002: an unsigned char local shifted left by an unsigned char variable + * is not truncated to 8 bits when it is returned from a function returning + * unsigned char. 200 << 5 is 6400, which is 0 as an unsigned char. */ +#include +#include + +static uint8_t a1[4] = { 200, 2, 3, 4 }; +static uint8_t count = 5; + +static uint8_t from_array(void) +{ + uint8_t x = a1[0]; + x <<= count; + return x; +} + +static uint8_t from_constant(void) +{ + uint8_t x = 200; + x <<= count; + return x; +} + +static uint8_t after_add(void) +{ + uint8_t x = a1[0]; + x += count; + x <<= count; + return x; +} + +static uint8_t local_count(void) +{ + uint8_t x = a1[0]; + uint8_t n = count; + x <<= n; + return x; +} + +int main(void) +{ + printf("from array %u\n", (unsigned)from_array()); + printf("from constant %u\n", (unsigned)from_constant()); + printf("after add %u\n", (unsigned)after_add()); + printf("local count %u\n", (unsigned)local_count()); + return 0; +} diff --git a/tests/known-bugs/bug003.c b/tests/known-bugs/bug003.c new file mode 100644 index 000000000..e48289c9f --- /dev/null +++ b/tests/known-bugs/bug003.c @@ -0,0 +1,54 @@ +/* BUG-003: ++ and -- on a 32-bit array element are carried out in 16 bits + * and the result sign-extended, so any element whose value leaves the + * 16-bit signed range is corrupted. `a[k] += 1` is correct. */ +#include +#include + +static int32_t a[4] = { (-2147483647 - 1), 65535, 32767, 70000 }; +static uint32_t u[4] = { 0x80000000, 65535, 32767, 70000 }; +static int32_t d[4] = { -2147483647, 65535, 0, 65536 }; +static int16_t idx = 1; + +static void show(const char *name, uint32_t v) +{ + printf("%s %04x%04x\n", name, (unsigned)(v >> 16), (unsigned)(v & 0xffff)); +} + +static void increment(void) +{ + ++a[0]; + a[1]++; + ++a[2]; + a[idx + 2]++; +} + +static void increment_unsigned(void) +{ + ++u[0]; + u[idx]++; + u[2]++; + ++u[3]; +} + +static void decrement(int32_t *p) +{ + p[0]--; + --p[1]; + p[2]--; + --p[3]; +} + +int main(void) +{ + uint8_t i; + increment(); + increment_unsigned(); + decrement(d); + for (i = 0; i < 4; i++) + show("signed ++", (uint32_t)a[i]); + for (i = 0; i < 4; i++) + show("unsigned ++", u[i]); + for (i = 0; i < 4; i++) + show("pointer --", (uint32_t)d[i]); + return 0; +} diff --git a/tests/known-bugs/bug004.c b/tests/known-bugs/bug004.c new file mode 100644 index 000000000..a4045531f --- /dev/null +++ b/tests/known-bugs/bug004.c @@ -0,0 +1,54 @@ +/* BUG-004: an unsigned 16-bit local initialised from && or ||, then stored + * to a 32-bit lvalue, leaves garbage in the high word. Storing the logical + * result directly, or a local initialised from anything else, is correct. */ +#include +#include + +struct S { uint16_t m0; uint32_t m1; }; +static struct S s = { 255, 0x5dd1d183 }; +static uint32_t u[2] = { 0x80000000, 0x12345678 }; +static uint32_t g = 0xdeadbeef; +static uint16_t one = 1; +static uint16_t zero = 0; + +static void show(const char *name, uint32_t v) +{ + printf("%s %04x%04x\n", name, (unsigned)(v >> 16), (unsigned)(v & 0xffff)); +} + +static void or_to_member(void) +{ + uint16_t l = one || zero; + s.m1 = l; +} + +static void and_to_element(void) +{ + uint16_t l = one && one; + u[0] = l; +} + +static void or_to_global(void) +{ + uint16_t l = zero || zero; + g = l; +} + +static void constant_or(void) +{ + uint16_t l = ((uint16_t)1) || ((uint16_t)1); + u[1] = l; +} + +int main(void) +{ + or_to_member(); + and_to_element(); + or_to_global(); + constant_or(); + show("|| to member", s.m1); + show("&& to element", u[0]); + show("|| to global", g); + show("constant ||", u[1]); + return 0; +} diff --git a/tests/known-bugs/bug006.c b/tests/known-bugs/bug006.c new file mode 100644 index 000000000..ed4004aa1 --- /dev/null +++ b/tests/known-bugs/bug006.c @@ -0,0 +1,41 @@ +/* BUG-006: /= and %= on an unsigned bit-field store the wrong value. + * `bf = bf % x` and += are correct, as is %= on a signed bit-field. */ +#include +#include + +struct S { uint16_t m1; uint16_t m2 : 11; int16_t m4 : 6; uint16_t m5 : 5; }; +static struct S a = { 0x827f, 1148, -11, 18 }; +static struct S b = { 0x827f, 1148, -11, 18 }; +static struct S c = { 0x827f, 1148, -11, 18 }; +static int16_t big = 21930; +static uint32_t wide[2] = { 255, 21930 }; + +static void modulo_constant(void) +{ + a.m5 %= 7; + a.m2 %= 100; +} + +static void modulo_variable(uint32_t *p) +{ + b.m5 %= big; + b.m2 %= ((int16_t)p[1]); +} + +static void divide(void) +{ + c.m5 /= big; + c.m2 /= 7; +} + +int main(void) +{ + modulo_constant(); + modulo_variable(wide); + divide(); + printf("%%= constant %u %u\n", (unsigned)a.m5, (unsigned)a.m2); + printf("%%= variable %u %u\n", (unsigned)b.m5, (unsigned)b.m2); + printf("/= %u %u\n", (unsigned)c.m5, (unsigned)c.m2); + printf("neighbours %u %d %u %d\n", (unsigned)a.m1, (int)a.m4, (unsigned)c.m1, (int)c.m4); + return 0; +} diff --git a/tests/known-bugs/bug007.c b/tests/known-bugs/bug007.c new file mode 100644 index 000000000..cc1a98410 --- /dev/null +++ b/tests/known-bugs/bug007.c @@ -0,0 +1,52 @@ +/* BUG-007: a call through a function pointer whose argument is a local + * declared in a nested block (or in a for-init) and needs converting to the + * parameter type makes the MIR verifier report "incorrect argument ABI type". + * Depending on the surrounding code the compile then fails ("MIR verification + * failed before required emission"), or dcc exits 0 and the program crashes. + * The direct call, an explicit cast, and a function-scope local all work. */ +#include +#include + +static uint16_t seen; +static int32_t wide = 3; +static int16_t narrow = 3; + +static int16_t take_byte(unsigned char x) { seen += x; return 1; } +static int16_t (*take_byte_p)(unsigned char) = take_byte; +static int16_t take_long(int32_t x) { seen += (uint16_t)x; return 1; } +static int16_t (*take_long_p)(int32_t) = take_long; + +static void narrowing(void) +{ + { + int32_t v = wide; + (*take_byte_p)(v); + } +} + +static void widening(void) +{ + { + int16_t v = narrow; + take_long_p(v); + } +} + +static void for_init(void) +{ + for (int32_t i = 3; i != 0; --i) { + if ((*take_byte_p)(i)) + seen++; + } +} + +int main(void) +{ + narrowing(); + printf("narrowing %u\n", (unsigned)seen); + widening(); + printf("widening %u\n", (unsigned)seen); + for_init(); + printf("for-init %u\n", (unsigned)seen); + return 0; +} diff --git a/tests/known-bugs/bug008.c b/tests/known-bugs/bug008.c new file mode 100644 index 000000000..4f35a6c29 --- /dev/null +++ b/tests/known-bugs/bug008.c @@ -0,0 +1,35 @@ +/* BUG-008: assigning a negative constant to a 32-bit local declared in a + * nested block stops the compile with "MIR emission is required". A + * function-scope local, or a positive constant, compiles. */ +#include +#include + +static uint16_t low; + +static int32_t unsigned_local(void) +{ + { + uint32_t b = 201; + b = -3; + low = (uint16_t)b; + } + return 1; +} + +static int32_t signed_local(void) +{ + int32_t r = 0; + { + int32_t b = 201; + b = -70000; + r = b; + } + return r; +} + +int main(void) +{ + printf("unsigned %ld %u\n", (long)unsigned_local(), (unsigned)low); + printf("signed %ld\n", (long)signed_local()); + return 0; +} diff --git a/tests/known-bugs/bug009.c b/tests/known-bugs/bug009.c new file mode 100644 index 000000000..fcdbec50b --- /dev/null +++ b/tests/known-bugs/bug009.c @@ -0,0 +1,56 @@ +/* BUG-009: a do-while loop nested inside another loop never exits when its + * condition is `d < n` with a 32-bit `d`: the comparison is always true. + * This program hangs. The same loop on its own, a while loop, `d != n`, or a + * 16-bit `d` all terminate. */ +#include +#include + +static uint16_t passes; +static int32_t limit = 3; + +static int32_t signed_counter(void) +{ + int32_t i, d = 0, n = 0; + for (i = 0; i < 4; i++) { + n = limit; + do { + passes++; + ++d; + } while (d < n); + } + return d; +} + +static int32_t unsigned_counter(void) +{ + uint32_t d = 0, n = 0; + int16_t i; + for (i = 6; i != 2; --i) { + n = (uint32_t)limit; + do { + passes++; + ++d; + } while (d < n); + } + return (int32_t)d; +} + +static int32_t global_bound(void) +{ + int32_t i, d = 0; + for (i = 6; i != 2; --i) { + do { + passes++; + ++d; + } while (d < limit); + } + return d; +} + +int main(void) +{ + printf("signed %ld %u\n", (long)signed_counter(), (unsigned)passes); + printf("unsigned %ld %u\n", (long)unsigned_counter(), (unsigned)passes); + printf("global bound %ld %u\n", (long)global_bound(), (unsigned)passes); + return 0; +} diff --git a/tests/known-bugs/bug010.c b/tests/known-bugs/bug010.c new file mode 100644 index 000000000..19f5842a7 --- /dev/null +++ b/tests/known-bugs/bug010.c @@ -0,0 +1,47 @@ +/* BUG-010: narrowing the value of a 32-bit post-increment or post-decrement + * is rejected with DCC-E1002, whether the narrowing is a cast, a return, or + * an argument conversion. A pre-increment, a 16-bit variable, and the + * unconverted value all compile. */ +#include +#include + +static int16_t take(int8_t a) { return a; } + +static int32_t cast_of_post_increment(void) +{ + int32_t l2 = 70000; + int32_t r; + r = (int16_t)(l2++); + return r + l2; +} + +static int32_t cast_of_post_decrement(void) +{ + uint32_t u2 = 70000; + int32_t r; + r = (int32_t)(u2--); + return r + (int32_t)u2; +} + +static int32_t argument(void) +{ + uint32_t u2 = 300; + int32_t r; + r = take((int8_t)(u2++)); + return r + (int32_t)u2; +} + +static uint8_t returned(void) +{ + int32_t l3 = 300; + return l3++; +} + +int main(void) +{ + printf("cast of post-increment %ld\n", (long)cast_of_post_increment()); + printf("cast of post-decrement %ld\n", (long)cast_of_post_decrement()); + printf("argument %ld\n", (long)argument()); + printf("returned %u\n", (unsigned)returned()); + return 0; +} diff --git a/tests/known-bugs/bug011.c b/tests/known-bugs/bug011.c new file mode 100644 index 000000000..be43eefb2 --- /dev/null +++ b/tests/known-bugs/bug011.c @@ -0,0 +1,50 @@ +/* BUG-011: compound assignment to an element of a char, signed char or + * unsigned char array that is a struct member is rejected with DCC-E1002, + * for every operator and for constant and variable indexes. `s.b[i]++`, + * `s.b[i] = s.b[i] ^ 1`, a plain byte array, and a 16-bit array member all + * compile. */ +#include +#include + +struct B { uint8_t b[4]; int8_t c[4]; char name[4]; }; +static struct B s = { { 4, 5, 6, 7 }, { 1, 2, 3, 4 }, "abc" }; +static struct B sa[2]; +static int16_t idx = 2; + +static void direct(void) +{ + s.b[0] ^= 1; + s.b[1] += 1; + s.b[idx] |= 0x20; + s.b[3] <<= 1; + s.c[2] -= 5; + s.name[1] &= 0x5f; +} + +static void through_pointer(struct B *p) +{ + p->b[2] *= 3; + p->c[idx] += 3; + p->name[idx] |= 0x80; +} + +static void element(void) +{ + sa[1].b[2] += 1; + sa[1].c[idx - 1] -= 1; +} + +int main(void) +{ + uint8_t i; + sa[0] = s; + sa[1] = s; + direct(); + element(); + through_pointer(&sa[0]); + for (i = 0; i < 4; i++) + printf("%u %d %u | %u %d | %u %d %u\n", (unsigned)s.b[i], (int)s.c[i], + (unsigned)(uint8_t)s.name[i], (unsigned)sa[1].b[i], (int)sa[1].c[i], + (unsigned)sa[0].b[i], (int)sa[0].c[i], (unsigned)(uint8_t)sa[0].name[i]); + return 0; +} diff --git a/tests/known-bugs/bug012.c b/tests/known-bugs/bug012.c new file mode 100644 index 000000000..2aac41321 --- /dev/null +++ b/tests/known-bugs/bug012.c @@ -0,0 +1,27 @@ +/* BUG-012: unary - or ~ applied to ! of a constant is rejected with + * DCC-E1002. The same operators applied to ! of a variable compile. */ +#include +#include + +static int32_t negated(void) +{ + int32_t r; + r = -(!(-128)); + r += -(!0); + return r; +} + +static int32_t complemented(void) +{ + int32_t r; + r = ~(!15); + r += ~(!0); + return r; +} + +int main(void) +{ + printf("negated %ld\n", (long)negated()); + printf("complemented %ld\n", (long)complemented()); + return 0; +} diff --git a/tests/known-bugs/bug013.c b/tests/known-bugs/bug013.c new file mode 100644 index 000000000..b9ecda6b2 --- /dev/null +++ b/tests/known-bugs/bug013.c @@ -0,0 +1,57 @@ +/* BUG-013: an unsigned bit-field narrower than int must promote to int + * (C89 3.2.1.1, C99 6.3.1.1), and dcc does so for `bf - 1000` written + * directly. But once the bit-field is the operand of unary - or ~, of any + * other binary operator, or an arm of ?:, the result is typed unsigned int: + * it is zero-extended when widened to long and compared as unsigned. */ +#include +#include + +struct S { uint16_t m0 : 9; uint16_t m1 : 14; uint16_t m2 : 11; }; +static struct S s = { 444, 9817, 346 }; +static int16_t sel = 1; +static int32_t out[3]; + +static void unary(void) +{ + out[0] = -s.m0; + out[1] = ~s.m0; + out[2] = (1 <= (~s.m1)); +} + +static int32_t binary(int16_t which) +{ + switch (which) { + case 0: return (s.m2 | 1) - 1000; + case 1: return (s.m2 ^ 1) - 1000; + case 2: return (s.m2 & 3) - 30; + case 3: return (s.m2 + 1) - 1000; + case 4: return (s.m2 * 2) - 1000; + case 5: return (s.m2 / 2) - 1000; + case 6: return (s.m2 % 7) - 1000; + case 7: return (s.m2 >> 1) - 1000; + default: return (s.m2 << 1) - 1000; + } +} + +static int32_t conditional(int16_t which) +{ + if (which == 0) + return 1 - (sel ? s.m2 : 1); + if (which == 1) + return (sel ? s.m2 : 1) - 1000; + if ((sel ? (-21931) : s.m1) > 1) + return 5; + return 9; +} + +int main(void) +{ + int16_t i; + unary(); + printf("unary %ld %ld %ld\n", (long)out[0], (long)out[1], (long)out[2]); + for (i = 0; i < 9; i++) + printf("binary %d %ld\n", i, (long)binary(i)); + for (i = 0; i < 3; i++) + printf("conditional %d %ld\n", i, (long)conditional(i)); + return 0; +} diff --git a/tests/known-bugs/bug014.c b/tests/known-bugs/bug014.c new file mode 100644 index 000000000..5c5ae2791 --- /dev/null +++ b/tests/known-bugs/bug014.c @@ -0,0 +1,64 @@ +/* BUG-014: a signed 32-bit local that has two reaching definitions, is then + * incremented by one and stored to a global, stops the compile with + * "missing direct MIR named-home offset". Every spelling of the increment + * fails. Decrementing, adding 2, an unsigned local, or storing to an array + * element all compile. */ +#include +#include + +static int32_t g = 65536; +static int16_t sel = 1; + +static int32_t post_increment(void) +{ + int32_t l = 1; + if (sel) + l = 70000; + l++; + g = l; + return l; +} + +static int32_t add_one(void) +{ + int32_t l = 1; + if (sel) + l = 5; + l += 1; + g = l; + return l; +} + +static int32_t assigned_pre_increment(void) +{ + int32_t l = 1; + switch (sel) { + case 0: + break; + case 1: + l = 7; + break; + } + g = ++l; + return l; +} + +static int32_t after_goto(void) +{ + int32_t l = 1; + if (sel == 0) + goto skip; + l = 9; +skip: + g = (++l); + return l; +} + +int main(void) +{ + printf("post-increment %ld %ld\n", (long)post_increment(), (long)g); + printf("add one %ld %ld\n", (long)add_one(), (long)g); + printf("assigned pre-increment %ld %ld\n", (long)assigned_pre_increment(), (long)g); + printf("after goto %ld %ld\n", (long)after_goto(), (long)g); + return 0; +} diff --git a/tests/known-bugs/bug015.c b/tests/known-bugs/bug015.c new file mode 100644 index 000000000..a54db78bb --- /dev/null +++ b/tests/known-bugs/bug015.c @@ -0,0 +1,64 @@ +/* BUG-015: locals declared in a nested block lose their declared type in + * further shapes. Through a nested-block pointer to a 32-bit object, `*q *= k` + * is computed in 16 bits and `c <= *q` is compared as signed; a nested-block + * unsigned char is compared as unsigned int. Each is correct when the local + * is declared at function scope. */ +#include +#include + +struct S { uint16_t m5 : 6; }; +static struct S s = { 24 }; +static uint32_t a4[8] = { 1, 0xaa55, 7, 100, 21930, 1, 0xf941ce65, 257 }; + +static void multiply_through_pointer(void) +{ + { + uint32_t *q = &a4[4]; + (*q) *= 11; + } +} + +static int32_t compare_through_pointer(void) +{ + signed char c = -1; + int32_t r = 0; + { + uint32_t *q = &a4[0]; + if (c <= (*q)) + r = 7; + } + return r; +} + +static int32_t compare_int_through_pointer(void) +{ + int16_t c = -1; + int32_t r = 0; + { + uint32_t *q = &a4[3]; + if (c <= (*q)) + r = 7; + } + return r; +} + +static int32_t byte_local(void) +{ + signed char l2 = -66; + { + unsigned char b = 1; + if ((b & s.m5) > l2) + return 1; + } + return 3; +} + +int main(void) +{ + multiply_through_pointer(); + printf("multiply %04x%04x\n", (unsigned)(a4[4] >> 16), (unsigned)(a4[4] & 0xffff)); + printf("compare char %ld\n", (long)compare_through_pointer()); + printf("compare int %ld\n", (long)compare_int_through_pointer()); + printf("byte local %ld\n", (long)byte_local()); + return 0; +} diff --git a/tests/known-bugs/bug016.c b/tests/known-bugs/bug016.c new file mode 100644 index 000000000..f1cec8efe --- /dev/null +++ b/tests/known-bugs/bug016.c @@ -0,0 +1,84 @@ +/* BUG-016: a local keeps the signedness of its initialiser instead of its + * declared type. An unsigned 16-bit local initialised from a signed 16-bit + * value is sign-extended when widened to 32 bits (and the reverse), and a + * signed 32-bit local initialised from an unsigned expression divides as + * unsigned. A constant initialiser and a same-width cast to the declared + * type behave the same way. The code is correct when the value is assigned + * in a separate statement. */ +#include +#include + +struct S { int8_t a; int16_t m3; int32_t m0; }; +static struct S s = { -128, -31256, 20 }; +static int8_t a6[2] = { -102, -86 }; +static int32_t a3[2] = { -2, -3 }; +static int16_t gs = -31256; +static uint16_t gu = 50000; + +static int32_t unsigned_from_signed(void) +{ + uint16_t l = gs; + return l; +} + +static int32_t signed_from_unsigned(void) +{ + int16_t l = gu; + return l; +} + +static int32_t compared(void) +{ + uint16_t l = s.m3; + return ((int32_t)1) <= l; +} + +static int32_t from_cast(void) +{ + uint16_t l = (int16_t)s.a; + return ((int32_t)l) > ((uint16_t)1); +} + +static int32_t remainder_of(void) +{ + unsigned short l = (int16_t)a6[0]; + return l % s.m0; +} + +static int32_t divided(uint16_t x) +{ + int32_t l = ((uint32_t)1) | a3[1]; + return x / l; +} + +static int32_t from_constant(void) +{ + unsigned short l = -100; + return l; +} + +static int32_t same_width_cast(int16_t p) +{ + unsigned short l = (uint16_t)p; + return l; +} + +static int32_t same_width_cast_long(uint32_t p) +{ + int32_t l = (int32_t)p; + return l % s.m0; +} + +int main(void) +{ + printf("unsigned from signed %ld\n", (long)unsigned_from_signed()); + printf("signed from unsigned %ld\n", (long)signed_from_unsigned()); + printf("compared %ld\n", (long)compared()); + printf("from cast %ld\n", (long)from_cast()); + printf("remainder %ld\n", (long)remainder_of()); + printf("divided %ld\n", (long)divided(50000)); + printf("from constant %ld\n", (long)from_constant()); + printf("same-width cast %ld\n", (long)same_width_cast(-31256)); + printf("same-width cast long %ld\n", (long)same_width_cast_long(0xfffffffd)); + return 0; +} diff --git a/tests/known-bugs/bug017.c b/tests/known-bugs/bug017.c new file mode 100644 index 000000000..1c046d4ce --- /dev/null +++ b/tests/known-bugs/bug017.c @@ -0,0 +1,24 @@ +/* BUG-017: an unsigned short parameter and-assigned with itself and then + * returned through a widening cast returns 0. `return x;` without the cast, + * `x |= x`, `x &= 3`, and a local copy of the parameter are all correct. */ +#include +#include + +static int16_t and_self(unsigned short x) +{ + x &= x; + return (uint32_t)x; +} + +static int32_t and_self_long(unsigned short x) +{ + x &= x; + return (int32_t)x; +} + +int main(void) +{ + printf("and self %d %d\n", (int)and_self(1), (int)and_self(1234)); + printf("and self long %ld\n", (long)and_self_long(40000)); + return 0; +} diff --git a/tests/known-bugs/bug018.c b/tests/known-bugs/bug018.c new file mode 100644 index 000000000..922e0d41b --- /dev/null +++ b/tests/known-bugs/bug018.c @@ -0,0 +1,56 @@ +/* BUG-018: compound assignment of a 32-bit value to a bit-field, or to an + * element of a 16-bit array that is a struct member, is rejected with + * DCC-E1002. The same statements compile with a 16-bit right operand, with a + * plain member or a 32-bit array member as the target, or written as + * `x = x op y`. */ +#include +#include + +struct S { uint16_t u : 9; int16_t s : 7; int16_t w[2]; uint16_t x[2]; }; +static struct S g = { 444, -15, { 300, -300 }, { 300, 40000 } }; +static struct S h = { 444, -15, { 300, -300 }, { 300, 40000 } }; +static struct S ga[2]; +static uint32_t r = 5; +static int32_t big = 70003; +static int16_t idx = 1; + +static void bit_field(void) +{ + g.u |= r; + g.u += big; + g.s ^= ((int32_t)1); + g.u <<= (big & 3); + g.s >>= (r & 15); +} + +static void bit_field_through_pointer(struct S *p) +{ + p->u -= r; + p->u *= r; + p->u /= r; + p->u %= big; +} + +static void array_member(struct S *p) +{ + g.w[1] += big; + g.w[idx] ^= r; + p->x[1] -= big; + g.x[idx] <<= (r & 7); + ga[idx].w[0] += big; + ga[idx].u &= big; +} + +int main(void) +{ + ga[1] = g; + bit_field(); + bit_field_through_pointer(&h); + array_member(&h); + printf("bit-field %u %d\n", (unsigned)g.u, (int)g.s); + printf("bit-field via pointer %u\n", (unsigned)h.u); + printf("array member %d %d %u %u\n", (int)g.w[0], (int)g.w[1], (unsigned)h.x[1], + (unsigned)g.x[1]); + printf("element of struct array %d %u\n", (int)ga[1].w[0], (unsigned)ga[1].u); + return 0; +} diff --git a/tests/known-bugs/bug019.c b/tests/known-bugs/bug019.c new file mode 100644 index 000000000..ae115719b --- /dev/null +++ b/tests/known-bugs/bug019.c @@ -0,0 +1,101 @@ +/* BUG-019: a declaration in a nested scope (a block-local, or a for-init + * declaration) that is unreachable because a return, break or goto precedes + * it in the same block stops the compile with "MIR emission is required". + * The same dead code compiles at function scope, without the declaration, + * or when the jump is conditional. */ +#include +#include + +static uint16_t low; +static int16_t sel = 1; +static int16_t zero = 0; + +static int32_t after_return(void) +{ + if (sel) { + return 1; + { + int16_t b = 1; + low += (uint16_t)b; + } + } + return 0; +} + +static int32_t for_init_after_return(void) +{ + if (sel) { + return 2; + for (uint8_t k = 3; k < 5; ++k) { + low++; + } + } + return 0; +} + +static int32_t after_break(void) +{ + int32_t r = 0; + int16_t i; + for (i = 0; i < 3; i++) { + r++; + break; + { + uint8_t b = 1; + low += b; + } + } + return r; +} + +static int32_t after_goto(void) +{ + int32_t r = 0; + if (sel) { + goto out; + { + int16_t b = 1; + low += (uint16_t)b; + } + } +out: + r = 4; + return r; +} + +static int32_t in_switch(void) +{ + switch (sel) { + case 1: + return 5; + { + int16_t b = 1; + low += (uint16_t)b; + } + } + return 0; +} + +static int32_t in_while(void) +{ + while (zero == 0) { + return 6; + { + int16_t b = 1; + low += (uint16_t)b; + } + } + return 0; +} + +int main(void) +{ + printf("after return %ld\n", (long)after_return()); + printf("for-init after return %ld\n", (long)for_init_after_return()); + printf("after break %ld\n", (long)after_break()); + printf("after goto %ld\n", (long)after_goto()); + printf("in switch %ld\n", (long)in_switch()); + printf("in while %ld\n", (long)in_while()); + printf("dead code ran %u times\n", (unsigned)low); + return 0; +} diff --git a/tests/known-bugs/bug020.c b/tests/known-bugs/bug020.c new file mode 100644 index 000000000..afee888b2 --- /dev/null +++ b/tests/known-bugs/bug020.c @@ -0,0 +1,82 @@ +/* BUG-020: after a switch statement, a relational comparison between a + * variable and the 32-bit product, quotient or remainder of a 32-bit + * parameter stops the compile with "MIR emission is required". The same + * comparison compiles with no switch before it, with ==, with the operands + * the other way round, or with the parameter copied to a local first. */ +#include +#include + +struct S { uint8_t b[4]; }; +static struct S s = { { 1, 255, 127, 3 } }; +static uint8_t g8 = 3; +static int16_t g16 = 3; +static int32_t g32 = 3; +static int16_t sel = 5; +static uint16_t hits; + +static int32_t remainder_ge(int32_t x) +{ + switch (sel) { + case 5: + hits++; + } + if (g16 >= (x % 115)) + return 1; + return 0; +} + +static int32_t quotient_lt(int32_t x) +{ + switch (sel) { + case 0: + hits++; + break; + default: + break; + } + if (g8 < (x / 115)) + return 1; + return 0; +} + +static int32_t product_ge(int32_t x) +{ + switch (sel) { + case 5: + hits++; + } + if (g32 >= (x * 115)) + return 1; + return 0; +} + +static int32_t member_ge(int32_t x) +{ + switch (sel) { + case 5: + hits++; + } + if (s.b[3] >= (x % g16)) + return 1; + return 0; +} + +static int32_t as_value(int32_t x) +{ + switch (sel) { + case 5: + hits++; + } + return g16 >= (x % 115); +} + +int main(void) +{ + printf("remainder >= %ld %ld\n", (long)remainder_ge(70000), (long)remainder_ge(-70000)); + printf("quotient < %ld %ld\n", (long)quotient_lt(70000), (long)quotient_lt(300)); + printf("product >= %ld %ld\n", (long)product_ge(70000), (long)product_ge(-70000)); + printf("member >= %ld %ld\n", (long)member_ge(70000), (long)member_ge(70001)); + printf("as value %ld %ld\n", (long)as_value(70000), (long)as_value(118)); + printf("switch arms taken %u\n", (unsigned)hits); + return 0; +} diff --git a/tests/known-bugs/bug021.c b/tests/known-bugs/bug021.c new file mode 100644 index 000000000..71264575b --- /dev/null +++ b/tests/known-bugs/bug021.c @@ -0,0 +1,58 @@ +/* BUG-021: the value of a post-increment or post-decrement is the new value + * instead of the old one. It happens for a signed 16-bit local that was + * assigned from an element of a local unsigned 16-bit array, in a function + * that also has a signed char local initialised from a narrowing cast. + * The value is correct when that char local is absent or is a constant, when + * the array is static, or when the incremented local is unsigned. */ +#include +#include + +static uint8_t a0[8] = { 16, 76, 156, 127, 255, 188, 0, 57 }; + +static uint16_t returned_increment(void) +{ + signed char l1 = (int8_t)(1 - a0[1]); + int16_t l3 = 1; + uint16_t v6[2] = { 0xffde, 9029 }; + l3 = v6[1]; + return l3++; +} + +static uint16_t returned_decrement(void) +{ + signed char l1 = (int8_t)(1 - a0[1]); + int16_t l3 = 1; + uint16_t v6[2] = { 0xffde, 9029 }; + l3 = v6[1]; + return l3--; +} + +static uint16_t assigned_increment(void) +{ + signed char l1 = (int8_t)(((int32_t)1) - a0[1]); + int16_t l3 = 1; + uint16_t v6[8] = { 0xffde, 0xaa55, 9029, 0xff85, 129, 0xfffe, 1, 127 }; + uint16_t r; + l3 = v6[2]; + r = l3++; + return r; +} + +static uint16_t through_pointer(void) +{ + signed char l1 = (int8_t)(((int32_t)1) - a0[1]); + int16_t l3 = 1; + uint16_t v6[8] = { 0xffde, 0xaa55, 9029, 0xff85, 129, 0xfffe, 1, 127 }; + uint16_t *p7 = &v6[2]; + l3 = (*p7); + return l3++; +} + +int main(void) +{ + printf("returned x++ %u\n", (unsigned)returned_increment()); + printf("returned x-- %u\n", (unsigned)returned_decrement()); + printf("assigned x++ %u\n", (unsigned)assigned_increment()); + printf("through pointer %u\n", (unsigned)through_pointer()); + return 0; +} diff --git a/tests/perf_baselines.csv b/tests/perf_baselines.csv index 9108f76e2..1c4592f14 100644 --- a/tests/perf_baselines.csv +++ b/tests/perf_baselines.csv @@ -69,6 +69,7 @@ "tbitfld","317747","320382","6912","7040" "tbits","2129662","2134072","7552","7552" "tbits32","28577","29386","6016","6016" +"tblkdiv","167220","167483","6272","6272" "tblktern","55516","56751","8576","8704" "tblktype","29911","30988","7040","7168" "tbool","102775","113653","11136","12032" @@ -123,6 +124,7 @@ "tcanest","23181","23395","5376","5376" "tcapmbr","24122","24387","5376","5504" "tcaslv","1961496","1968714","11392","11776" +"tcastasg","72694","73097","5760","5760" "tcastcnd","30923","32038","7168","7296" "tcastlog","51104","51330","5248","5248" "tcharbit","23323","23428","5120","5120" @@ -483,6 +485,7 @@ "tvolopt","95455","95714","5888","5888" "tvpapar","22566","22790","5120","5120" "tvplain","74161","74578","7808","7808" +"twcasgm","91056","91592","6528","6656" "twhcomma","228262","232963","8448","8448" "twild","263295","263446","8320","8320" "tzpad","431338","432392","8448","8448" diff --git a/tests/tblkdiv.c b/tests/tblkdiv.c new file mode 100644 index 000000000..5dbb8da41 --- /dev/null +++ b/tests/tblkdiv.c @@ -0,0 +1,65 @@ +/* tblkdiv.c - dividing a 32-bit member by a nested-block 16-bit local + * + * `m /= b` where `m` is a 32-bit struct member and `b` an unsigned 16-bit + * local declared in a nested block. The division is a 32-bit one; dcc used + * to carry it out in 16 bits and zero the member's high word. The baseline + * is the host compiler's output. + */ +#include +#include + +struct S { uint16_t m0; int32_t m3; }; +static struct S s1 = { 1, 610804097 }; +static struct S s2[4] = { { 1, 2 }, { 3, 4 }, { 5, 610804097 }, { 7, 8 } }; +static struct S s3 = { 1, 610804097 }; +static struct S s4 = { 1, -610804097 }; + +static void show(const char *name, int32_t v) +{ + printf("%s %ld\n", name, (long)v); +} + +static void member(void) +{ + { + uint16_t b = (uint16_t)1; + s1.m3 /= b; + } +} + +static void member_of_element(void) +{ + { + uint16_t b = (uint16_t)1; + s2[2].m3 /= b; + } +} + +static void by_three(struct S *p) +{ + { + uint16_t b = (uint16_t)3; + p->m3 /= b; + } +} + +static void remainder_of(struct S *p) +{ + { + uint16_t b = (uint16_t)1000; + p->m3 %= b; + } +} + +int main(void) +{ + member(); + member_of_element(); + by_three(&s3); + remainder_of(&s4); + show("member / 1", s1.m3); + show("element member / 1", s2[2].m3); + show("pointer member / 3", s3.m3); + show("pointer member % 1000", s4.m3); + return 0; +} diff --git a/tests/tcastasg.c b/tests/tcastasg.c new file mode 100644 index 000000000..a8ccca99b --- /dev/null +++ b/tests/tcastasg.c @@ -0,0 +1,64 @@ +/* tcastasg.c - a narrowing cast applied to the value of an assignment + * + * The value of `x = e` or `x op= e` is the stored value in x's type; a cast + * to an 8- or 16-bit type then narrows it again. Covered as an expression + * value, an if condition and a switch control. dcc used to reject these + * with DCC-E1002; the baseline is the host compiler's output. + */ +#include +#include + +static uint16_t g16 = 7; +static int32_t g32 = 70000; + +static int32_t as_value(int16_t p) +{ + int8_t c1 = 1; + int16_t l1 = 1; + int32_t l2 = 1; + int32_t r; + r = (uint16_t)(c1 = 257); + r += (int16_t)(l1 = p); + r += (int8_t)(l2 = g16); + r += (uint8_t)(c1 += 3); + r += (uint16_t)(l2 = g32); + r += (int8_t)(l1 = p); + return r + c1 + l1 + l2; +} + +static int32_t as_condition(void) +{ + int32_t l2 = 1; + int32_t r = 0; + if ((int16_t)(l2 = g32) > 0) + r += 1000; + if ((int8_t)(l2 = 384) < 0) + r += 100; + return r + l2; +} + +static int32_t as_switch_control(void) +{ + uint8_t l3 = 9; + switch ((uint8_t)(l3 = 1)) { + case 1: + return 11 + l3; + default: + return 22; + } +} + +static int32_t in_comparison(void) +{ + int16_t l1 = 5; + return ((uint16_t)1) >= ((uint16_t)(l1 = ((uint16_t)1))); +} + +int main(void) +{ + printf("value %ld\n", (long)as_value(300)); + printf("condition %ld\n", (long)as_condition()); + printf("switch %ld\n", (long)as_switch_control()); + printf("comparison %ld\n", (long)in_comparison()); + return 0; +} diff --git a/tests/twcasgm.c b/tests/twcasgm.c new file mode 100644 index 000000000..e3f36f03a --- /dev/null +++ b/tests/twcasgm.c @@ -0,0 +1,60 @@ +/* twcasgm.c - compound assignment of a 32-bit value to a narrower lvalue + * + * The right operand is unsigned long or long and the target is an 8- or + * 16-bit struct member, array element or pointer target. The operation is + * carried out in 32 bits and the result narrowed to the target. dcc used to + * reject every one of these statements with DCC-E1002; the baseline is the + * host compiler's output. + */ +#include +#include + +struct S { uint8_t b; int16_t w; }; +static struct S s1 = { 200, 300 }; +static struct S s2 = { 200, 300 }; +static struct S sa[2] = { { 1, 2 }, { 200, 300 } }; +static uint8_t a8[4] = { 1, 200, 3, 4 }; +static int16_t a16[4] = { 1, 300, 3, 4 }; +static uint8_t m8[2][2] = { { 1, 2 }, { 200, 4 } }; +static uint32_t r = 5; +static int32_t big = 70003; +static int16_t idx = 1; + +static void member_through_pointer(struct S *p) +{ + p->b += r; + p->w ^= r; +} + +static void member(void) +{ + s2.b <<= r; + s2.w /= r; +} + +static void array_element(void) +{ + a8[1] += big; + a16[idx] -= big; + m8[idx][0] ^= r; + sa[idx].w += big; +} + +static void through_pointer(uint8_t *p, int16_t *q) +{ + *p |= r; + q[idx] %= r; +} + +int main(void) +{ + member_through_pointer(&s1); + member(); + array_element(); + through_pointer(&a8[2], &a16[1]); + printf("member via pointer %u %d\n", (unsigned)s1.b, (int)s1.w); + printf("member %u %d\n", (unsigned)s2.b, (int)s2.w); + printf("array %u %d %u %d\n", (unsigned)a8[1], (int)a16[1], (unsigned)m8[1][0], (int)sa[1].w); + printf("pointer %u %d\n", (unsigned)a8[2], (int)a16[2]); + return 0; +} From 05a036f0da5295ca66da2ecf9f9ce365c1ae6bb2 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 15:17:32 +1100 Subject: [PATCH 02/14] Log compiler bugs 022-029 and steer the clang-oracle fuzzer round them Eight more defects found by differential fuzzing against clang, each with a reproducer in tests/known-bugs/ whose baseline is clang's output, an entry in compiler-bugs.md and an upstream issue (#242-#249). BUG-004, 006, 012, 019 and 025 gain the further shapes found since. Fuzzer: - the host lint also rejects C23-only syntax, and the renderer no longer ends a switch on a bare case label; - a program that does not fit in 64K or in the TPA is a target limit, not a failure; - known_shapes() no longer reports loop counter steps or constants, so the reducer's guard against sliding into a logged bug is effective, and a program whose repair rebuilt a logged shape is dropped; - new avoidance features for BUG-019, 023 and 025-029. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 378 ++++++++++++++++++++++++-- scripts/README.md | 12 +- scripts/clang-oracle-fuzz.py | 27 +- scripts/clang_oracle_gen.py | 270 +++++++++++++++--- scripts/run-known-bugs.py | 4 +- tests/known-bugs/baselines/bug004.txt | 1 + tests/known-bugs/baselines/bug006.txt | 2 + tests/known-bugs/baselines/bug022.txt | 4 + tests/known-bugs/baselines/bug023.txt | 8 + tests/known-bugs/baselines/bug024.txt | 4 + tests/known-bugs/baselines/bug025.txt | 12 + tests/known-bugs/baselines/bug026.txt | 9 + tests/known-bugs/baselines/bug027.txt | 7 + tests/known-bugs/baselines/bug028.txt | 6 + tests/known-bugs/baselines/bug029.txt | 4 + tests/known-bugs/bug004.c | 13 + tests/known-bugs/bug006.c | 26 +- tests/known-bugs/bug022.c | 56 ++++ tests/known-bugs/bug023.c | 111 ++++++++ tests/known-bugs/bug024.c | 87 ++++++ tests/known-bugs/bug025.c | 142 ++++++++++ tests/known-bugs/bug026.c | 102 +++++++ tests/known-bugs/bug027.c | 141 ++++++++++ tests/known-bugs/bug028.c | 66 +++++ tests/known-bugs/bug029.c | 92 +++++++ 25 files changed, 1522 insertions(+), 62 deletions(-) create mode 100644 tests/known-bugs/baselines/bug022.txt create mode 100644 tests/known-bugs/baselines/bug023.txt create mode 100644 tests/known-bugs/baselines/bug024.txt create mode 100644 tests/known-bugs/baselines/bug025.txt create mode 100644 tests/known-bugs/baselines/bug026.txt create mode 100644 tests/known-bugs/baselines/bug027.txt create mode 100644 tests/known-bugs/baselines/bug028.txt create mode 100644 tests/known-bugs/baselines/bug029.txt create mode 100644 tests/known-bugs/bug022.c create mode 100644 tests/known-bugs/bug023.c create mode 100644 tests/known-bugs/bug024.c create mode 100644 tests/known-bugs/bug025.c create mode 100644 tests/known-bugs/bug026.c create mode 100644 tests/known-bugs/bug027.c create mode 100644 tests/known-bugs/bug028.c create mode 100644 tests/known-bugs/bug029.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 41e5ea949..eaa743a88 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -25,26 +25,36 @@ has been fixed by this work; this file is the log. | [BUG-002](#bug-002) | #223 | wrong result | `unsigned char` local `<<=` an `unsigned char` variable is not truncated on return | | [BUG-003](#bug-003) | #224 | wrong result | `++`/`--` on a 32-bit array element is done in 16 bits | | [BUG-004](#bug-004) | #225 | wrong result | 16-bit local set from `&&`/`\|\|`, stored to a 32-bit lvalue, leaves a garbage high word | -| [BUG-006](#bug-006) | #226 | wrong result | `/=` and `%=` on an unsigned bit-field | +| [BUG-006](#bug-006) | #226 | wrong result | `/=`, `%=` and `>>=` on an unsigned bit-field whose top bit is set | | [BUG-007](#bug-007) | #227 | rejects valid C, or crashes | Indirect call with a nested-block local argument that needs converting | | [BUG-008](#bug-008) | #228 | rejects valid C | Negative constant assigned to a 32-bit nested-block local | | [BUG-009](#bug-009) | #229 | hang | Nested `do … while (d < n)` with a 32-bit `d` never exits | | [BUG-010](#bug-010) | #230 | rejects valid C | Cast or narrowing return of a 32-bit post-increment or post-decrement | | [BUG-011](#bug-011) | #231 | rejects valid C | Compound assignment to an element of a byte array inside a struct | -| [BUG-012](#bug-012) | #232 | rejects valid C | Unary `-` or `~` applied to `!` of a constant | +| [BUG-012](#bug-012) | #232 | rejects valid C | Unary `-` or `~` applied to `!`, `&&` or `\|\|` of constants | | [BUG-013](#bug-013) | #233 | wrong result | Unsigned bit-field is not promoted to `int` once it is an operand | | [BUG-014](#bug-014) | #234 | rejects valid C | 32-bit local with two definitions, incremented, stored to a global | | [BUG-015](#bug-015) | #235 | wrong result | Nested-block pointer to a 32-bit object, and nested-block `unsigned char`, lose their type | | [BUG-016](#bug-016) | #236 | wrong result | A local takes the signedness of its initialiser, not of its declaration | | [BUG-017](#bug-017) | #237 | wrong result | Parameter `x &= x` returned through a widening cast gives 0 | | [BUG-018](#bug-018) | #238 | rejects valid C | 32-bit value compound-assigned to a bit-field or to an element of a 16-bit array member | -| [BUG-019](#bug-019) | #239 | rejects valid C | Unreachable nested-scope declaration after `return`, `break` or `goto` | +| [BUG-019](#bug-019) | #239 | rejects valid C | Unreachable nested-scope declaration after `return`, `break`, `goto` or `if (1) break;` | | [BUG-020](#bug-020) | #240 | rejects valid C | After a `switch`, a variable compared with `param % k` (32-bit parameter) | | [BUG-021](#bug-021) | #241 | wrong result | `x++`/`x--` yields the new value for a 16-bit local assigned from a local array element | - -Nineteen are open: nine give wrong results with no diagnostic, one hangs, -and nine stop the compile (BUG-007 can instead compile and crash). BUG-003, -BUG-013 and BUG-016 are the ones most likely to be hit by ordinary code. +| [BUG-022](#bug-022) | #242 | silent bad code | A reused label name in a nested block hides `MIR emission is required` for later functions | +| [BUG-023](#bug-023) | #243 | wrong result | `if (x++ > K) x = v;` leaves `x` as 1 when not taken (local assigned from another type) | +| [BUG-024](#bug-024) | #244 | wrong result | `if (x++)` in a loop tests the incremented value (8-bit local, later `do … while`) | +| [BUG-025](#bug-025) | #245 | wrong result | `return x;` of a nested-block object is not converted to an 8-bit or 32-bit return type | +| [BUG-026](#bug-026) | #246 | wrong result | `d = a - b; return d * d;`: the second operand is read from an unallocated stack slot | +| [BUG-027](#bug-027) | #247 | wrong result | Local assigned in an `if … else` nested in an `if`, then updated: the assignment is lost when a loop follows | +| [BUG-028](#bug-028) | #248 | wrong result | `signed char` `>>=` an `unsigned int` count shifts logically (counts 9 to 15) | +| [BUG-029](#bug-029) | #249 | wrong result | 8-bit local and another variable assigned in opposite arms of an `if … else`: `(int)c + x` loses `x` | + +Twenty-seven are open: sixteen give wrong results with no diagnostic, one +hangs, nine stop the compile (BUG-007 can instead compile and crash), and +one (BUG-022) turns a compile error into a broken program. BUG-026 is the +one most likely to be hit by ordinary code, then BUG-003, BUG-013, BUG-016 +and BUG-025. Two more were found and then fixed upstream before they were reported; see [Fixed upstream](#fixed-upstream). @@ -115,22 +125,40 @@ s.m1 = l; /* uint32_t m1: want 00000001, dcc xxxx0001 (e.g. 09f90001) - Wrong with variable or constant operands, `&&` or `||`, and a 32-bit member, array element or global as the destination. +- The same garbage high word appears when `&&` or `||` is assigned straight + to a 32-bit local declared in a nested block: + `{ int32_t b = 0; b = ((--x) || 1); g = b; }` gives `00010001`. That is + correct with the local at function scope, as an initialiser + (`int32_t b = (--x) || 1;`), and for `!` or a comparison. - Correct: `l = 1`; `l = one`; `s.m1 = one || zero` stored directly. - Commit `f00e062e` ("Widen && and || results converted to long or float") fixed a neighbouring shape. ## BUG-006 -**`/=` and `%=` on an unsigned bit-field store the wrong value.** +**`/=`, `%=` and `>>=` on an unsigned bit-field store the wrong value when +the field's top bit is set.** The field is sign-extended from its own width, +as if it were a signed bit-field, before the operation. ```c struct S { uint16_t m2 : 11; uint16_t m5 : 5; } s = { 1148, 18 }; -s.m5 %= 7; /* want 4, dcc 2 */ -s.m5 /= 21930; /* want 0, dcc 2 */ -s.m2 %= 21930; /* want 1148, dcc 296 */ +s.m5 %= 7; /* want 4, dcc 2 */ +s.m5 /= 21930; /* want 0, dcc 2 */ +s.m2 %= 21930; /* want 1148, dcc 296 */ +s.m5 >>= 1; /* want 9, dcc 25 */ +s.m2 >>= n; /* n = 3: want 143, dcc 1935 */ ``` -- Correct: `s.m5 = s.m5 % 7`; `+=`; `%=` on a signed bit-field. +- Every wrong value is what sign extension gives. 18 in 5 bits is `10010`, + which sign-extends to `0xfff2` (65522): 65522 % 7 is 2, 65522 / 21930 is 2, + and `0xfff2 >> 1` done arithmetically is `0xfff9`, 25 in 5 bits. +- Wrong for a field of any width, including all 16 bits + (`uint16_t a : 16` holding `0x8421`: `a >>= 3` gives `0xf084`), in a + global or local struct and through a pointer (`p->a >>= 3`), and when the + result is used (`r = (s.a >>= 3)`). +- Correct: the same statements when the field's top bit is clear; + `s.m5 = s.m5 % 7` and `s.m5 = s.m5 >> 1`; `+=`, `-=`, `*=`, `^=`, `<<=` + (their low bits do not depend on the extension); a signed bit-field. ## BUG-007 @@ -231,15 +259,19 @@ p->name[i] |= 0x80; ## BUG-012 -**Unary `-` or `~` applied to `!` of a constant is rejected.** `DCC-E1002`. +**Unary `-` or `~` applied to `!`, `&&` or `||` of constants is rejected.** +`DCC-E1002`. ```c r = -(!(-128)); r = ~(!15); r = -(!0); +r = ~(1 || 2); +if (n > (~(1 && 2))) r = 1; ``` -- Accepted: `-(!p)` and `~(!p)` with a variable; `r = !(-128); r = -r;`. +- Accepted: `-(!p)`, `~(!p)` and `~(p || 2)` with a variable; + `r = !(-128); r = -r;`; `~(3 > 2)`; `!(1 || 2)`; `(1 || 2) + 1`. ## BUG-013 @@ -263,7 +295,7 @@ if ((sel ? -21931 : s.m1) > 1) /* false; dcc takes the branch */ ## BUG-014 -**A signed 32-bit local that has two reaching definitions, is incremented by +**A 32-bit local that has two reaching definitions, is incremented by one and is then stored to a global stops the compile.** `dcc: fatal: missing direct MIR named-home offset`. @@ -274,9 +306,12 @@ l++; /* also ++l, l += 1, l = l + 1, g = ++l */ g = l; ``` -- The second definition can come from `if`, `switch` or a `goto`. -- Accepted: `--l`; `l += 2`; a `uint32_t` local; storing to an array element - or a local; a local with a single definition. +- The second definition can come from `if`, `switch`, a `goto` or a loop + (`do { g = ++l; } while (…);`). +- It also fails for a `uint32_t` local stored to a `uint32_t` global. +- Accepted: `--l`; `l += 2`; storing to an array element or a local; storing + through a conversion (`g = (int32_t)u;` with a `uint32_t u`); a local with a + single definition. ## BUG-015 @@ -386,10 +421,13 @@ if (sel) { - Fails after `return`, `break` and `goto`, inside `if`, `else`, `for`, `while` and `switch` bodies. +- It also fails when the jump is conditional on a constant that is true, + which leaves the same code unreachable: `if (7) break;`, `if (1) return 5;`, + `if ((0 + 7) >= (-128)) continue;`, `if (0) { } else { break; }`. - Accepted: the same dead code at function scope (not inside a nested block); dead code with no declaration (`return 1; { low += 1; }`); a `for` loop whose counter is declared at function scope; a conditional jump - (`if (sel) continue;`). + on a variable (`if (sel) continue;`, `if (1 || sel) break;`). - Dead code like this is rare in hand-written programs, so this is low priority; it is still valid C. @@ -450,6 +488,301 @@ static uint16_t f(void) cause is probably shared state (a slot or register), not the increment itself. +## BUG-022 + +**When a label name used in one function is reused inside a nested block of +a later function, dcc stops reporting `MIR emission is required` for the +functions after it.** It exits 0, and the program hangs or restarts in a +loop when the function it could not emit is called. + +```c +static uint16_t first(void) { L1:; return 1; } +static uint16_t second(void) { { L1:; } return 2; } /* same name, nested block */ +static int8_t third(void) { { int32_t b = 0; b = -8305; low = (uint16_t)b; } return 3; } +``` + +`third` is a function dcc cannot emit (BUG-008). Without `first` and +`second` the compile stops with the fatal error. With them it succeeds and +the program fails at run time. + +- Needs the same label name in both functions and the second one inside a + nested block. With the second label renamed, or at function scope, the + fatal error is reported as usual. +- Shown with the functions of BUG-008 and BUG-019. Functions that fail with + `missing direct MIR named-home offset` (BUG-014) still stop the compile. +- This only matters while some function cannot be emitted, but then it turns + a compile error into a silently broken program. + +## BUG-023 + +**A 16-bit local that was assigned a value of a different type and is then +updated loses the assigned value when a branch or loop follows.** The update +is applied to 0 or to a stale value instead. + +```c +static uint16_t f(int16_t n) +{ + uint16_t l; + l = n; /* 300 */ + if (l++ > 400) /* false */ + l = 0; + return l; /* want 301, dcc 1 */ +} + +short l = 0; +l = big; /* uint16_t big = 0xbeef, so l = -16657 */ +l++; +for (i = 5; i > 0; i--) { + if (zero) continue; + l++; +} +g = l; /* want -16651, dcc 6 (or another unrelated value) */ +``` + +- First shape: wrong when the local is `uint16_t` assigned from an `int16_t` + parameter or global, from `(uint8_t)n` or from `n + 0`, and when it is + `int16_t` assigned from a `uint16_t`; for `++` and `--`; for `>`, `>=`, `<` + and `==`. Correct with `++l`, an `else` branch, a `while` instead of `if`, + `l++;` as a separate statement, or the branch taken. +- Second shape: wrong for `l++`, `++l`, `l += 1`, `l = l + 1` and `l--` + before the loop, with any update of `l` inside it. Correct without the + update before the loop, or without the conditional `continue`. +- Both are correct when the local was assigned a value of its own type + (`uint16_t` from `uint16_t`), and for 8-bit and 32-bit locals. +- The dependence on the assigned value's type suggests the same root as + BUG-016. + +## BUG-024 + +**`if (x++)` inside a loop tests the incremented value instead of the old +one**, for an 8-bit local that a later `do … while` loop also modifies. + +```c +int8_t l = 0, i; +short d = 0; +for (i = 0; i < 3; i++) { + if (l++) /* l is 0, 1, 2: true twice; dcc: three times */ + hits++; +} +do { l--; } while (++d < 2); +``` + +- Wrong for `signed char` and `unsigned char`; for `if (l++)` and + `if (l++ != 0)`; with the first loop a `for` or a `while`; with `l--`, + `l -= 1` or `l++` in the later loop. +- Correct: a 16-bit local; no later `do … while` (a plain `l--;`, or a + `while` loop, after the first loop is fine); the `if` not in a loop. +- BUG-021 is another case where a post-increment yields the new value. + +## BUG-025 + +**`return x;`, where `x` is an object declared in a nested block, is not +converted to the function's return type.** To an 8-bit return type it is +not truncated (the caller receives all 16 bits); to a 32-bit return type it +is not widened (the high word is whatever was in the register). + +```c +static uint8_t f(void) +{ + { + int16_t b = 300; + return b; /* want 44 (300 truncated to 8 bits), dcc 300 */ + } +} +static int8_t g(void) { { uint8_t b = 200; return b; } } /* want -56, dcc 200 */ +static uint32_t w(void) { { int16_t b = 7; return b; } } /* want 7, dcc e.g. 0x012c0007 */ +``` + +- 8-bit return type (`uint8_t`, `int8_t`, plain `char`): wrong for a + nested-block `int16_t`, `uint16_t`, `int32_t` (the low 16 bits are + returned) and an 8-bit local of the other signedness. +- 32-bit return type (`int32_t`, `uint32_t`): wrong for a nested-block + `int8_t`, `uint8_t`, `int16_t` and `uint16_t`. A nested-block `int32_t` + is correct. +- Also wrong for a `static` local, an array element (`return b[0];`) and a + struct member (`return s.m;`); for `return (b);`, `return b++;` and + `return b = n;`. +- The nested scope can be a plain block, the body of an `if`, `while`, + `do`, `for` or `switch`, or a `for` initialiser + (`for (int16_t b = 300; …) return b;`). +- Correct: a local declared at function scope, even when the `return` is + inside a nested block; a parameter; `return (uint8_t)b;`, `return b + 1;`, + `return -b;`, `return +b;`, `return n ? b : 1;`, `return *p;`, + `return (int32_t)b;`, `return b + 0;`; a 16-bit return type; copying to a + function-scope local and returning that. +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. +- Like BUG-007, BUG-008 and BUG-015 it depends on the object being declared + in a nested block. + +## BUG-026 + +**A variable that is assigned a computed value and is then, in the next +statement, both operands of one binary operator is read from a stack slot +the function never allocates.** The result is garbage. + +```c +int sq(int a, int b) +{ + int d = a - b; + return d * d; /* sq(10, 1) is 81 and sq(20, 2) is 324; dcc returned 0 and 16578 */ +} +``` + +dcc emits, for `sq`: + +``` + push ix + ld ix,0 + add ix,sp ; no frame is allocated + ... + sbc hl,de ; a - b + push hl ; first operand: forwarded + ld l,(ix-10) ; second operand: read from below the stack pointer + ld h,(ix-9) + ex de,hl + pop hl + call __mulu +``` + +- Wrong for a local with an initialiser (`int d = a - b;`) or assigned + (`d = a - b;`), and for a parameter (`a = a ^ b; return a - a;`); assigned + from an expression or from a global (`a = g; return a + a;`). +- Wrong for `+ - * & | ^ << >> == != < > <= >=`; in `return d * d;`, + `g = d + d;`, `d = d * d;`, `return (d + d) * 2;`, `return d * d + 1;` and + `if (d + d > 3)`; when the second operand is a copy + (`int e = d; return d * e;`). A `long` local is wrong for `*`, `<<` and + `>>`. +- `int`, `unsigned`, `short`, `int16_t` and `uint16_t` behave the same, in + `static` and external functions. +- Correct: `d / d`, `d % d`, `d && d`, `d || d`; a different second operand + (`d * a`, `d * (d + 1)`, `d + a + d`, `d + d + d`); another statement + between the assignment and the use (`int d = a - b; calls++; return d * d;`); + `int d = a;` or `int d = 7;` (nothing computed); a `char` local; a + `register` local; another initialised local declared first + (`int t = 1; int d = a - b; return d * d + t;`). +- The selected emitter is `spilled-scalar-cfg`, which reports `frame=0 + slots=0` for `sq` (`DCC_MIR_COST_REPORT=1`). +- Not a regression: the v3.6.0 compiler (`20290235`) emits the same code. +- BUG-017 (`x &= x` on a parameter) also has one variable as both operands + and may be the same defect. + +## BUG-027 + +**A 16-bit local that is assigned in an `if … else` nested inside another +`if`, and then updated, loses the assignment when a loop follows.** The value +merged from the two arms is never stored; the update is applied to the old +value. + +```c +int f(int n) +{ + int l = 0, i; + if (one) { + if (one) { l = n; } else { } + l++; + } + for (i = 0; i < 2; i++) + seen = l; + return l; /* f(9): want 10, dcc 1 */ +} +``` + +dcc emits, after the inner `if … else`: + +``` +L15: + ld l,(ix+4) ; l = n: the value is left in HL + ld h,(ix+5) +L5: + inc (ix-2) ; l++ in place on l's slot, which still holds 0 + jp nz, L16 + inc (ix-1) +``` + +- Wrong for `l++`, `++l`, `l--`, `l += 2` and `l = l + 1`; with the + assignment in the `if` arm, the `else` arm or both + (`if (c) l = n; else l = 2;`); with an empty or a non-empty `else`; when the + outer `if` has an `else` of its own; for a local and for a parameter; + assigned a variable or a constant; with constant or variable conditions. +- What follows can be a `for` or `while` loop that reads the local, or a + conditional expression (`wide = (n ? x : 1);`). +- Correct: no outer `if`, or a plain block or a loop in its place; the inner + `if` without an `else`; `l = c ? n : l;` or a `switch` in place of the + inner `if … else`; nothing but `return l;` after it; an `unsigned char` or + `long` local. +- The selected candidate is `spilled-phi-slot` (`DCC_MIR_COST_REPORT=1`). +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. +- The symptom is BUG-023's (an update applied to a stale value), but nothing + here is assigned from another type, so it is logged separately. + +## BUG-028 + +**`>>=` on a signed 8-bit object with a count of type `unsigned int` shifts +logically instead of arithmetically.** The left operand is promoted to +`int`, which is signed, so the count's type must not matter. + +```c +signed char g = -34; +unsigned n = 11; +g >>= n; /* want -1, dcc 31 */ +g >>= 11u; /* want -1, dcc 31 */ +``` + +dcc sign-extends `g` to 16 bits and then shifts it with `srl h` / `rr l` +(`sra h` is needed), so zeros come in at the top: `0xffde >> 11` is `0x1f`. + +- Wrong for a global, a local, an array element (`a[7] >>= 11u;`) and + through a pointer; with the count a constant (`11u`, `(uint16_t)11`, + `((uint16_t)20155) & 15`) or an `unsigned int` variable. +- The result only differs for counts 9 to 15: for 1 to 8 the low 8 bits of + the logical and the arithmetic shift are the same. +- Correct: a count of type `int`, `unsigned char` or `unsigned long`; + `g = g >> n;`; a 16-bit or 32-bit left operand; `<<=`. +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. +- Low priority: shifting an 8-bit value right by more than 8 is unusual. + +## BUG-029 + +**An 8-bit local is assigned in one arm of an `if … else` and another +variable in the other arm; when the two are then combined, with the 8-bit +local cast or negated, the other variable's value is lost.** On the path +that assigned the 8-bit local, the other operand becomes a copy of it (or 0). + +```c +static uint16_t f(uint16_t x) +{ + int8_t l = 0; + if (sel) { x = 1; } else { l = 100; } + return (uint16_t)((int16_t)l - x); /* sel 0, x 32767: want 32869, dcc 0 (100 - 100) */ +} +``` + +dcc emits (`x` should be in DE at `L4`): + +``` +L8: ; sel != 0 + ld de,1 ; x = 1 + jp L4 +L3: ; else + ld hl,100 ; l = 100 + push hl + pop de ; ... and DE, the home of x, is overwritten with it +L4: + or a + sbc hl,de ; l - x +``` + +- Wrong for `(int16_t)l - x`, `(int16_t)l + x`, `(uint8_t)l - x`, `-l - x` + and `g = (int16_t)l; return g - x;`; for `int8_t`, `uint8_t` and `char`; + when the other variable is a parameter, a 16-bit local or another 8-bit + local; whichever arm assigns which; when one arm assigns both + (`if (sel) { c = '-'; x = 5; } else { c = '+'; } return (int)c + x;`). +- Correct: no cast (`l - x`); `(int32_t)l - x`; the operands the other way + round (`x - (int16_t)l`); `&`; a 16-bit `l`; an `if` with no `else`; both + arms assigning only `l`; a `switch` or `?:` in place of the `if … else`. +- The selected emitter is `homed-scalar-cfg` (`DCC_MIR_COST_REPORT=1`). +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. + --- ## Fixed upstream @@ -472,3 +805,10 @@ they were reported. Their reproducers now pass. (`-x -a:400 6502FUN.HEX`) runs past 30 s in both peep modes. That variable is diagnostic-only, so this is latent at most, and it has not been investigated. It was observed at `aa9b2f1c` and not rechecked since. +- **A module larger than 64K is not diagnosed as such.** Without `dccpeep`, + a 1,100-line generated program whose code passes 64K assembles with + `No Fatal error(s)` and then fails in the linker with + `l80c: module T: code range 2853..2854 out of bounds (segment size 2853)`: + the assembler's location counter appears to wrap. With `dccpeep` the same + program gets the clear `program too large: extends past 0xFFFF`. This is a + confusing message for a real limit, not wrong code, and is not reported. diff --git a/scripts/README.md b/scripts/README.md index da1d8302b..3f5027d8e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -84,7 +84,8 @@ through a checking helper, and the driver repairs the operations that trip, or drops the program. A program is used only when - the checked build, under ASan and UBSan, reports nothing; -- clang finds no unsequenced modification and no missing `return`; and +- clang finds no unsequenced modification, no missing `return` and no syntax + that is only valid from C23; and - `-O0`, `-O2`, the sanitized build and the checked build all print the same. So a difference from dcc is a dcc defect, not a porting difference. @@ -106,10 +107,15 @@ python3 scripts/clang-oracle-fuzz.py --list-configs `compiler-bugs.md`, so a run reports new failures instead of the same ones again. `KNOWN_BUGS` in `clang_oracle_gen.py` maps each such form to its entry; delete a line when its bug is fixed. `--off` also takes individual - generator features (`-h` lists them) to narrow a search by hand. + generator features (`-h` lists them) to narrow a search by hand. A program + whose undefined-behaviour repair rebuilt one of those forms is dropped and + counted as `INVALID`. - Failures are reported as `COMPILE` (dcc rejects the program), `VERIFY` (dcc prints a MIR verifier diagnostic but still exits 0), `MISMATCH`, `RUN-HANG` - or `BUILD-HANG`. A failing seed keeps `t.c`, `t.expected` and each failing + or `BUILD-HANG`. A program that only overflows its stack, does not link + into 64K or reports `out of ram` at start-up has met a limit of the target, + not a defect; it is counted as `limit-only`. A failing seed keeps `t.c`, + `t.expected` and each failing config's output under `build/clang-oracle-fuzz/`. An output line is a call number and the returned value, or 0x4000 plus the call number and a checksum of every global after that call. diff --git a/scripts/clang-oracle-fuzz.py b/scripts/clang-oracle-fuzz.py index c0a1bc1e5..1144276f8 100644 --- a/scripts/clang-oracle-fuzz.py +++ b/scripts/clang-oracle-fuzz.py @@ -153,9 +153,11 @@ def host_oracle(prog, work, repair=True, strict=True, timeout=10, rounds=40): src.write_text(gen.render(prog, title=', seed %s' % prog.seed)) # clang proves at compile time what no run can: an object modified twice, # or modified and read, with no sequence point between; and a value - # returning function that can fall off its end + # returning function that can fall off its end. It also accepts newer C + # than dcc claims to, which must not be mistaken for a dcc rejection lint = subprocess.run(['clang', '-std=gnu99', '-fsigned-char', '-fsyntax-only', '-Wno-everything', '-Werror=unsequenced', '-Werror=return-type', + '-Werror=c23-extensions', str(src)], capture_output=True, text=True) if lint.returncode != 0: return None, 'not well defined: ' + lint.stderr.strip().splitlines()[0][-160:] @@ -181,7 +183,7 @@ def host_oracle(prog, work, repair=True, strict=True, timeout=10, rounds=40): def dcc_run(source, build, cfg, args): """(kind, detail, output) of building and running source with dcc. - kind: ok, COMPILE, BUILD, BUILD-HANG, VERIFY or RUN-HANG.""" + kind: ok, COMPILE, BUILD, BUILD-HANG, VERIFY, RUN-HANG or TOOBIG.""" shutil.rmtree(build, ignore_errors=True) env = dict(os.environ) for key in DIAGNOSTIC_ENV: @@ -206,6 +208,9 @@ def dcc_run(source, build, cfg, args): first = first.replace(str(source) + ':', '') if 'error DCC' in first or 'dcc: fatal' in first or 'MIR' in first: return 'COMPILE', first[:200], log + if 'program too large' in log: + # over 64K: a limit of the target, not a defect + return 'TOOBIG', 'program does not fit in 64K', log return 'BUILD', first[:200], log verify = re.search(r'^; MIR \S+: (instruction \d+ .*)$', log, re.M) if verify: @@ -226,6 +231,9 @@ def judge(kind, detail, output, expected): return 'ok', '' if 'stack overflow' in output: return 'STACK', 'stack overflow' + if 'out of ram' in output: + # the runtime's start-up check: code and stack do not fit in the TPA + return 'TOOBIG', 'program does not fit in memory' want, got = expected.splitlines(), output.splitlines() wrong = [w.split()[0] for w, g in zip(want, got) if w != g] if len(got) != len(want): @@ -264,6 +272,13 @@ def run_seed(seed, args, configs): if not args.keep: shutil.rmtree(work, ignore_errors=True) return seed, 'INVALID', {'-': ('INVALID', reason)} + logged = gen.known_shapes(prog) + if logged: + # the generator steers round these, but a repair can rebuild one + if not args.keep: + shutil.rmtree(work, ignore_errors=True) + return seed, 'INVALID', {'-': ('INVALID', 'repair made a logged shape: %s' + % ','.join(sorted(logged)))} failures = {} for cfg in configs: build = work / cfg.name @@ -303,9 +318,13 @@ def fuzz(args): if args.verbose or status == 'GENERATOR': print('seed %d: %s %s' % (seed, status, failures['-'][1])) continue - real = {k: v for k, v in failures.items() if v[0] != 'STACK'} + real = {k: v for k, v in failures.items() if v[0] not in ('STACK', 'TOOBIG')} + if any(v[0] == 'TOOBIG' for v in failures.values()): + # past 64K the assembler's location counter wraps, and the + # larger builds of the same program fail in the linker instead + real = {k: v for k, v in real.items() if v[0] != 'BUILD'} if not real: - status = 'stack-only' if failures else 'ok' + status = 'limit-only' if failures else 'ok' counts[status] = counts.get(status, 0) + 1 continue counts['FAIL'] = counts.get('FAIL', 0) + 1 diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 505397584..a860f0de9 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -348,6 +348,107 @@ def init_keeps_type(init, t): return ty(init) is t +def folds(e): + """e is a constant once an assignment of a constant is taken as its + value: `~3`, `-(x = 77)`.""" + if e.op == 'asg': + return e.a == '=' and folds(e.k[1]) + return e.op == 'const' or (e.op in ('bin', 'un', 'cast', 'cond') + and all(folds(k) for k in e.k)) + + +def negative_constant(rhs, t): + """rhs is a constant, possibly under casts, that is negative before or + after them or, for a signed 32-bit target, has the top bit set: the + BUG-008 shape. Any other expression that folds is taken to be one.""" + casts = [] + while rhs.op == 'cast': + casts.append(ty(rhs)) + rhs = rhs.k[0] + if rhs.op != 'const': + return folds(rhs) + v = rhs.a + for ct in reversed(casts): + v = ct.wrap(v) if is_int(ct) else v + return rhs.a < 0 or v < 0 or (t is I32 and v >= 1 << 31) + + +def same_variable(a, b): + """a and b are the same variable: as the two operands of one binary + operator, the BUG-026 shape.""" + return a.op == 'var' and b.op == 'var' and a.a is b.a + + +def nested_if_else(s): + """s is an `if` with, directly in one of its arms, an `if` that has an + `else`: the BUG-027 shape.""" + return s.op == 'if' and any( + t.op == 'if' and len(t.b) > 1 and t.b[1] is not None + for arm in s.b if arm is not None for t in arm.stmts) + + +def byte_arm(s): + """s is an `if … else` with a plain assignment to an 8-bit local or + parameter directly in one of its arms: the BUG-029 shape when the other + arm assigns another variable.""" + if s.op != 'if' or len(s.b) < 2 or s.b[1] is None: + return False + for arm in s.b: + for t in arm.stmts: + e = t.e[0] if t.op == 'expr' and t.e else None + if e is not None and e.op == 'asg' and e.a == '=' and e.k[0].op == 'var' \ + and e.k[0].a.storage != 'global' and is_int(e.k[0].a.t) \ + and e.k[0].a.t.bits == 8: + return True + return False + + +def block_object(e): + """e names, or assigns to, an object declared in a nested block: as the + operand of `return` in a function returning an 8-bit type, the BUG-025 + shape.""" + if e.op in ('incdec', 'asg'): + e = e.k[0] + return e.op in ('var', 'idx') and e.a.storage == 'block' + + +def cross_type(rhs, t): + """rhs, stored to a 16-bit local of type t, arrives as a value of another + type, or through a cast that only changes signedness: the BUG-023 shape. + A constant, and a cast that also changes width, `(t)(int32_t)e`, are + not.""" + if is_constant(rhs): + return False + if ty(rhs) is not t: + return True + if rhs.op == 'cast': + inner = ty(rhs.k[0]) + return is_int(inner) and inner is not t and inner.bits == 16 + return False + + +def logic_value(e): + """e is `!x`, `x && y` or `x || y`: under unary `-` or `~` with constant + operands, the BUG-012 shape.""" + return (e.op == 'un' and e.a == '!') or (e.op == 'bin' and e.a in LOGIC) + + +def is_constant(e): + """e names no object and calls nothing, so a compiler can fold it.""" + return e.op == 'const' or (e.op in ('bin', 'un', 'cast', 'cond') + and all(is_constant(k) for k in e.k)) + + +def const_jump(s): + """s jumps on a condition that folds, leaving what follows it + unreachable: with a declaring scope after it, the BUG-019 shape.""" + if s.op == 'goto': + return s.e[0] is not None and is_constant(s.e[0]) + return s.op == 'if' and is_constant(s.e[0]) and any( + x.op in ('ret', 'break', 'continue') for sub in s.b if sub is not None + for x in sub.stmts) + + def known_shapes(prog): """The features in prog.off whose bug shape the program contains anyway. @@ -356,14 +457,22 @@ def known_shapes(prog): It uses this to refuse an edit that introduces a shape the program did not already have.""" found = set() + # a loop's own counter step, `for (…; …; i++)` or `while (w++ < n)`, is in + # every program and is not one of the logged increment shapes + steps = set() + for b in all_blocks(prog): + for s in b.stmts: + if s.op == 'for' and s.e[2] is not None: + steps.add(id(s.e[2])) + elif s.op == 'while' and s.e[0].op == 'bin' and s.e[0].k[0].op == 'incdec': + steps.add(id(s.e[0].k[0])) def stored(rhs, d): if not is_int(d.t) or d.storage == 'global': return - if d.t.bits <= 16 and rhs.op == 'bin' and rhs.a in LOGIC: + if (d.t.bits <= 16 or d.storage == 'block') and rhs.op == 'bin' and rhs.a in LOGIC: found.add('logicinit') - if d.storage == 'block' and d.t.bits == 32 and rhs.op == 'const' \ - and (rhs.a < 0 or (d.t is I32 and rhs.a >= 1 << 31)): + if d.storage == 'block' and d.t.bits == 32 and negative_constant(rhs, d.t): found.add('blockwideneg') def expr(e, root): @@ -374,6 +483,8 @@ def expr(e, root): if is_int(lt) and e.a == '=': if lv.op == 'var': stored(rhs, lv.a) + if lv.a.storage != 'global' and lt.bits == 16 and cross_type(rhs, lt): + found.add('crosstypeasg') elif is_int(lt): local = lv.op == 'var' and lv.a.storage != 'global' if lv.op != 'var' and lt.bits < 32 and ty(rhs).bits == 32: @@ -383,7 +494,9 @@ def expr(e, root): found.add('widecasgbf') if e.a == '<<=' and lt.bits == 8 and ty(rhs).bits == 8: found.add('bytevarshl') - if e.a in ('/=', '%=') and is_bitfield(lv) and not lv.a.t.signed: + if e.a == '>>=' and lt is I8 and ty(rhs) is U16: + found.add('bytesarcount') + if e.a in ('/=', '%=', '>>=') and is_bitfield(lv) and not lv.a.t.signed: found.add('bfdivasg') if e.a in ('/=', '%=') and lv.op == 'fld' and lt.bits == 32: found.add('blockwidediv') @@ -391,21 +504,30 @@ def expr(e, root): found.add('structbytecasg') if lv.op == 'var' and rhs.op == 'var' and rhs.a is lv.a: found.add('selfand') - if local and lt is I32 and e.a == '+=' and rhs.op == 'const' and rhs.a == 1: + if local and lt.bits == 32 and e.a == '+=' and rhs.op == 'const' and rhs.a == 1 \ + and id(e) not in steps: found.add('widelocalinc') elif op == 'incdec': lv = e.k[0] lt = ty(lv) + local = lv.op == 'var' and lv.a.storage != 'global' if is_int(lt) and lt.bits == 32: if lv.op in ('idx', 'pidx') or (lv.op == 'fld' and lv.a.dims): found.add('wideidxincdec') - if lv.op == 'var' and not e.a[1] and not root: - found.add('widepostinc') - if lv.op == 'var' and lt is I32 and lv.a.storage != 'global' and e.a[0] == '++': - found.add('widelocalinc') + if id(e) not in steps: + if lv.op == 'var' and not e.a[1] and not root: + found.add('widepostinc') + if local and e.a[0] == '++': + found.add('widelocalinc') + if is_int(lt) and lv.op == 'var' and not e.a[1] and not root \ + and id(e) not in steps: + found.add('postincvalue') elif op == 'un': - if e.a in ('-', '~') and e.k[0].op == 'un' and e.k[0].a == '!': + if e.a in ('-', '~') and logic_value(e.k[0]): found.add('negnotconst') + elif op == 'bin': + if same_variable(e.k[0], e.k[1]): + found.add('selfoperand') elif op == 'call': if e.a.via_ptr and any(is_int(p.t) and ty(a) is not p.t for a, p in zip(e.k, e.a.params)): @@ -425,11 +547,29 @@ def decl(d): for d in b.decls: decl(d) for s in b.stmts: + if const_jump(s): + found.add('constjump') + if nested_if_else(s): + found.add('nestedifelse') + if byte_arm(s): + found.add('bytearm') if s.op == 'for' and s.a is not None: decl(s.a) for e in s.e: if e is not None: expr(e, True) + + def returns(f, b): + for s in b.stmts: + if s.op == 'ret' and s.e and f.ret.bits == 8 and block_object(s.e[0]): + found.add('blockret') + for sub in s.b: + if sub is not None: + returns(f, sub) + + for f in prog.funcs: + if f.ret is not None and is_int(f.ret): + returns(f, f.body) return found & prog.off @@ -897,6 +1037,9 @@ def stmt(self, s, out, ind): self.block(body, out, ind + ' ') if has_break: out.append(ind + ' break;') + if out[-1].endswith(':'): + # a label needs a statement after it before C23 + out.append(ind + ' ;') out.append(ind + '}') elif op in ('break', 'continue'): out.append('%s%s;' % (ind, op)) @@ -1017,6 +1160,7 @@ def __init__(self, fn, pure): self.prot = set() self.used, self.banned = set(), set() self.loop = 0 + self.in_if = 0 self.nosfx = 0 self.names = 0 self.labels = 0 @@ -1056,7 +1200,8 @@ def fresh(self): 'widecassign', 'bytevarshl', 'castsfx', 'widepostinc', 'structbytecasg', 'negnotconst', 'bfdivasg', 'wideidxincdec', 'logicinit', 'blockwidediv', 'ptrcallconv', 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', - 'selfand', 'widecasgbf', 'widehelperparam')) + 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', + 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1079,6 +1224,15 @@ def fresh(self): 'selfand': 'BUG-017', 'widecasgbf': 'BUG-018', 'widehelperparam': 'BUG-020', + 'postincvalue': 'BUG-021, BUG-023, BUG-024', + 'duplabel': 'BUG-022', + 'crosstypeasg': 'BUG-023', + 'constjump': 'BUG-019', + 'blockret': 'BUG-025', + 'selfoperand': 'BUG-026', + 'nestedifelse': 'BUG-027', + 'bytesarcount': 'BUG-028', + 'bytearm': 'BUG-029', } @@ -1096,6 +1250,10 @@ def on(self, feature): # ---- small helpers def mk(self, op, k=(), a=None, t=None): + if op == 'bin' and not self.on('selfoperand') and same_variable(k[0], k[1]): + # one variable is never both operands of an operator + self.prog.nid += 1 + k = [k[0], E('const', (), 1, I16, self.prog.nid)] self.prog.nid += 1 return E(op, k, a, t, self.prog.nid) @@ -1349,8 +1507,7 @@ def gen_expr(self, c, depth): if kind == 'un': op = r.choice(('-', '~', '!')) operand = sub() - if op in ('-', '~') and operand.op == 'un' and operand.a == '!' \ - and not self.on('negnotconst'): + if op in ('-', '~') and logic_value(operand) and not self.on('negnotconst'): return operand return self.mk('un', [operand], op) if kind == 'cast': @@ -1370,14 +1527,16 @@ def gen_expr(self, c, depth): x = r.random() if x < 0.45: # with `widepostinc` off a 32-bit victim is only pre-incremented: - # its old value could reach a narrower type - prefix = r.random() < 0.5 or (d.t.bits == 32 and not self.on('widepostinc')) + # its old value could reach a narrower type. With `postincvalue` + # off nothing uses the value of a post-increment at all. + prefix = r.random() < 0.5 or not self.on('postincvalue') \ + or (d.t.bits == 32 and not self.on('widepostinc')) op = r.choice(('++', '--')) - if d.t is I32 and not self.on('widelocalinc'): + if d.t.bits == 32 and not self.on('widelocalinc'): op = '--' return self.mk('incdec', [lv], a=(op, prefix)) if x < 0.75: - return self.mk('asg', [lv, sub()], '=') + return self.mk('asg', [lv, self.stored(sub(), d.t, d.storage)], '=') return self.compound(lv, sub(), r.choice(('+=', '-=', '^=', '|=', '&=', '>>='))) def gen_cond(self, c, depth): @@ -1443,11 +1602,15 @@ def stored(self, rhs, t, storage, init=False): initialiser), steered round logged bugs whose feature is off.""" if storage == 'global' or not is_int(t): return rhs - if not self.on('logicinit') and t.bits <= 16 and rhs.op == 'bin' and rhs.a in LOGIC: + if not self.on('logicinit') and (t.bits <= 16 or storage == 'block') \ + and rhs.op == 'bin' and rhs.a in LOGIC: rhs.a = '|' if rhs.a == '||' else '&' if not self.on('blockwideneg') and storage == 'block' and t.bits == 32 \ - and rhs.op == 'const' and (rhs.a < 0 or (t is I32 and rhs.a >= 1 << 31)): - rhs.a = -rhs.a if rhs.a < 0 and rhs.a != rhs.t.lo else 1 + and negative_constant(rhs, t): + rhs = self.small(1, 1) + if not init and not self.on('crosstypeasg') and t.bits == 16 and cross_type(rhs, t): + # a cast that changes width as well as type keeps the value + rhs = self.mk('cast', [self.mk('cast', [rhs], t=I32)], t=t) return rhs def initialiser(self, c, t, expr, storage): @@ -1480,7 +1643,7 @@ def compound(self, lv, rhs, op): if op != '=' and is_int(lt): if not self.on('selfand') and lv.op == 'var' and rhs.op == 'var' and rhs.a is lv.a: rhs = self.small(1, 7) - if not self.on('widelocalinc') and lv.op == 'var' and lt is I32 \ + if not self.on('widelocalinc') and lv.op == 'var' and lt.bits == 32 \ and lv.a.storage != 'global' and op == '+=' and rhs.op == 'const' \ and rhs.a == 1: rhs = self.small(2, 2) @@ -1488,7 +1651,7 @@ def compound(self, lv, rhs, op): and lt.bits == 8: op = '=' return self.mk('asg', [lv, rhs], op) - if not self.on('bfdivasg') and is_bitfield(lv) and op in ('/=', '%=') \ + if not self.on('bfdivasg') and is_bitfield(lv) and op in ('/=', '%=', '>>=') \ and not lv.a.t.signed: rhs = self.mk('bin', [clone(lv), rhs], op[:-1]) return self.mk('asg', [lv, rhs], '=') @@ -1505,6 +1668,8 @@ def compound(self, lv, rhs, op): if not self.on('bytevarshl') and op == '<<=' and lt.bits == 8 \ and ty(rhs).bits == 8: rhs = self.mk('cast', [rhs], t=I16) + if not self.on('bytesarcount') and op == '>>=' and lt is I8 and ty(rhs) is U16: + rhs = self.mk('cast', [rhs], t=I16) return self.mk('asg', [lv, rhs], op) def st_cassign(self, c, depth, b): @@ -1526,7 +1691,7 @@ def st_incdec(self, c, depth, b): if lv is None: return op = self.rng.choice(('++', '--')) - if not self.on('widelocalinc') and lv.op == 'var' and ty(lv) is I32 \ + if not self.on('widelocalinc') and lv.op == 'var' and ty(lv).bits == 32 \ and lv.a.storage != 'global': op = '--' indexed = lv.op in ('idx', 'pidx') or (lv.op == 'fld' and lv.a.dims) @@ -1538,11 +1703,19 @@ def st_incdec(self, c, depth, b): def st_if(self, c, depth, b): r = self.rng cond = self.gen_cond(c, min(depth, 2)) + # with `nestedifelse` off an `if` inside an `if` gets no `else` + bare = c.in_if and not self.on('nestedifelse') + c.in_if += 1 then = self.gen_block(c, depth - 1, r.randint(1, 3), decls=r.random() < 0.3) blocks = [then] - if r.random() < 0.45: + if r.random() < 0.45 and not bare: blocks.append(self.gen_block(c, depth - 1, r.randint(1, 2), decls=r.random() < 0.2)) - b.stmts.append(S('if', [cond], blocks)) + c.in_if -= 1 + s = S('if', [cond], blocks) + if not self.on('bytearm') and byte_arm(s): + # keep the assignment, lose the other arm + del s.b[1:] + b.stmts.append(s) def loop_bound(self, c, b, n): """(bound, slack): a constant n, or a protected local set to a value in @@ -1557,6 +1730,7 @@ def loop_bound(self, c, b, n): base = self.gen_expr(c, 1) value = self.mk('bin', [self.mk('bin', [base, self.small(3, 3)], '&'), self.small(n, n)], '+') + value = self.stored(value, t, 'local') b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=d), value], '=')])) c.ints.append(d) return self.mk('var', a=d), 3 @@ -1569,7 +1743,9 @@ def loop_body(self, c, depth, counter=None, hi=None): c.ints.append(counter) c.counters.append(counter) c.loop += 1 + outer, c.in_if = c.in_if, 0 body = self.gen_block(c, depth - 1, r.randint(1, 3), decls=r.random() < 0.3) + c.in_if = outer c.loop -= 1 c.release(mark) return body @@ -1709,25 +1885,49 @@ def st_block(self, c, depth, b): b.stmts.append(S('block', b=[self.gen_block(c, depth - 1, r.randint(1, 3), decls=r.randint(1, 2))])) + def jump_cond(self, c): + """The condition of a conditional jump, or None when only one that + folds to a constant could be made and `constjump` is off.""" + for _ in range(8): + cond = self.gen_cond(c, 1) + if self.on('constjump') or not is_constant(cond): + return cond + return None + def st_brk(self, c, depth, b): - cond = self.gen_cond(c, 1) + cond = self.jump_cond(c) + if cond is None: + return self.st_assign(c, depth, b) inner = Block() inner.stmts.append(S(self.rng.choice(('break', 'continue')))) b.stmts.append(S('if', [cond], [inner])) + def returned(self, fn, e): + """e as the operand of `return` in fn. With `blockret` off a + nested-block object returned to an 8-bit type goes through a cast.""" + if not self.on('blockret') and fn.ret.bits == 8 and block_object(e): + return self.mk('cast', [e], t=fn.ret) + return e + def st_ret(self, c, depth, b): if c.fn.ret is None or not is_int(c.fn.ret): return - cond = self.gen_cond(c, 1) + cond = self.jump_cond(c) + if cond is None: + return self.st_assign(c, depth, b) inner = Block() c.fresh() - inner.stmts.append(S('ret', [self.gen_expr(c, min(depth, 2))])) + inner.stmts.append(S('ret', [self.returned(c.fn, self.gen_expr(c, min(depth, 2)))])) b.stmts.append(S('if', [cond], [inner])) def st_goto(self, c, depth, b): + cond = self.jump_cond(c) + if cond is None: + return self.st_assign(c, depth, b) c.labels += 1 - label = 'L%d' % c.labels - b.stmts.append(S('goto', [self.gen_cond(c, 1)], a=label)) + # a label name is unique in the program unless `duplabel` is on + label = 'L%d' % c.labels if self.on('duplabel') else '%s_L%d' % (c.fn.name, c.labels) + b.stmts.append(S('goto', [cond], a=label)) for _ in range(self.rng.randint(1, 2)): self.st_assign(c, depth - 1, b) b.stmts.append(S('label', a=label)) @@ -1874,8 +2074,9 @@ def st_call(self, c, depth, b): local = [d for d in simple if d.storage != 'global' and not d.addr_taken] x = r.random() if x < 0.5 and simple: - b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=r.choice(simple)), - call], '=')])) + dst = r.choice(simple) + b.stmts.append(S('expr', [self.mk('asg', [ + self.mk('var', a=dst), self.stored(call, dst.t, dst.storage)], '=')])) elif x < 0.7 and local: op = r.choice(('+=', '^=', '-=', '|=')) b.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=r.choice(local)), @@ -1884,7 +2085,10 @@ def st_call(self, c, depth, b): other = self.mk('var', a=r.choice(local)) if local and r.random() < 0.5 \ else self.const() cond = self.mk('bin', [call, other], r.choice(CMP)) - b.stmts.append(S('if', [cond], [self.gen_block(c, depth - 1, r.randint(1, 2))])) + c.in_if += 1 + then = self.gen_block(c, depth - 1, r.randint(1, 2)) + c.in_if -= 1 + b.stmts.append(S('if', [cond], [then])) else: b.stmts.append(S('expr', [call])) diff --git a/scripts/run-known-bugs.py b/scripts/run-known-bugs.py index 30f228c6c..2614be8d2 100644 --- a/scripts/run-known-bugs.py +++ b/scripts/run-known-bugs.py @@ -95,9 +95,11 @@ def check(name, args): out, err = host_output(source, work, ['-O2'], args.timeout) if out is not None: # unsequenced modifications and missing returns are undefined but - # invisible to the sanitizers; clang diagnoses them at compile time + # invisible to the sanitizers; clang diagnoses them at compile time. + # C23-only syntax is not something dcc has to accept lint = subprocess.run(['clang', '-std=gnu99', '-fsigned-char', '-fsyntax-only', '-Wno-everything', '-Werror=unsequenced', '-Werror=return-type', + '-Werror=c23-extensions', str(source)], capture_output=True, text=True) if lint.returncode: out, err = None, 'not well defined: ' + lint.stderr.strip().splitlines()[0][-150:] diff --git a/tests/known-bugs/baselines/bug004.txt b/tests/known-bugs/baselines/bug004.txt index 612ee8e1d..d9aa59607 100644 --- a/tests/known-bugs/baselines/bug004.txt +++ b/tests/known-bugs/baselines/bug004.txt @@ -2,3 +2,4 @@ && to element 00000001 || to global 00000000 constant || 00000001 +block local 00000001 diff --git a/tests/known-bugs/baselines/bug006.txt b/tests/known-bugs/baselines/bug006.txt index b6b255fe3..c4b0e857b 100644 --- a/tests/known-bugs/baselines/bug006.txt +++ b/tests/known-bugs/baselines/bug006.txt @@ -1,4 +1,6 @@ %= constant 4 48 %= variable 18 1148 /= 0 164 +>>= 9 143 +top bit clear 2 125 neighbours 33407 -11 33407 -11 diff --git a/tests/known-bugs/baselines/bug022.txt b/tests/known-bugs/baselines/bug022.txt new file mode 100644 index 000000000..ad60539d9 --- /dev/null +++ b/tests/known-bugs/baselines/bug022.txt @@ -0,0 +1,4 @@ +first 1 +second 2 +third 3 57231 +fourth 4 57231 diff --git a/tests/known-bugs/baselines/bug023.txt b/tests/known-bugs/baselines/bug023.txt new file mode 100644 index 000000000..c25de7dc8 --- /dev/null +++ b/tests/known-bugs/baselines/bug023.txt @@ -0,0 +1,8 @@ +from parameter 301 +from global 301 +signed from unsigned 301 +decrement 299 +equality 301 +stored 1 301 +updated in loop -16651 +decremented in loop -16663 diff --git a/tests/known-bugs/baselines/bug024.txt b/tests/known-bugs/baselines/bug024.txt new file mode 100644 index 000000000..0b1a098da --- /dev/null +++ b/tests/known-bugs/baselines/bug024.txt @@ -0,0 +1,4 @@ +signed char 2 1 +unsigned char 0 0 +compared 0 2 +while loop 0 0 diff --git a/tests/known-bugs/baselines/bug025.txt b/tests/known-bugs/baselines/bug025.txt new file mode 100644 index 000000000..01c0590ce --- /dev/null +++ b/tests/known-bugs/baselines/bug025.txt @@ -0,0 +1,12 @@ +block 44 +if 44 +for 44 +to signed -56 +to unsigned 200 +from long 112 +element 44 +updated 88 +function scope 44 +widened 7 +widened signed -2 +to 16 bits -2 diff --git a/tests/known-bugs/baselines/bug026.txt b/tests/known-bugs/baselines/bug026.txt new file mode 100644 index 000000000..eb7a34a4c --- /dev/null +++ b/tests/known-bugs/baselines/bug026.txt @@ -0,0 +1,9 @@ +1: square 81 twice 18 minus 0 equal 1 and 11 +1: global 10 condition 1 long 81 copied 81 +1: other operand 90 separated 81 +2: square 324 twice 36 minus 0 equal 1 and 22 +2: global 10 condition 1 long 324 copied 324 +2: other operand 360 separated 324 +3: square 729 twice 54 minus 0 equal 1 and 33 +3: global 10 condition 1 long 729 copied 729 +3: other operand 810 separated 729 diff --git a/tests/known-bugs/baselines/bug027.txt b/tests/known-bugs/baselines/bug027.txt new file mode 100644 index 000000000..f6c697a60 --- /dev/null +++ b/tests/known-bugs/baselines/bug027.txt @@ -0,0 +1,7 @@ +then arm 10 10 +both arms 10 10 +else arm 11 11 +l = l + 1 10 10 +parameter 231 +no outer if 10 10 +no else 10 10 diff --git a/tests/known-bugs/baselines/bug028.txt b/tests/known-bugs/baselines/bug028.txt new file mode 100644 index 000000000..1b5029254 --- /dev/null +++ b/tests/known-bugs/baselines/bug028.txt @@ -0,0 +1,6 @@ +array element -1 +global -1 +local -1 +pointer -1 +int count -1 +16-bit object -1 diff --git a/tests/known-bugs/baselines/bug029.txt b/tests/known-bugs/baselines/bug029.txt new file mode 100644 index 000000000..00967970a --- /dev/null +++ b/tests/known-bugs/baselines/bug029.txt @@ -0,0 +1,4 @@ +sel 0: minus 32869 plus 32810 two bytes 95 negated 32669 +sel 0: no cast 32869 16 bits 32869 +sel 1: minus 65535 plus 50 two bytes 65535 negated 65535 +sel 1: no cast 65535 16 bits 65535 diff --git a/tests/known-bugs/bug004.c b/tests/known-bugs/bug004.c index a4045531f..8aee3dde0 100644 --- a/tests/known-bugs/bug004.c +++ b/tests/known-bugs/bug004.c @@ -8,6 +8,7 @@ struct S { uint16_t m0; uint32_t m1; }; static struct S s = { 255, 0x5dd1d183 }; static uint32_t u[2] = { 0x80000000, 0x12345678 }; static uint32_t g = 0xdeadbeef; +static uint32_t g2 = 0xdeadbeef; static uint16_t one = 1; static uint16_t zero = 0; @@ -40,15 +41,27 @@ static void constant_or(void) u[1] = l; } +/* the same un-widened result, assigned to a 32-bit nested-block local */ +static void block_local(uint16_t x) +{ + { + int32_t b = 0; + b = ((--x) || 1); + g2 = (uint32_t)b; + } +} + int main(void) { or_to_member(); and_to_element(); or_to_global(); constant_or(); + block_local(1); show("|| to member", s.m1); show("&& to element", u[0]); show("|| to global", g); show("constant ||", u[1]); + show("block local", g2); return 0; } diff --git a/tests/known-bugs/bug006.c b/tests/known-bugs/bug006.c index ed4004aa1..823ee1e06 100644 --- a/tests/known-bugs/bug006.c +++ b/tests/known-bugs/bug006.c @@ -1,5 +1,7 @@ -/* BUG-006: /= and %= on an unsigned bit-field store the wrong value. - * `bf = bf % x` and += are correct, as is %= on a signed bit-field. */ +/* BUG-006: /=, %= and >>= on an unsigned bit-field store the wrong value + * when the field's top bit is set: the field is sign-extended from its + * width before the operation. `bf = bf % x` and += are correct, as are these + * operators on a signed bit-field or on a value with the top bit clear. */ #include #include @@ -7,7 +9,10 @@ struct S { uint16_t m1; uint16_t m2 : 11; int16_t m4 : 6; uint16_t m5 : 5; }; static struct S a = { 0x827f, 1148, -11, 18 }; static struct S b = { 0x827f, 1148, -11, 18 }; static struct S c = { 0x827f, 1148, -11, 18 }; +static struct S d = { 0x827f, 1148, -11, 18 }; +static struct S e = { 0x827f, 1000, -11, 9 }; static int16_t big = 21930; +static uint8_t three = 3; static uint32_t wide[2] = { 255, 21930 }; static void modulo_constant(void) @@ -28,14 +33,31 @@ static void divide(void) c.m2 /= 7; } +static void shift_right(void) +{ + d.m5 >>= 1; + d.m2 >>= three; +} + +/* the same operators, with the top bit of each field clear: correct */ +static void top_bit_clear(void) +{ + e.m5 %= 7; + e.m2 >>= 3; +} + int main(void) { modulo_constant(); modulo_variable(wide); divide(); + shift_right(); + top_bit_clear(); printf("%%= constant %u %u\n", (unsigned)a.m5, (unsigned)a.m2); printf("%%= variable %u %u\n", (unsigned)b.m5, (unsigned)b.m2); printf("/= %u %u\n", (unsigned)c.m5, (unsigned)c.m2); + printf(">>= %u %u\n", (unsigned)d.m5, (unsigned)d.m2); + printf("top bit clear %u %u\n", (unsigned)e.m5, (unsigned)e.m2); printf("neighbours %u %d %u %d\n", (unsigned)a.m1, (int)a.m4, (unsigned)c.m1, (int)c.m4); return 0; } diff --git a/tests/known-bugs/bug022.c b/tests/known-bugs/bug022.c new file mode 100644 index 000000000..73f5c8685 --- /dev/null +++ b/tests/known-bugs/bug022.c @@ -0,0 +1,56 @@ +/* BUG-022: when a label name used in one function is reused inside a nested + * block of a later function, dcc stops reporting "MIR emission is required" + * for functions after that. It exits 0 and the program hangs or restarts in + * a loop when the function it could not emit is called. With the second + * label renamed, or not inside a nested block, the compile fails as it + * should (see BUG-008 and BUG-019 for the function it cannot emit). */ +#include +#include + +static uint16_t low; +static int16_t sel = 1; + +static uint16_t first(void) +{ +L1:; + return 1; +} + +static uint16_t second(void) +{ + { +L1:; + } + return 2; +} + +static int8_t third(void) +{ + { + int32_t b = 0; + b = -8305; + low = (uint16_t)b; + } + return 3; +} + +static int8_t fourth(void) +{ + if (sel) { + return 4; + { + int16_t b = 1; + low += (uint16_t)b; + } + } + return 0; +} + +int main(void) +{ + printf("first %u\n", (unsigned)first()); + printf("second %u\n", (unsigned)second()); + printf("third %d %u\n", (int)third(), (unsigned)low); + printf("fourth %d %u\n", (int)fourth(), (unsigned)low); + return 0; +} diff --git a/tests/known-bugs/bug023.c b/tests/known-bugs/bug023.c new file mode 100644 index 000000000..e7c2cc9f2 --- /dev/null +++ b/tests/known-bugs/bug023.c @@ -0,0 +1,111 @@ +/* BUG-023: a 16-bit local that was assigned a value of a different type and + * is then updated loses the assigned value when a branch or loop follows: + * the update is applied to 0 or to a stale value. `if (l++ > K) l = v;` + * leaves 1 (or 65535) when the branch is not taken, and `l++` before a loop + * that updates l again conditionally does not count from the assigned + * value. It is correct when the local was assigned a value of its own type. */ +#include +#include + +static int16_t gs = 300; +static uint16_t gu = 300; +static uint16_t big = 0xbeef; +static int16_t zero = 0; +static int32_t wide; +static uint16_t out; + +static uint16_t from_parameter(int16_t n) +{ + uint16_t l; + l = n; + if (l++ > 400) + l = 0; + return l; +} + +static uint16_t from_global(void) +{ + uint16_t l; + l = gs; + if (l++ >= 400) + l = 77; + return l; +} + +static uint16_t signed_from_unsigned(void) +{ + int16_t l; + l = gu; + if (l++ > 400) + l = 0; + return l; +} + +static uint16_t decrement(int16_t n) +{ + uint16_t l; + l = n; + if (l-- > 400) + l = 0; + return l; +} + +static uint16_t equality(int16_t n) +{ + uint16_t l; + l = n; + if (l++ == 400) + l = 77; + return l; +} + +static uint16_t stored(int16_t n) +{ + uint16_t l; + l = n; + if (l++ < 200) + l = 77; + out = l; + return 1; +} + +static int32_t updated_in_loop(void) +{ + short l = 0; + uint8_t i; + l = big; /* a uint16_t: 0xbeef becomes -16657 */ + l++; + for (i = 5; i > 0; i--) { + if (zero) + continue; + l++; + } + return l; +} + +static int32_t decremented_in_loop(void) +{ + short l = 0; + uint8_t i; + l = big; + l -= 1; + for (i = 5; i > 0; i--) { + if (zero) + continue; + wide = (--l); + } + return wide; +} + +int main(void) +{ + printf("from parameter %u\n", (unsigned)from_parameter(300)); + printf("from global %u\n", (unsigned)from_global()); + printf("signed from unsigned %u\n", (unsigned)signed_from_unsigned()); + printf("decrement %u\n", (unsigned)decrement(300)); + printf("equality %u\n", (unsigned)equality(300)); + printf("stored %u %u\n", (unsigned)stored(300), (unsigned)out); + printf("updated in loop %ld\n", (long)updated_in_loop()); + printf("decremented in loop %ld\n", (long)decremented_in_loop()); + return 0; +} diff --git a/tests/known-bugs/bug024.c b/tests/known-bugs/bug024.c new file mode 100644 index 000000000..abed4ac4f --- /dev/null +++ b/tests/known-bugs/bug024.c @@ -0,0 +1,87 @@ +/* BUG-024: `if (x++)` inside a loop tests the incremented value instead of + * the old one, for an 8-bit local that a later do-while loop also modifies. + * The first pass, where x is 0, takes the branch. It is correct for a 16-bit + * local, without the later do-while, or when the `if` is not in a loop. */ +#include +#include + +static uint16_t hits; +static int16_t out; + +static void signed_byte(void) +{ + int8_t l = 0; + int8_t i; + short d = 0; + for (i = 0; i < 3; i++) { + if (l++) + hits++; + } + do { + l--; + } while ((++d) < 2); + out = l; +} + +static void unsigned_byte(void) +{ + uint8_t l = 0; + int8_t i; + short d = 0; + for (i = 3; i < 4; i += 1) { + if (l++) + hits++; + } + do { + l--; + } while ((++d) < 0); + out = l; +} + +static void compared(void) +{ + int8_t l = 0; + int8_t i; + short d = 0; + for (i = 3; i < 4; i += 1) { + if (l++ != 0) + hits++; + } + do { + l++; + } while ((++d) < 0); + out = l; +} + +static void while_loop(void) +{ + int8_t l = 0; + int8_t i; + short d = 0; + i = 3; + while (i < 4) { + if (l++) + hits++; + i++; + } + do { + l--; + } while ((++d) < 0); + out = l; +} + +int main(void) +{ + signed_byte(); + printf("signed char %u %d\n", (unsigned)hits, (int)out); + hits = 0; + unsigned_byte(); + printf("unsigned char %u %d\n", (unsigned)hits, (int)out); + hits = 0; + compared(); + printf("compared %u %d\n", (unsigned)hits, (int)out); + hits = 0; + while_loop(); + printf("while loop %u %d\n", (unsigned)hits, (int)out); + return 0; +} diff --git a/tests/known-bugs/bug025.c b/tests/known-bugs/bug025.c new file mode 100644 index 000000000..179d3032c --- /dev/null +++ b/tests/known-bugs/bug025.c @@ -0,0 +1,142 @@ +/* BUG-025: `return x;`, where x is declared in a nested block, is not + * converted to the function's return type. To an 8-bit type it is not + * truncated: the caller receives all 16 bits. To a 32-bit type it is not + * widened: the high word is whatever was in the register. It is correct for + * a local declared at function scope, for a parameter, for + * `return (uint8_t)x;`, `return x + 1;` and for a 16-bit return type. */ +#include +#include + +static int16_t g300 = 300; + +static uint8_t block(void) +{ + { + int16_t b = 300; + return b; + } +} + +static uint8_t in_if(int16_t n) +{ + if (n) { + int16_t b = n; + return b; + } + return 1; +} + +static uint8_t for_counter(void) +{ + for (int16_t b = 300; b < 400; b++) { + return b; + } + return 1; +} + +static int8_t to_signed(void) +{ + { + uint8_t b = 200; + return b; + } +} + +static uint8_t to_unsigned(void) +{ + { + int8_t b = -56; + return b; + } +} + +static uint8_t from_long(void) +{ + { + int32_t b = 70000; + return b; + } +} + +static uint8_t element(void) +{ + { + int16_t b[2] = { 300, 1 }; + return b[0]; + } +} + +static uint8_t updated(int16_t n) +{ + { + int16_t b = g300; + b += n; + return b; + } +} + +static uint8_t function_scope(void) +{ + int16_t b = 300; + { + return b; + } +} + +/* a 32-bit return type: the high word is not set */ +static uint32_t widened(void) +{ + { + int16_t b = 7; + return b; + } +} + +static int32_t widened_signed(int16_t n) +{ + if (n) { + int16_t b = n; + return b; + } + return 1; +} + +/* correct: a 16-bit return type */ +static int16_t to_16_bits(void) +{ + { + int8_t b = -2; + return b; + } +} + +int main(void) +{ + int16_t r; + int32_t w; + r = block(); + printf("block %d\n", r); + r = in_if(300); + printf("if %d\n", r); + r = for_counter(); + printf("for %d\n", r); + r = to_signed(); + printf("to signed %d\n", r); + r = to_unsigned(); + printf("to unsigned %d\n", r); + r = from_long(); + printf("from long %d\n", r); + r = element(); + printf("element %d\n", r); + r = updated(300); + printf("updated %d\n", r); + r = function_scope(); + printf("function scope %d\n", r); + w = (int32_t)widened(); + printf("widened %ld\n", (long)w); + w = widened_signed(-2); + printf("widened signed %ld\n", (long)w); + r = to_16_bits(); + printf("to 16 bits %d\n", r); + return 0; +} diff --git a/tests/known-bugs/bug026.c b/tests/known-bugs/bug026.c new file mode 100644 index 000000000..314f89a6b --- /dev/null +++ b/tests/known-bugs/bug026.c @@ -0,0 +1,102 @@ +/* BUG-026: a 16-bit variable that is assigned a computed value and is then, + * in the next statement, both operands of one binary operator: the second + * operand is read from a stack slot the function never allocates, so + * `d = a - b; return d * d;` returns garbage. Plain `int` behaves the same + * as int16_t. It is correct when another statement comes between, when the + * other operand is a different value, and for 8-bit variables. */ +#include +#include + +static int16_t g5 = 5; +static int16_t calls; + +static int16_t square(int16_t a, int16_t b) +{ + int16_t d = a - b; + return d * d; +} + +static int16_t twice(int16_t a, int16_t b) +{ + int16_t d; + d = a - b; + return d + d; +} + +static int16_t minus_self(int16_t a, int16_t b) +{ + a = a ^ b; + return a - a; +} + +static int16_t equal_self(int16_t a, int16_t b) +{ + int16_t d = a + b; + return d == d; +} + +static int16_t and_self(int16_t a, int16_t b) +{ + int16_t d = a + b; + return d & d; +} + +static int16_t from_global(int16_t a) +{ + a = g5; + return a + a; +} + +static int16_t in_condition(int16_t a, int16_t b) +{ + int16_t d = a - b; + if (d + d > 3) + return 1; + return 0; +} + +static int32_t square_long(int16_t a, int16_t b) +{ + int32_t d = a - b; + return d * d; +} + +/* the second operand is a copy of the first */ +static int16_t copied(int16_t a, int16_t b) +{ + int16_t d = a - b; + int16_t e = d; + return d * e; +} + +/* correct: the other operand is a different value */ +static int16_t other_operand(int16_t a, int16_t b) +{ + int16_t d = a - b; + return d * a; +} + +/* correct: a statement comes between */ +static int16_t separated(int16_t a, int16_t b) +{ + int16_t d = a - b; + calls++; + return d * d; +} + +int main(void) +{ + int16_t i; + for (i = 1; i <= 3; i++) { + int16_t a = i * 10; + printf("%d: square %d twice %d minus %d equal %d and %d\n", (int)i, + (int)square(a, i), (int)twice(a, i), (int)minus_self(a, i), + (int)equal_self(a, i), (int)and_self(a, i)); + printf("%d: global %d condition %d long %ld copied %d\n", (int)i, + (int)from_global(a), (int)in_condition(a, i), + (long)square_long(a, i), (int)copied(a, i)); + printf("%d: other operand %d separated %d\n", (int)i, + (int)other_operand(a, i), (int)separated(a, i)); + } + return 0; +} diff --git a/tests/known-bugs/bug027.c b/tests/known-bugs/bug027.c new file mode 100644 index 000000000..0a0542dbe --- /dev/null +++ b/tests/known-bugs/bug027.c @@ -0,0 +1,141 @@ +/* BUG-027: a 16-bit local that is assigned in an if-else nested inside + * another `if`, and then updated, loses the assignment when a loop (or a + * conditional expression) follows: the value merged from the two arms is + * never stored, and the update is applied to the old value. It is correct + * without the outer `if`, when the inner `if` has no `else`, and when no + * loop follows. */ +#include +#include + +static int16_t one = 1; +static int16_t zero = 0; +static int16_t seen; +static int32_t wide; + +static int16_t then_arm(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + if (one) { + l = n; + } else { + } + l++; + } + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +static int16_t both_arms(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + if (one) + l = n; + else + l = 2; + l++; + } + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +static int16_t else_arm(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + if (zero) { + } else { + l = n; + } + l += 2; + } + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +static int16_t assigned_sum(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + if (one) { + l = n; + } else { + } + l = l + 1; + } + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +static int16_t parameter(uint16_t x, int16_t n) +{ + if (one) { + if (one) { + x = 230; + } else { + } + ++x; + } + wide = (n ? x : 1); + return 1; +} + +/* correct: no outer `if` */ +static int16_t no_outer_if(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + l = n; + } else { + } + l++; + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +/* correct: the inner `if` has no `else` */ +static int16_t no_else(int16_t n) +{ + int16_t l = 0; + int16_t i; + if (one) { + if (one) { + l = n; + } + l++; + } + for (i = 0; i < 2; i++) + seen = l; + return l; +} + +int main(void) +{ + int16_t r; + r = then_arm(9); + printf("then arm %d %d\n", r, seen); + r = both_arms(9); + printf("both arms %d %d\n", r, seen); + r = else_arm(9); + printf("else arm %d %d\n", r, seen); + r = assigned_sum(9); + printf("l = l + 1 %d %d\n", r, seen); + r = parameter(0xdf7e, 9); + printf("parameter %ld\n", (long)wide); + r = no_outer_if(9); + printf("no outer if %d %d\n", r, seen); + r = no_else(9); + printf("no else %d %d\n", r, seen); + return 0; +} diff --git a/tests/known-bugs/bug028.c b/tests/known-bugs/bug028.c new file mode 100644 index 000000000..34f112bbb --- /dev/null +++ b/tests/known-bugs/bug028.c @@ -0,0 +1,66 @@ +/* BUG-028: `>>=` on a signed 8-bit object with a count of type unsigned int + * shifts logically instead of arithmetically. The 8-bit value is promoted to + * a 16-bit int, so the difference shows for counts 9 to 15, where a negative + * value must become -1. It is correct with an int, unsigned char or unsigned + * long count, for `x = x >> n`, and for a 16-bit object. */ +#include +#include + +static int8_t a6[8] = { 100, 16, -1, 1, 17, 21, 40, -34 }; +static int8_t b6[8] = { 100, 16, -1, 1, 17, 21, 40, -34 }; +static int8_t g8 = -34; +static int8_t k8 = -34; +static int16_t g16 = -34; +static uint16_t cu11 = 11; +static int16_t ci11 = 11; + +static int16_t array_element(void) +{ + a6[7] >>= 11u; + return a6[7]; +} + +static int16_t global(void) +{ + g8 >>= cu11; + return g8; +} + +static int16_t local(void) +{ + int8_t l = -34; + l >>= (uint16_t)11; + return l; +} + +static int16_t through_pointer(void) +{ + int8_t *p = &b6[7]; + *p >>= (uint16_t)11; + return b6[7]; +} + +/* correct: an int count */ +static int16_t int_count(void) +{ + k8 >>= ci11; + return k8; +} + +/* correct: a 16-bit object */ +static int16_t wider(void) +{ + g16 >>= cu11; + return g16; +} + +int main(void) +{ + printf("array element %d\n", array_element()); + printf("global %d\n", global()); + printf("local %d\n", local()); + printf("pointer %d\n", through_pointer()); + printf("int count %d\n", int_count()); + printf("16-bit object %d\n", wider()); + return 0; +} diff --git a/tests/known-bugs/bug029.c b/tests/known-bugs/bug029.c new file mode 100644 index 000000000..981911711 --- /dev/null +++ b/tests/known-bugs/bug029.c @@ -0,0 +1,92 @@ +/* BUG-029: an 8-bit local is assigned in one arm of an if-else and another + * variable in the other arm; the two are then combined in one expression in + * which the 8-bit local is cast or negated. On the path that assigned the + * 8-bit local, the other variable is replaced by the 8-bit local's value, so + * `(int16_t)l - x` is `l - l`. It is correct without the cast (`l - x`), for + * a 16-bit local, and with the operands the other way round. */ +#include +#include + +static int16_t sel; + +static uint16_t minus(uint16_t x) +{ + int8_t l = 0; + if (sel) { + x = 1; + } else { + l = 100; + } + return (uint16_t)((int16_t)l - x); +} + +static uint16_t plus_char(uint16_t x) +{ + char c = 0; + if (sel) { + c = '-'; + x = 5; + } else { + c = '+'; + } + return (int16_t)c + x; +} + +static uint16_t two_bytes(void) +{ + int8_t l1 = 0; + int8_t l2 = 5; + if (sel) { + l2 = 1; + } else { + l1 = 100; + } + return (uint16_t)((int16_t)l1 - (int16_t)l2); +} + +static uint16_t negated(uint16_t x) +{ + int8_t l = 0; + if (sel) { + x = 1; + } else { + l = 100; + } + return (uint16_t)(-l - x); +} + +/* correct: no cast */ +static uint16_t no_cast(uint16_t x) +{ + int8_t l = 0; + if (sel) { + x = 1; + } else { + l = 100; + } + return (uint16_t)(l - x); +} + +/* correct: a 16-bit local */ +static uint16_t sixteen_bits(uint16_t x) +{ + int16_t l = 0; + if (sel) { + x = 1; + } else { + l = 100; + } + return (uint16_t)((int16_t)l - x); +} + +int main(void) +{ + for (sel = 0; sel < 2; sel++) { + printf("sel %d: minus %u plus %u two bytes %u negated %u\n", (int)sel, + (unsigned)minus(32767), (unsigned)plus_char(32767), + (unsigned)two_bytes(), (unsigned)negated(32767)); + printf("sel %d: no cast %u 16 bits %u\n", (int)sel, + (unsigned)no_cast(32767), (unsigned)sixteen_bits(32767)); + } + return 0; +} From 4d6b2374237e8d03ab34c007a4c92939b7ed2b86 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 16:07:49 +1100 Subject: [PATCH 03/14] Log compiler bugs 030-033 and widen earlier entries Four more defects from the clang-oracle fuzzer, each with a reproducer in tests/known-bugs/ proven by clang, a compiler-bugs.md entry and an upstream issue (#250-#253): a for-init declaration that shares a stack slot with its enclosing block's locals, a nested-block 32-bit local stored with only its low byte, a dccpeep pass that drops an add, and a rejected constant division by zero. BUG-015, 016 and 030 gain further shapes. The generator steers round the new shapes, and around returns of a nested-block object to a 32-bit type (BUG-025). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 171 +++++++++++++++++++++++++- scripts/clang_oracle_gen.py | 63 +++++++++- tests/known-bugs/baselines/bug030.txt | 7 ++ tests/known-bugs/baselines/bug031.txt | 6 + tests/known-bugs/baselines/bug032.txt | 5 + tests/known-bugs/baselines/bug033.txt | 1 + tests/known-bugs/bug030.c | 158 ++++++++++++++++++++++++ tests/known-bugs/bug031.c | 85 +++++++++++++ tests/known-bugs/bug032.c | 59 +++++++++ tests/known-bugs/bug033.c | 32 +++++ 10 files changed, 578 insertions(+), 9 deletions(-) create mode 100644 tests/known-bugs/baselines/bug030.txt create mode 100644 tests/known-bugs/baselines/bug031.txt create mode 100644 tests/known-bugs/baselines/bug032.txt create mode 100644 tests/known-bugs/baselines/bug033.txt create mode 100644 tests/known-bugs/bug030.c create mode 100644 tests/known-bugs/bug031.c create mode 100644 tests/known-bugs/bug032.c create mode 100644 tests/known-bugs/bug033.c diff --git a/compiler-bugs.md b/compiler-bugs.md index eaa743a88..8d16f52f1 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -49,12 +49,17 @@ has been fixed by this work; this file is the log. | [BUG-027](#bug-027) | #247 | wrong result | Local assigned in an `if … else` nested in an `if`, then updated: the assignment is lost when a loop follows | | [BUG-028](#bug-028) | #248 | wrong result | `signed char` `>>=` an `unsigned int` count shifts logically (counts 9 to 15) | | [BUG-029](#bug-029) | #249 | wrong result | 8-bit local and another variable assigned in opposite arms of an `if … else`: `(int)c + x` loses `x` | - -Twenty-seven are open: sixteen give wrong results with no diagnostic, one -hangs, nine stop the compile (BUG-007 can instead compile and crash), and +| [BUG-030](#bug-030) | #250 | wrong result, or hang | A `for`-init declaration overlaps its enclosing block's locals on the stack (after an earlier sibling scope) | +| [BUG-031](#bug-031) | #251 | wrong result | Nested-block 32-bit local copied from another local, stored to a 16-bit lvalue: only the low byte is loaded | +| [BUG-032](#bug-032) | #252 | wrong result (`dccpeep` only) | `dccpeep` drops `x + K` (K from 2 to 255) when the sum is copied to another slot | +| [BUG-033](#bug-033) | #253 | rejects valid C | A constant divided by a constant zero, in code that never runs | + +Thirty-one are open: nineteen give wrong results with no diagnostic +(BUG-030 can also hang; BUG-032 needs `dccpeep`), one +hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the -one most likely to be hit by ordinary code, then BUG-003, BUG-013, BUG-016 -and BUG-025. +one most likely to be hit by ordinary code, then BUG-030 (it corrupts +memory in C99-style code), BUG-003, BUG-013, BUG-016 and BUG-025. Two more were found and then fixed upstream before they were reported; see [Fixed upstream](#fixed-upstream). @@ -332,6 +337,10 @@ signed char l2 = -66; function scope. - These are grouped because they share the nested-block condition; they may have separate causes. See also BUG-008. +- Beside a nested-block 32-bit local, another operand's cast is lost: + `{ int32_t b = 0; out = b + ((int16_t)((uint16_t)a6[1])); }` with `a6[1]` + holding -1 gives 65535, and `if (b >= ((int16_t)((uint16_t)a6[1])))` is + not taken. Correct with `b` at function scope, or with one cast. ## BUG-016 @@ -360,6 +369,10 @@ r = x / l; /* uint16_t x = 50000: want -16666, dcc 0 */ (`uint16_t l; l = gs;`), when the initialiser's cast also changes width (`(uint16_t)(int32_t)p`), for 8-bit locals, and when the local is only used at its own width (`l > 5`, `l / 2`). +- A separate assignment does not help in a nested block when the value is an + unsigned constant: `{ short b = 0; b = (uint16_t)0xb840; out = (b % three); }` + with a 32-bit `three` of 3 gives 2 (`47168 % 3`), not -2. Correct with `b` + at function scope or a signed constant. ## BUG-017 @@ -783,6 +796,154 @@ L4: - The selected emitter is `homed-scalar-cfg` (`DCC_MIR_COST_REPORT=1`). - Not a regression: the v3.6.0 compiler (`20290235`) does the same. +## BUG-030 + +**A C99 `for` initialiser declaration overlaps the locals of its enclosing +block on the stack, when an earlier sibling scope also declares a local.** +The loop counter overwrites part of an array, a pointer, or a variable whose +address is taken. The sibling scopes can be the arms of an `if … else`, the +cases of a `switch`, or two blocks inside any other block. + +```c +if (sel) { + uint32_t a = 0; + n += (uint16_t)a; +} else { + uint16_t x[2] = { 0x1234, 0x5678 }; + for (uint8_t i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; /* want 444c; dcc 124c: x[1] is now 0x0078 */ +} +``` + +With a pointer in place of the array, dcc emits (the pointer `q` is at +`ix-6`/`ix-5`, and so is the counter): + +``` + ld (ix-6),l ; q = &bytes[0] + ld (ix-5),h + ... + ld (ix-5),5 ; for (uint8_t i = 5; ...): i is also at ix-5 +``` + +- Wrong when the `else` arm's locals are an array, two pointers, or a + variable with its address taken; when the earlier arm's local is 8, 16 or + 32 bits wide, used or not; for an 8-bit or 16-bit counter; in the `else` + arm of an `if` and in a later `case` of a `switch`. +- Equally wrong for two sibling blocks inside the body of a `for`, `while` + or `if`, or inside a plain block: + `for (…) { { uint32_t a = 0; … } { uint16_t x[2] = …; for (uint8_t i = 5; …) … } }`. + Three generated programs that hang reduce to this form. +- With a pointer walk (`for (; q != end; ++q) { for (uint8_t i = 5; …) … }`) + the counter clears the pointer's high byte on every pass and the loop never + ends. +- Correct: the counter declared at the top of the block (`uint8_t i;`); a + plain nested block in place of the `for` (`{ uint8_t c = 5; … }`); the + earlier scope declaring nothing; two sibling blocks directly in the + function body; the earlier local declared in the enclosing block itself; + the two scopes in the other order; locals that do not need memory (plain + `uint16_t x, y;`). +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. + +## BUG-031 + +**A 32-bit local declared in a nested block and initialised from a +function-scope 32-bit local is stored to a 16-bit lvalue with only its low +byte loaded.** The high byte stored is whatever was in the register. + +```c +static void f(uint32_t x) /* f(0x00030201) */ +{ + uint32_t y = x; + { + uint32_t b = y; + a2[1] = b; /* uint16_t a2[]: want 0201, dcc fe01 */ + } +} +``` + +dcc emits, for the store: + +``` + ld l,(ix+6) ; low byte of the value; H is not loaded + ld (a2+2),hl +``` + +- Wrong when the destination is an array element (constant or variable + index), a struct member or `*p`; for `uint32_t` and `int32_t`; in a plain + block and in an `if` body. +- Correct: the local declared at function scope; initialised from a global, + a constant or an expression (`x + 1`); assigned instead of initialised + (`uint32_t b; b = y;`); stored with a cast (`a2[1] = (uint16_t)b;`) or as + `b + 1`; stored to a plain global or to an 8-bit lvalue; a `for` + initialiser (`for (uint32_t b = y; …)`). +- Not a regression: the v3.6.0 compiler (`20290235`) does the same. +- One more case of a nested-block local losing its declared width; see + BUG-015 and BUG-025. + +## BUG-032 + +**With `dccpeep`, a 16-bit local plus a constant from 2 to 255, copied to +another slot, loses the sum.** The copy stores whatever HL held before the +add. Without `dccpeep` the program is correct. + +```c +static uint32_t g6 = 256; +static uint8_t g8 = 69; +static uint16_t f(int16_t x) +{ + x = x; + return (g6 <= 1) ? g8 : (uint16_t)(x + 200); /* f(1): want 201, with dccpeep 0 */ +} +``` + +dcc's output for the false arm, and what `dccpeep` makes of it: + +``` + ld l,(ix-2) | ld a,(ix-2) ; peep: small_const_incr_carry_skip + ld h,(ix-1) | add a,200 + ld de,200 | ld (ix-2),a + add hl,de | jr nc,LI0 + ld (ix-2),l | inc (ix-1) + ld (ix-1),h | LI0: + ld l,(ix-2) | + ld h,(ix-1) | + ld (ix-4),l | ld (ix-4),l ; HL was never loaded: it still + ld (ix-3),h | ld (ix-3),h ; holds the result of __leu +``` + +- The pass's guard, `hl_relied_on_after()` in `src/dccpeep/dccpeep.c`, looks + for the text `hl` in the lines that follow. `ld (ix-4),l` and + `ld (ix-3),h` read H and L separately, so they are not seen. +- Wrong for constants 200 and 255, in `?:` and in + `if (…) r = g8; else r = (uint16_t)(x + 200);`, with `x = x;` or `x += 0;` + before it. +- Correct with `dccpeep`: constants 1, 256, 2000 and -56; a 16-bit + comparison in the condition (no helper call, so HL happens to be right); + without `x = x;`. +- The pass dates from `bff0d144` (2026-07-26), before v3.6.0. + +## BUG-033 + +**A constant divided by a constant zero is rejected, even in code that never +runs.** `DCC-E1002: unsupported if condition or branch` (or +`unsupported AST statement`), a message that does not mention division. + +```c +if (never) + return 1000 / 0; +if (never) + out = 7 % 0; +``` + +- The division is undefined only if it is evaluated, so a program that + never reaches it is valid; clang compiles it with a warning. +- Accepted: `n / 0` and `n % 0` with a variable `n`. +- Low priority: nobody writes this on purpose. It can come out of macros + (`SIZE / UNITS` in a branch guarded by `if (UNITS)`), and when it does the + diagnostic points at the enclosing `if` rather than at the division. +- Already present in v3.6.0 (`20290235`). + --- ## Fixed upstream diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index a860f0de9..656fc590a 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -403,6 +403,26 @@ def byte_arm(s): return False +def block_return(ret, e): + """`return e;` to return type ret, where e is a nested-block object that + needs converting: the BUG-025 shape.""" + return block_object(e) and (ret.bits == 8 or (ret.bits == 32 and ty(e).bits < 32)) + + +def wide_block_copy(d): + """d is a 32-bit nested-block local initialised from a local or a + parameter by name: the BUG-031 shape.""" + return d.storage == 'block' and is_int(d.t) and d.t.bits == 32 \ + and isinstance(d.init, E) and d.init.op == 'var' and d.init.a.storage != 'global' + + +def const_div_zero(e): + """e is a constant divided by a constant zero, which dcc rejects even + where it is never evaluated: the BUG-033 shape.""" + return e.op == 'bin' and e.a in DIV and e.k[1].op == 'const' and e.k[1].a == 0 \ + and is_constant(e.k[0]) + + def block_object(e): """e names, or assigns to, an object declared in a nested block: as the operand of `return` in a function returning an 8-bit type, the BUG-025 @@ -528,6 +548,8 @@ def expr(e, root): elif op == 'bin': if same_variable(e.k[0], e.k[1]): found.add('selfoperand') + if const_div_zero(e): + found.add('constdivzero') elif op == 'call': if e.a.via_ptr and any(is_int(p.t) and ty(a) is not p.t for a, p in zip(e.k, e.a.params)): @@ -536,6 +558,8 @@ def expr(e, root): expr(k, False) def decl(d): + if wide_block_copy(d): + found.add('blockwidecopy') if isinstance(d.init, E): if is_int(d.t): stored(d.init, d) @@ -561,13 +585,25 @@ def decl(d): def returns(f, b): for s in b.stmts: - if s.op == 'ret' and s.e and f.ret.bits == 8 and block_object(s.e[0]): + if s.op == 'ret' and s.e and block_return(f.ret, s.e[0]): found.add('blockret') for sub in s.b: if sub is not None: returns(f, sub) + def arms(b, inside): + """A `for` that declares its counter, anywhere inside another + statement: the BUG-030 shape when an earlier sibling scope declares + a local too.""" + for s in b.stmts: + if inside and s.op == 'for' and s.a is not None: + found.add('armfordecl') + for sub in s.b: + if sub is not None: + arms(sub, True) + for f in prog.funcs: + arms(f.body, False) if f.ret is not None and is_int(f.ret): returns(f, f.body) return found & prog.off @@ -1161,6 +1197,7 @@ def __init__(self, fn, pure): self.used, self.banned = set(), set() self.loop = 0 self.in_if = 0 + self.arm = 0 self.nosfx = 0 self.names = 0 self.labels = 0 @@ -1201,7 +1238,8 @@ def fresh(self): 'bfdivasg', 'wideidxincdec', 'logicinit', 'blockwidediv', 'ptrcallconv', 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', - 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm')) + 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', + 'armfordecl', 'blockwidecopy', 'constdivzero')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1233,6 +1271,9 @@ def fresh(self): 'nestedifelse': 'BUG-027', 'bytesarcount': 'BUG-028', 'bytearm': 'BUG-029', + 'armfordecl': 'BUG-030', + 'blockwidecopy': 'BUG-031', + 'constdivzero': 'BUG-033', } @@ -1254,6 +1295,11 @@ def mk(self, op, k=(), a=None, t=None): # one variable is never both operands of an operator self.prog.nid += 1 k = [k[0], E('const', (), 1, I16, self.prog.nid)] + if op == 'bin' and a in DIV and not self.on('constdivzero') \ + and k[1].op == 'const' and k[1].a == 0 and is_constant(k[0]): + # the checking layer only repairs a division that runs + self.prog.nid += 1 + k = [k[0], E('const', (), 1, I16, self.prog.nid)] self.prog.nid += 1 return E(op, k, a, t, self.prog.nid) @@ -1562,12 +1608,14 @@ def new_local(self, c, prefix, t, init, top=True): def gen_block(self, c, depth, n, decls=0): b = Block() mark = c.mark() + c.arm += 1 if not self.on('blockdecl'): decls = 0 for _ in range(decls): self.block_decl(c, b, depth) for _ in range(n): self.gen_stmt(c, depth, b) + c.arm -= 1 c.release(mark) return b @@ -1618,6 +1666,10 @@ def initialiser(self, c, t, expr, storage): starts as expr. With `inittype` off, an expr that is not already of exactly type t is assigned by a following statement instead.""" expr = self.stored(expr, t, storage) + if not self.on('blockwidecopy') and storage == 'block' and t.bits == 32 \ + and expr.op == 'var' and expr.a.storage != 'global': + # assigned by a following statement instead + return mk_const(0, t), expr if self.on('inittype') or init_keeps_type(expr, t): return expr, None return mk_const(0, t if t.bits >= 16 else I16), expr @@ -1756,7 +1808,10 @@ def st_for(self, c, depth, b): lo = r.randint(0, 3) n = r.randint(1, 5) hi = lo + n - c99 = self.on('c99for') and r.random() < 0.35 + # with `armfordecl` off a `for` declares its counter only when it is + # directly in the function body, not inside any other statement + c99 = self.on('c99for') and r.random() < 0.35 \ + and (self.on('armfordecl') or not c.arm) down = r.random() < 0.35 if c99: d = Decl(c.name('i'), t, 'block', cname=self.spelled(t)) @@ -1905,7 +1960,7 @@ def st_brk(self, c, depth, b): def returned(self, fn, e): """e as the operand of `return` in fn. With `blockret` off a nested-block object returned to an 8-bit type goes through a cast.""" - if not self.on('blockret') and fn.ret.bits == 8 and block_object(e): + if not self.on('blockret') and block_return(fn.ret, e): return self.mk('cast', [e], t=fn.ret) return e diff --git a/tests/known-bugs/baselines/bug030.txt b/tests/known-bugs/baselines/bug030.txt new file mode 100644 index 000000000..8cde28215 --- /dev/null +++ b/tests/known-bugs/baselines/bug030.txt @@ -0,0 +1,7 @@ +array in else 444c 5 +address taken 1234 5 +pointer walk 0008 40 +switch case 444c 5 +sibling blocks 444c 5 +counter in block 444c 5 +no earlier declaration 444c 5 diff --git a/tests/known-bugs/baselines/bug031.txt b/tests/known-bugs/baselines/bug031.txt new file mode 100644 index 000000000..034495f78 --- /dev/null +++ b/tests/known-bugs/baselines/bug031.txt @@ -0,0 +1,6 @@ +element 0201 +member 0201 +pointer 0201 +in if 0201 +function scope 0201 +global 0201 diff --git a/tests/known-bugs/baselines/bug032.txt b/tests/known-bugs/baselines/bug032.txt new file mode 100644 index 000000000..0d898a44e --- /dev/null +++ b/tests/known-bugs/baselines/bug032.txt @@ -0,0 +1,5 @@ +conditional 201 300 +if else 201 300 +add 255 256 355 +add 256 257 356 +narrow test 201 300 diff --git a/tests/known-bugs/baselines/bug033.txt b/tests/known-bugs/baselines/bug033.txt new file mode 100644 index 000000000..8d04f961a --- /dev/null +++ b/tests/known-bugs/baselines/bug033.txt @@ -0,0 +1 @@ +1 2 diff --git a/tests/known-bugs/bug030.c b/tests/known-bugs/bug030.c new file mode 100644 index 000000000..79571c3cd --- /dev/null +++ b/tests/known-bugs/bug030.c @@ -0,0 +1,158 @@ +/* BUG-030: a C99 `for` initialiser declaration is given a stack slot that + * overlaps the locals of its enclosing block, when an earlier sibling scope + * (the other arm of an `if`, an earlier case of a `switch`, or an earlier + * block inside the same parent block) also declares a local. The loop + * counter then overwrites part of an array, a pointer or a variable whose + * address is taken. It is correct when the counter is declared at the top + * of the block, when no earlier scope declares anything, and for two blocks + * directly in the function body. */ +#include +#include + +static int8_t bytes[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; +static uint16_t n; +static int16_t sel; +static int16_t three = 3; + +static uint16_t array_in_else(void) +{ + uint16_t r = 0; + if (sel) { + uint32_t a = 0; + n += (uint16_t)a; + } else { + uint16_t x[2] = { 0x1234, 0x5678 }; + for (uint8_t i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; + } + return r; +} + +static uint16_t address_taken(void) +{ + uint16_t r = 0; + if (sel) { + uint32_t a = 0; + n += (uint16_t)a; + } else { + uint16_t x = 0x1234; + uint16_t *px = &x; + for (uint8_t i = 5; i != 0; --i) + n++; + r = *px; + } + return r; +} + +/* the counter overlaps the walking pointer: without the `n < 200` bound + * this loop never ends */ +static uint16_t pointer_walk(void) +{ + uint16_t r = 0; + if (sel) { + uint32_t a = 0; + n += (uint16_t)a; + } else { + int8_t *q = &bytes[0]; + int8_t *end = bytes + 8; + for (; q != end && n < 200; ++q) { + for (uint8_t i = 5; i != 0; --i) + n++; + } + r = (uint16_t)(q - bytes); + } + return r; +} + +static uint16_t switch_case(void) +{ + uint16_t r = 0; + switch (three) { + case 0: + { + uint32_t a = 0; + n += (uint16_t)a; + } + break; + case 3: + { + uint16_t x[2] = { 0x1234, 0x5678 }; + for (uint8_t i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; + } + break; + } + return r; +} + +/* two sibling blocks inside a loop body, rather than arms of one statement */ +static uint16_t sibling_blocks(void) +{ + uint16_t r = 0; + int16_t k; + for (k = 0; k < 1; k++) { + { + uint32_t a = 0; + n += (uint16_t)a; + } + { + uint16_t x[2] = { 0x1234, 0x5678 }; + for (uint8_t i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; + } + } + return r; +} + +/* correct: the counter is declared at the top of the block */ +static uint16_t counter_in_block(void) +{ + uint16_t r = 0; + if (sel) { + uint32_t a = 0; + n += (uint16_t)a; + } else { + uint16_t x[2] = { 0x1234, 0x5678 }; + uint8_t i; + for (i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; + } + return r; +} + +/* correct: the first arm declares nothing */ +static uint16_t no_earlier_declaration(void) +{ + uint16_t r = 0; + if (sel) { + n += 1; + } else { + uint16_t x[2] = { 0x1234, 0x5678 }; + for (uint8_t i = 5; i != 0; --i) + n++; + r = x[0] ^ x[1]; + } + return r; +} + +static void show(const char *name, uint16_t r) +{ + printf("%s %04x %u\n", name, (unsigned)r, (unsigned)n); + n = 0; +} + +int main(void) +{ + show("array in else", array_in_else()); + show("address taken", address_taken()); + show("pointer walk", pointer_walk()); + show("switch case", switch_case()); + show("sibling blocks", sibling_blocks()); + show("counter in block", counter_in_block()); + show("no earlier declaration", no_earlier_declaration()); + return 0; +} diff --git a/tests/known-bugs/bug031.c b/tests/known-bugs/bug031.c new file mode 100644 index 000000000..c46faaab5 --- /dev/null +++ b/tests/known-bugs/bug031.c @@ -0,0 +1,85 @@ +/* BUG-031: a 32-bit local declared in a nested block and initialised from a + * function-scope 32-bit local is stored to a 16-bit array element, struct + * member or pointer target with only its low byte loaded: the high byte + * written is whatever was in the register. It is correct when the local is + * declared at function scope, when it is initialised from a global, a + * constant or an expression, and when the destination is a plain global. */ +#include +#include + +struct S { uint16_t m; }; +static uint16_t a2[4] = { 129, 22830, 1, 63 }; +static struct S s = { 7 }; +static uint16_t g16 = 5; + +static void element(uint32_t x) +{ + uint32_t y = x; + { + uint32_t b = y; + a2[1] = b; + } +} + +static void member(uint32_t x) +{ + uint32_t y = x; + { + uint32_t b = y; + s.m = b; + } +} + +static void through_pointer(uint32_t x) +{ + uint32_t y = x; + uint16_t *p = &a2[2]; + { + int32_t b = y; + *p = b; + } +} + +static void in_if(int16_t n, uint32_t x) +{ + uint32_t y = x; + if (n) { + uint32_t b = y; + a2[3] = b; + } +} + +/* correct: declared at function scope */ +static void function_scope(uint32_t x) +{ + uint32_t y = x; + uint32_t b = y; + a2[0] = b; +} + +/* correct: stored to a plain global */ +static void to_global(uint32_t x) +{ + uint32_t y = x; + { + uint32_t b = y; + g16 = b; + } +} + +int main(void) +{ + element(0x00030201); + member(0x00030201); + through_pointer(0x00030201); + in_if(1, 0x00030201); + function_scope(0x00030201); + to_global(0x00030201); + printf("element %04x\n", (unsigned)a2[1]); + printf("member %04x\n", (unsigned)s.m); + printf("pointer %04x\n", (unsigned)a2[2]); + printf("in if %04x\n", (unsigned)a2[3]); + printf("function scope %04x\n", (unsigned)a2[0]); + printf("global %04x\n", (unsigned)g16); + return 0; +} diff --git a/tests/known-bugs/bug032.c b/tests/known-bugs/bug032.c new file mode 100644 index 000000000..4c5a2feb8 --- /dev/null +++ b/tests/known-bugs/bug032.c @@ -0,0 +1,59 @@ +/* BUG-032: with dccpeep, a 16-bit local plus a constant from 2 to 255 that + * is then copied to another slot loses the sum: the copy stores whatever HL + * held before. dccpeep's small_const_incr_carry_skip pass turns the add into + * an 8-bit add on the stack slot, which no longer leaves the result in HL, + * but the following `ld (ix-N),l` / `ld (ix-M),h` still store HL. The + * program is correct without dccpeep (dcc-peep=false). */ +#include +#include + +static uint32_t g6 = 256; +static uint16_t g16 = 256; +static uint8_t g8 = 69; + +static uint16_t conditional(int16_t x) +{ + x = x; + return (g6 <= 1) ? g8 : (uint16_t)(x + 200); +} + +static uint16_t if_else(int16_t x) +{ + uint16_t r; + x = x; + if ((1 * g6) <= 1) + r = g8; + else + r = (uint16_t)(x + 200); + return r; +} + +static uint16_t add_255(int16_t x) +{ + x = x; + return ((g6 * g6) <= 1) ? g8 : (uint16_t)(x + 255); +} + +/* correct: 256 does not fit the 8-bit add */ +static uint16_t add_256(int16_t x) +{ + x = x; + return ((g6 * g6) <= 1) ? g8 : (uint16_t)(x + 256); +} + +/* correct: a 16-bit comparison leaves no helper call before the add */ +static uint16_t narrow_test(int16_t x) +{ + x = x; + return (g16 <= 1) ? g8 : (uint16_t)(x + 200); +} + +int main(void) +{ + printf("conditional %u %u\n", (unsigned)conditional(1), (unsigned)conditional(100)); + printf("if else %u %u\n", (unsigned)if_else(1), (unsigned)if_else(100)); + printf("add 255 %u %u\n", (unsigned)add_255(1), (unsigned)add_255(100)); + printf("add 256 %u %u\n", (unsigned)add_256(1), (unsigned)add_256(100)); + printf("narrow test %u %u\n", (unsigned)narrow_test(1), (unsigned)narrow_test(100)); + return 0; +} diff --git a/tests/known-bugs/bug033.c b/tests/known-bugs/bug033.c new file mode 100644 index 000000000..a3534b506 --- /dev/null +++ b/tests/known-bugs/bug033.c @@ -0,0 +1,32 @@ +/* BUG-033: a constant divided by a constant zero is rejected with DCC-E1002 + * ("unsupported if condition or branch"), even in code that never runs. The + * division is undefined only if it is evaluated, so the program is valid, + * and the message does not mention division. `n / 0` with a variable `n` is + * accepted. Low priority. */ +#include +#include + +static int16_t never = 0; +static int16_t out; + +static int16_t in_return(void) +{ + if (never) + return 1000 / 0; + return 1; +} + +static void in_assignment(void) +{ + if (never) + out = 7 % 0; + else + out = 2; +} + +int main(void) +{ + in_assignment(); + printf("%d %d\n", in_return(), out); + return 0; +} From 55190fa0dcdf7c79d4d2f2fc7437e180a7eeea69 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 16:22:50 +1100 Subject: [PATCH 04/14] Log compiler bug 034 (dccpeep cp8_to_bit3) and widen BUG-012 dccpeep turns `8 == v` into a test of bit 3 (upstream issue #254). Unary operators over a folded constant expression are rejected in more forms than BUG-012 listed; the generator now steers round all of them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 49 +++++++++++++++++++++++++-- scripts/clang_oracle_gen.py | 6 ++-- tests/known-bugs/baselines/bug034.txt | 6 ++++ tests/known-bugs/bug034.c | 47 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/known-bugs/baselines/bug034.txt create mode 100644 tests/known-bugs/bug034.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 8d16f52f1..036aee18a 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -53,9 +53,10 @@ has been fixed by this work; this file is the log. | [BUG-031](#bug-031) | #251 | wrong result | Nested-block 32-bit local copied from another local, stored to a 16-bit lvalue: only the low byte is loaded | | [BUG-032](#bug-032) | #252 | wrong result (`dccpeep` only) | `dccpeep` drops `x + K` (K from 2 to 255) when the sum is copied to another slot | | [BUG-033](#bug-033) | #253 | rejects valid C | A constant divided by a constant zero, in code that never runs | +| [BUG-034](#bug-034) | #254 | wrong result (`dccpeep` only) | `dccpeep` turns `8 == v` into a test of bit 3 | -Thirty-one are open: nineteen give wrong results with no diagnostic -(BUG-030 can also hang; BUG-032 needs `dccpeep`), one +Thirty-two are open: twenty give wrong results with no diagnostic +(BUG-030 can also hang; BUG-032 and BUG-034 need `dccpeep`), one hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the one most likely to be hit by ordinary code, then BUG-030 (it corrupts @@ -277,6 +278,11 @@ if (n > (~(1 && 2))) r = 1; - Accepted: `-(!p)`, `~(!p)` and `~(p || 2)` with a variable; `r = !(-128); r = -r;`; `~(3 > 2)`; `!(1 || 2)`; `(1 || 2) + 1`. +- Two unary operators over a constant binary expression are rejected the + same way: `~(-(7 >> 13))`, `-(~(7 >> 13))`, `-(-(7 >> 13))`, + `~(+(7 >> 13))`, `~(-(8 - 8))`, `~(-(1 > 2))`. One unary operator + (`~(7 >> 13)`), two over a literal (`~(-5)`) and a variable inside + (`~(-(n >> 13))`) are accepted. ## BUG-013 @@ -944,6 +950,45 @@ if (never) diagnostic points at the enclosing `if` rather than at the division. - Already present in v3.6.0 (`20290235`). +## BUG-034 + +**With `dccpeep`, `8 == v` can become a test of bit 3**, so 9, 12, 47 and +every other value with that bit set compares equal to 8. Without `dccpeep` +the program is correct. + +```c +static uint8_t g8 = 47; +static int16_t f(uint16_t x) +{ + if (8 == (uint16_t)g8) { /* false; with dccpeep it is taken */ + x = 100; + } + return x; +} +``` + +`dccpeep` output for the test (`g8`, zero-extended, is in `ix-2`/`ix-1`): + +``` + bit 3,(ix-2) ; peep: cp8_to_bit3 + jr z,L3 + ld a,(ix-1) + or a + jr nz,L3 +``` + +- The rewrite is in `pass_and1_ix_to_bit()` in `src/dccpeep/dccpeep.c`: + `ld a,(ix+K) / cp 8 / jp nz,L` becomes `bit 3,(ix+K) / jp z,L`. Its + comment says "Valid for depth values 0-8: bit3 is set only at depth=8", + but nothing checks that the value is in that range. +- Wrong for `8 == (uint16_t)g8` and `8 != (uint16_t)g8` in an `if` with no + `else`, for the values 9, 12 and 47. +- Correct with `dccpeep`: the values 7, 8 and 16; a comparison with 16; + `c == 8` on a `char` parameter or local in an `if`, a loop, a `switch` or + as a value (the simple forms do not produce the three-instruction pattern, + so `if (c == '\b')` is safe as far as these probes go). +- The rewrite has been in the file since at least `3dd3b434` (2026-06-14). + --- ## Fixed upstream diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 656fc590a..aa2308883 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -543,7 +543,8 @@ def expr(e, root): and id(e) not in steps: found.add('postincvalue') elif op == 'un': - if e.a in ('-', '~') and logic_value(e.k[0]): + if e.a in ('-', '~') and (logic_value(e.k[0]) or ( + e.k[0].op != 'const' and is_constant(e.k[0]))): found.add('negnotconst') elif op == 'bin': if same_variable(e.k[0], e.k[1]): @@ -1553,7 +1554,8 @@ def gen_expr(self, c, depth): if kind == 'un': op = r.choice(('-', '~', '!')) operand = sub() - if op in ('-', '~') and logic_value(operand) and not self.on('negnotconst'): + if op in ('-', '~') and not self.on('negnotconst') and (logic_value(operand) or ( + operand.op != 'const' and is_constant(operand))): return operand return self.mk('un', [operand], op) if kind == 'cast': diff --git a/tests/known-bugs/baselines/bug034.txt b/tests/known-bugs/baselines/bug034.txt new file mode 100644 index 000000000..be5e2e4a8 --- /dev/null +++ b/tests/known-bugs/baselines/bug034.txt @@ -0,0 +1,6 @@ +8: equal 100 not equal 85 sixteen 85 +7: equal 85 not equal 1 sixteen 85 +9: equal 85 not equal 1 sixteen 85 +12: equal 85 not equal 1 sixteen 85 +47: equal 85 not equal 1 sixteen 85 +16: equal 85 not equal 1 sixteen 100 diff --git a/tests/known-bugs/bug034.c b/tests/known-bugs/bug034.c new file mode 100644 index 000000000..fe8f40d17 --- /dev/null +++ b/tests/known-bugs/bug034.c @@ -0,0 +1,47 @@ +/* BUG-034: with dccpeep, `8 == v` can become a test of bit 3. dccpeep's + * cp8_to_bit3 rewrite turns `ld a,(ix+K) / cp 8 / jp nz,L` into + * `bit 3,(ix+K) / jp z,L`, which is only the same thing for values 0 to 8 + * (its own comment says so). Any other value with bit 3 set, such as 9, 12 + * or 47, then compares equal to 8. The program is correct without dccpeep + * (dcc-peep=false). */ +#include +#include + +static uint8_t g8; + +static int16_t equal(uint16_t x) +{ + if (8 == (uint16_t)g8) { + x = 100; + } + return x; +} + +static int16_t not_equal(uint16_t x) +{ + if (8 != (uint16_t)g8) { + x = 1; + } + return x; +} + +/* correct: 16 is not rewritten */ +static int16_t sixteen(uint16_t x) +{ + if (16 == (uint16_t)g8) { + x = 100; + } + return x; +} + +int main(void) +{ + static const uint8_t values[6] = { 8, 7, 9, 12, 47, 16 }; + uint8_t i; + for (i = 0; i < 6; i++) { + g8 = values[i]; + printf("%u: equal %d not equal %d sixteen %d\n", (unsigned)g8, + equal(85), not_equal(85), sixteen(85)); + } + return 0; +} From 93c95e659f0078962d3be299e8a79745293b6403 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 18:35:35 +1100 Subject: [PATCH 05/14] Log BUG-035, widen BUG-015, BUG-031 and BUG-032, and steer the fuzzer dccpeep's preserve_ix_word_signed_range compares a 16-bit local with itself (upstream issue #255). A nested-block uint16_t pointer is shifted and divided as signed (BUG-015), a constant-initialised nested-block local assigned from another local loses its high byte (BUG-031), and a dccpeep add can feed a helper call (BUG-032). The generator avoids all four, and the fuzzer counts an image the emulator cannot load as a target limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 89 ++++++++++++-- scripts/clang-oracle-fuzz.py | 5 +- scripts/clang_oracle_gen.py | 25 +++- tests/known-bugs/baselines/bug015.txt | 3 + tests/known-bugs/baselines/bug031.txt | 1 + tests/known-bugs/baselines/bug032.txt | 2 + tests/known-bugs/baselines/bug035.txt | 6 + tests/known-bugs/bug015.c | 37 +++++- tests/known-bugs/bug031.c | 20 +++- tests/known-bugs/bug032.c | 23 ++++ tests/known-bugs/bug035.c | 159 ++++++++++++++++++++++++++ 11 files changed, 351 insertions(+), 19 deletions(-) create mode 100644 tests/known-bugs/baselines/bug035.txt create mode 100644 tests/known-bugs/bug035.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 036aee18a..a73c9f5dd 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -54,9 +54,10 @@ has been fixed by this work; this file is the log. | [BUG-032](#bug-032) | #252 | wrong result (`dccpeep` only) | `dccpeep` drops `x + K` (K from 2 to 255) when the sum is copied to another slot | | [BUG-033](#bug-033) | #253 | rejects valid C | A constant divided by a constant zero, in code that never runs | | [BUG-034](#bug-034) | #254 | wrong result (`dccpeep` only) | `dccpeep` turns `8 == v` into a test of bit 3 | +| [BUG-035](#bug-035) | #255 | wrong result (`dccpeep` only) | `dccpeep` compares a 16-bit local with itself in `a > x \|\| x < k` after a struct member on the stack | -Thirty-two are open: twenty give wrong results with no diagnostic -(BUG-030 can also hang; BUG-032 and BUG-034 need `dccpeep`), one +Thirty-three are open: twenty-one give wrong results with no diagnostic +(BUG-030 can also hang; BUG-032, BUG-034 and BUG-035 need `dccpeep`), one hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the one most likely to be hit by ordinary code, then BUG-030 (it corrupts @@ -347,6 +348,12 @@ signed char l2 = -66; `{ int32_t b = 0; out = b + ((int16_t)((uint16_t)a6[1])); }` with `a6[1]` holding -1 gives 65535, and `if (b >= ((int16_t)((uint16_t)a6[1])))` is not taken. Correct with `b` at function scope, or with one cast. +- A nested-block pointer to a `uint16_t` element with the top bit set is + shifted and divided as signed (found by fuzzing at `83dc7a91`; seeds + 4245 and later): `{ uint16_t *q = &a2[1]; r = (*q) >> (g1 & 15); }` with + `a2[1]` = 0x8001 and `g1` = 239 gives 255, not 1, and `(*q) / (g1 & 15)` + gives 120, not 136. `*q >> 15` is wrong too; `r = (*q)` and `&` or `+` + are right, and so is a pointer declared at function scope. ## BUG-016 @@ -879,10 +886,14 @@ dcc emits, for the store: index), a struct member or `*p`; for `uint32_t` and `int32_t`; in a plain block and in an `if` body. - Correct: the local declared at function scope; initialised from a global, - a constant or an expression (`x + 1`); assigned instead of initialised - (`uint32_t b; b = y;`); stored with a cast (`a2[1] = (uint16_t)b;`) or as + a constant or an expression (`x + 1`); declared without an initialiser and + then assigned (`uint32_t b; b = y;`); stored with a cast (`a2[1] = (uint16_t)b;`) or as `b + 1`; stored to a plain global or to an 8-bit lvalue; a `for` initialiser (`for (uint32_t b = y; …)`). +- Also wrong: initialised with a constant and then assigned the other local, + `{ int32_t b = 0; b = y; a2[1] = b; }` (found by fuzzing, seed 5089). + A different type on the right, such as `uint32_t b = 0; b = y;` with an + `int32_t y`, is correct. - Not a regression: the v3.6.0 compiler (`20290235`) does the same. - One more case of a nested-block local losing its declared width; see BUG-015 and BUG-025. @@ -927,6 +938,10 @@ dcc's output for the false arm, and what `dccpeep` makes of it: - Correct with `dccpeep`: constants 1, 256, 2000 and -56; a 16-bit comparison in the condition (no helper call, so HL happens to be right); without `x = x;`. +- The sum can also be the operand of a helper call, not only a copy: + `while ((w++) < 1) sink = (l += 225) / 121;` with `int16_t l = 1` gives 0, + not 1, with `dccpeep`, because `ld l,(ix-2)` / `ld h,(ix-1)` before + `call __q2s` is removed (the same for `%`). Without the loop it is correct. - The pass dates from `bff0d144` (2026-07-26), before v3.6.0. ## BUG-033 @@ -989,6 +1004,59 @@ static int16_t f(uint16_t x) so `if (c == '\b')` is safe as far as these probes go). - The rewrite has been in the file since at least `3dd3b434` (2026-06-14). +## BUG-035 + +**With `dccpeep`, a test of a 16-bit local as the right operand, after a +struct member that is already on the stack, compares the local with itself.** +Without `dccpeep` the program is correct. + +```c +struct S1 { int16_t m0; uint16_t m1; uint8_t m2; }; +static struct S1 s1 = { 0, 292, 255 }; /* x1 = s1 */ +static int32_t a3[4] = { 32766, 32768, 147, -1682854718 }; + +static uint8_t f(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; /* 0x9c42 */ + for (i8 = 3; i8 > 1; i8 -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + a3[1] = 1; /* 0 > 0x9c42 and 0x9c42 < 1 are false: want 32768, dccpeep 1 */ + switch ((uint32_t)1) { case 100: break; default: break; case 10: break; } + } + l2 = (uint16_t)1; + return 1; +} +``` + +What `dccpeep` makes of the first test (`pop hl` / `pop de` read the same word): + +``` + push hl ; x1.m0 + ld l,(ix-2) + ld h,(ix-1) + push hl ; peep: preserve_ix_word_signed_range (inserted) + push hl ; the original push of l2 + pop de + pop hl ; was x1.m0, is now l2 + ex de,hl + or a + sbc hl,de ; l2 - l2 +``` + +- `pass_preserve_ix_word_across_signed_range()` in `src/dccpeep/dccpeep.c` + is written for `x < low || x > high`, where the word is the left operand of + the `sbc hl,de`. It scans from the `ld l,(ix-N)` / `ld h,(ix-M)` pair to + the next `sbc hl,de` / `jp c` and accepts a `push hl` / `pop de` / + `pop hl` shuffle in between, so it also fires when the word is the right + operand. +- Wrong for the loop with a `switch` on a constant or on the counter, and + with a `uint8_t` or `uint32_t` counter. Correct: the same function without + the `switch`, without the assignment after the loop, or with one test + instead of two. +- Reproduces with `5f733050`, `32d3e46d` and `5bcac74e`. + --- ## Fixed upstream @@ -1006,11 +1074,14 @@ they were reported. Their reproducers now pass. ## Not yet confirmed -- **`a1` hangs with the regional emitter forced.** With - `DCC_MIR_PREFER_REGIONAL=1` the `a1` test's `6502fun` scenario - (`-x -a:400 6502FUN.HEX`) runs past 30 s in both peep modes. That variable - is diagnostic-only, so this is latent at most, and it has not been - investigated. It was observed at `aa9b2f1c` and not rechecked since. +- **`a1` is 5.7 times slower with the regional emitter forced.** With + `DCC_MIR_PREFER_REGIONAL=1`, at `83dc7a91`, `runall.py -apps a1 -mode=full` + no longer hangs and the output is correct, but it is a performance + regression: peep 10,329,123 -> 67,385,816 cycles and nopeep 12,162,933 -> + 69,161,297 cycles, with `.COM` 19,072 -> 22,400 and 21,120 -> 24,576 bytes. + The `6502fun` scenario runs well inside 30 s. That variable is + diagnostic-only, so this is latent at most, and it has not been + investigated further. - **A module larger than 64K is not diagnosed as such.** Without `dccpeep`, a 1,100-line generated program whose code passes 64K assembles with `No Fatal error(s)` and then fails in the linker with diff --git a/scripts/clang-oracle-fuzz.py b/scripts/clang-oracle-fuzz.py index 1144276f8..31b7fd503 100644 --- a/scripts/clang-oracle-fuzz.py +++ b/scripts/clang-oracle-fuzz.py @@ -231,8 +231,9 @@ def judge(kind, detail, output, expected): return 'ok', '' if 'stack overflow' in output: return 'STACK', 'stack overflow' - if 'out of ram' in output: - # the runtime's start-up check: code and stack do not fit in the TPA + if 'out of ram' in output or 'too large to fit in memory' in output: + # the runtime's start-up check, or the emulator refusing the image: + # code and stack do not fit in the TPA return 'TOOBIG', 'program does not fit in memory' want, got = expected.splitlines(), output.splitlines() wrong = [w.split()[0] for w, g in zip(want, got) if w != g] diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index aa2308883..cbcf23e74 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -503,6 +503,9 @@ def expr(e, root): if is_int(lt) and e.a == '=': if lv.op == 'var': stored(rhs, lv.a) + if lv.a.storage == 'block' and lt.bits == 32 and rhs.op == 'var' \ + and rhs.a.storage != 'global': + found.add('blockwidecopy') if lv.a.storage != 'global' and lt.bits == 16 and cross_type(rhs, lt): found.add('crosstypeasg') elif is_int(lt): @@ -527,6 +530,9 @@ def expr(e, root): if local and lt.bits == 32 and e.a == '+=' and rhs.op == 'const' and rhs.a == 1 \ and id(e) not in steps: found.add('widelocalinc') + if local and lt.bits == 16 and e.a == '+=' and rhs.op == 'const' \ + and 2 <= rhs.a <= 255: + found.add('peepsum') elif op == 'incdec': lv = e.k[0] lt = ty(lv) @@ -561,6 +567,8 @@ def expr(e, root): def decl(d): if wide_block_copy(d): found.add('blockwidecopy') + if d.storage == 'block' and isinstance(d.t, PtrType) and d.t.to is U16: + found.add('blockptr') if isinstance(d.init, E): if is_int(d.t): stored(d.init, d) @@ -1240,7 +1248,7 @@ def fresh(self): 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', - 'armfordecl', 'blockwidecopy', 'constdivzero')) + 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1275,6 +1283,7 @@ def fresh(self): 'armfordecl': 'BUG-030', 'blockwidecopy': 'BUG-031', 'constdivzero': 'BUG-033', + 'peepsum': 'BUG-032', } @@ -1658,6 +1667,9 @@ def stored(self, rhs, t, storage, init=False): if not self.on('blockwideneg') and storage == 'block' and t.bits == 32 \ and negative_constant(rhs, t): rhs = self.small(1, 1) + if not self.on('blockwidecopy') and storage == 'block' and t.bits == 32 \ + and rhs.op == 'var' and rhs.a.storage != 'global': + rhs = self.mk('bin', [rhs, self.small(1, 1)], '^') if not init and not self.on('crosstypeasg') and t.bits == 16 and cross_type(rhs, t): # a cast that changes width as well as type keeps the value rhs = self.mk('cast', [self.mk('cast', [rhs], t=I32)], t=t) @@ -1670,8 +1682,8 @@ def initialiser(self, c, t, expr, storage): expr = self.stored(expr, t, storage) if not self.on('blockwidecopy') and storage == 'block' and t.bits == 32 \ and expr.op == 'var' and expr.a.storage != 'global': - # assigned by a following statement instead - return mk_const(0, t), expr + # an expression, not a copy of the variable + expr = self.mk('bin', [expr, self.small(1, 1)], '^') if self.on('inittype') or init_keeps_type(expr, t): return expr, None return mk_const(0, t if t.bits >= 16 else I16), expr @@ -1701,6 +1713,10 @@ def compound(self, lv, rhs, op): and lv.a.storage != 'global' and op == '+=' and rhs.op == 'const' \ and rhs.a == 1: rhs = self.small(2, 2) + if not self.on('peepsum') and lv.op == 'var' and lt.bits == 16 \ + and lv.a.storage != 'global' and op == '+=' and rhs.op == 'const' \ + and 2 <= rhs.a <= 255: + rhs = self.small(1, 1) if not self.on('structbytecasg') and lv.op == 'fld' and lv.a.dims \ and lt.bits == 8: op = '=' @@ -2017,7 +2033,8 @@ def st_scopy(self, c, depth, b): def st_walk(self, c, depth, b): r = self.rng arrs = [d for d in c.arrs if len(d.dims) == 1 and (not c.pure or d.storage != 'global') - and (d.t.bits < 32 or self.on('blockptr'))] + and (d.t.bits < 32 or self.on('blockptr')) + and (d.t is not U16 or self.on('blockptr'))] if not arrs: return self.st_assign(c, depth, b) arr = r.choice(arrs) diff --git a/tests/known-bugs/baselines/bug015.txt b/tests/known-bugs/baselines/bug015.txt index a7d669298..77fc15e37 100644 --- a/tests/known-bugs/baselines/bug015.txt +++ b/tests/known-bugs/baselines/bug015.txt @@ -2,3 +2,6 @@ multiply 0003ae4e compare char 0 compare int 0 byte local 1 +shift u16 1 +divide u16 136 +shift outer 1 diff --git a/tests/known-bugs/baselines/bug031.txt b/tests/known-bugs/baselines/bug031.txt index 034495f78..c33eacb0b 100644 --- a/tests/known-bugs/baselines/bug031.txt +++ b/tests/known-bugs/baselines/bug031.txt @@ -2,5 +2,6 @@ element 0201 member 0201 pointer 0201 in if 0201 +assigned after init 0201 function scope 0201 global 0201 diff --git a/tests/known-bugs/baselines/bug032.txt b/tests/known-bugs/baselines/bug032.txt index 0d898a44e..8c344084e 100644 --- a/tests/known-bugs/baselines/bug032.txt +++ b/tests/known-bugs/baselines/bug032.txt @@ -3,3 +3,5 @@ if else 201 300 add 255 256 355 add 256 257 356 narrow test 201 300 +compound divide 1 +compound modulo 105 diff --git a/tests/known-bugs/baselines/bug035.txt b/tests/known-bugs/baselines/bug035.txt new file mode 100644 index 000000000..1885cd894 --- /dev/null +++ b/tests/known-bugs/baselines/bug035.txt @@ -0,0 +1,6 @@ +loop 32768 +byte counter 32768 +switch on counter 32768 +no switch 32768 +no tail 32768 +single test 32768 diff --git a/tests/known-bugs/bug015.c b/tests/known-bugs/bug015.c index a54db78bb..06c06b016 100644 --- a/tests/known-bugs/bug015.c +++ b/tests/known-bugs/bug015.c @@ -1,13 +1,16 @@ /* BUG-015: locals declared in a nested block lose their declared type in * further shapes. Through a nested-block pointer to a 32-bit object, `*q *= k` * is computed in 16 bits and `c <= *q` is compared as signed; a nested-block - * unsigned char is compared as unsigned int. Each is correct when the local - * is declared at function scope. */ + * unsigned char is compared as unsigned int. A nested-block pointer to a + * uint16_t with the top bit set is shifted and divided as signed. Each is + * correct when the local is declared at function scope. */ #include #include struct S { uint16_t m5 : 6; }; static struct S s = { 24 }; +static uint16_t a2[2] = { 21930, 0x8001 }; +static uint8_t g1 = 239; static uint32_t a4[8] = { 1, 0xaa55, 7, 100, 21930, 1, 0xf941ce65, 257 }; static void multiply_through_pointer(void) @@ -53,6 +56,33 @@ static int32_t byte_local(void) return 3; } +static uint8_t shift_through_u16_pointer(void) +{ + uint8_t r; + { + uint16_t *q = &a2[1]; + r = (*q) >> (g1 & 15); + } + return r; +} + +static uint8_t divide_through_u16_pointer(void) +{ + uint8_t r; + { + uint16_t *q = &a2[1]; + r = (*q) / (g1 & 15); + } + return r; +} + +/* correct: the pointer is declared at function scope */ +static uint8_t shift_through_outer_pointer(void) +{ + uint16_t *q = &a2[1]; + return (*q) >> (g1 & 15); +} + int main(void) { multiply_through_pointer(); @@ -60,5 +90,8 @@ int main(void) printf("compare char %ld\n", (long)compare_through_pointer()); printf("compare int %ld\n", (long)compare_int_through_pointer()); printf("byte local %ld\n", (long)byte_local()); + printf("shift u16 %u\n", (unsigned)shift_through_u16_pointer()); + printf("divide u16 %u\n", (unsigned)divide_through_u16_pointer()); + printf("shift outer %u\n", (unsigned)shift_through_outer_pointer()); return 0; } diff --git a/tests/known-bugs/bug031.c b/tests/known-bugs/bug031.c index c46faaab5..7cea1ffc2 100644 --- a/tests/known-bugs/bug031.c +++ b/tests/known-bugs/bug031.c @@ -3,12 +3,14 @@ * member or pointer target with only its low byte loaded: the high byte * written is whatever was in the register. It is correct when the local is * declared at function scope, when it is initialised from a global, a - * constant or an expression, and when the destination is a plain global. */ + * constant or an expression, and when the destination is a plain global. It + * is wrong too when the local is first initialised with a constant and then + * assigned from the other local. */ #include #include struct S { uint16_t m; }; -static uint16_t a2[4] = { 129, 22830, 1, 63 }; +static uint16_t a2[5] = { 129, 22830, 1, 63, 9 }; static struct S s = { 7 }; static uint16_t g16 = 5; @@ -49,6 +51,18 @@ static void in_if(int16_t n, uint32_t x) } } +/* the same through an assignment: the local is initialised with a constant + * and then assigned the other local */ +static void assigned_after_init(int32_t x) +{ + int32_t y = x; + { + int32_t b = 0; + b = y; + a2[4] = b; + } +} + /* correct: declared at function scope */ static void function_scope(uint32_t x) { @@ -73,12 +87,14 @@ int main(void) member(0x00030201); through_pointer(0x00030201); in_if(1, 0x00030201); + assigned_after_init(0x00030201); function_scope(0x00030201); to_global(0x00030201); printf("element %04x\n", (unsigned)a2[1]); printf("member %04x\n", (unsigned)s.m); printf("pointer %04x\n", (unsigned)a2[2]); printf("in if %04x\n", (unsigned)a2[3]); + printf("assigned after init %04x\n", (unsigned)a2[4]); printf("function scope %04x\n", (unsigned)a2[0]); printf("global %04x\n", (unsigned)g16); return 0; diff --git a/tests/known-bugs/bug032.c b/tests/known-bugs/bug032.c index 4c5a2feb8..a5218f384 100644 --- a/tests/known-bugs/bug032.c +++ b/tests/known-bugs/bug032.c @@ -10,6 +10,7 @@ static uint32_t g6 = 256; static uint16_t g16 = 256; static uint8_t g8 = 69; +static int16_t sink; static uint16_t conditional(int16_t x) { @@ -34,6 +35,26 @@ static uint16_t add_255(int16_t x) return ((g6 * g6) <= 1) ? g8 : (uint16_t)(x + 255); } +/* the sum is the operand of a helper call (a division by a constant) that + * expects it in HL; it needs the loop and a global destination */ +static int16_t compound_then_divide(void) +{ + int16_t l = 1; + uint8_t w = 0; + while ((w++) < 1) + sink = (l += 225) / 121; + return sink; +} + +static int16_t compound_then_modulo(void) +{ + int16_t l = 1; + uint8_t w = 0; + while ((w++) < 1) + sink = (l += 225) % 121; + return sink; +} + /* correct: 256 does not fit the 8-bit add */ static uint16_t add_256(int16_t x) { @@ -55,5 +76,7 @@ int main(void) printf("add 255 %u %u\n", (unsigned)add_255(1), (unsigned)add_255(100)); printf("add 256 %u %u\n", (unsigned)add_256(1), (unsigned)add_256(100)); printf("narrow test %u %u\n", (unsigned)narrow_test(1), (unsigned)narrow_test(100)); + printf("compound divide %d\n", (int)compound_then_divide()); + printf("compound modulo %d\n", (int)compound_then_modulo()); return 0; } diff --git a/tests/known-bugs/bug035.c b/tests/known-bugs/bug035.c new file mode 100644 index 000000000..4ee8e10ca --- /dev/null +++ b/tests/known-bugs/bug035.c @@ -0,0 +1,159 @@ +/* BUG-035: with dccpeep, `x < low || x > high` style tests of a 16-bit local + * can compare the local with itself. dccpeep's preserve_ix_word_signed_range + * pass inserts a `push hl` after the first `ld l,(ix-N)` / `ld h,(ix-M)` of + * the local, on the assumption that the word is the left operand of the + * `sbc hl,de`. Here the local is the right operand and the other operand + * (a struct parameter's member) is already on the stack, so the inserted push + * makes `pop de` and `pop hl` both yield the local. The program is correct + * without dccpeep (dcc-peep=false). */ +#include +#include + +struct S1 { + int16_t m0; + uint16_t m1; + uint8_t m2; +}; + +static struct S1 s1 = { 0, 292, 255 }; +static int32_t r_loop, r_counter8, r_switch_var, r_no_switch, r_no_tail, r_plain; + +/* a 16-bit local assigned a narrowed constant, tested twice in a loop that + * holds a switch, and reassigned after the loop */ +static uint8_t with_loop(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; + for (i8 = 3; i8 > 1; i8 -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + r_loop = 1; + switch ((uint32_t)1) { + case 100: + break; + default: + break; + case 10: + break; + } + } + l2 = (uint16_t)1; + return 1; +} + +static uint8_t with_byte_counter(struct S1 x1) +{ + uint16_t l2 = 0; + uint8_t k; + l2 = (uint32_t)1550294082; + for (k = 3; k > 1; k -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + r_counter8 = 1; + switch ((uint32_t)1) { + case 100: + break; + default: + break; + case 10: + break; + } + } + l2 = (uint16_t)1; + return 1; +} + +static uint8_t switch_on_counter(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; + for (i8 = 3; i8 > 1; i8 -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + r_switch_var = 1; + switch ((uint32_t)i8) { + case 100: + break; + default: + break; + case 10: + break; + } + } + l2 = (uint16_t)1; + return 1; +} + +/* correct: no switch in the loop */ +static uint8_t without_switch(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; + for (i8 = 3; i8 > 1; i8 -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + r_no_switch = 1; + } + l2 = (uint16_t)1; + return 1; +} + +/* correct: no assignment after the loop */ +static uint8_t without_tail(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; + for (i8 = 3; i8 > 1; i8 -= 1) { + if (((uint16_t)x1.m0 > l2) || (l2 < (uint16_t)1)) + r_no_tail = 1; + switch ((uint32_t)1) { + case 100: + break; + default: + break; + case 10: + break; + } + } + return 1; +} + +/* correct: one test only */ +static uint8_t single_test(struct S1 x1) +{ + uint16_t l2 = 0; + uint32_t i8; + l2 = (uint32_t)1550294082; + for (i8 = 3; i8 > 1; i8 -= 1) { + if ((uint16_t)x1.m0 > l2) + r_plain = 1; + switch ((uint32_t)1) { + case 100: + break; + default: + break; + case 10: + break; + } + } + l2 = (uint16_t)1; + return 1; +} + +int main(void) +{ + r_loop = r_counter8 = r_switch_var = r_no_switch = r_no_tail = r_plain = 32768; + with_loop(s1); + with_byte_counter(s1); + switch_on_counter(s1); + without_switch(s1); + without_tail(s1); + single_test(s1); + printf("loop %ld\n", (long)r_loop); + printf("byte counter %ld\n", (long)r_counter8); + printf("switch on counter %ld\n", (long)r_switch_var); + printf("no switch %ld\n", (long)r_no_switch); + printf("no tail %ld\n", (long)r_no_tail); + printf("single test %ld\n", (long)r_plain); + return 0; +} From 4fc9bf01bd804e0603410589821fcefe3aad2ba9 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 18:58:54 +1100 Subject: [PATCH 06/14] Widen BUG-015 with lost int16 cast beside a nested-block 32-bit local, and steer the fuzzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 5 + docs/docs/en/images/clang-oracle-testing.svg | 150 +++++++++++++++++++ scripts/clang_oracle_gen.py | 9 +- tests/known-bugs/baselines/bug015.txt | 4 + tests/known-bugs/bug015.c | 50 ++++++- 5 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 docs/docs/en/images/clang-oracle-testing.svg diff --git a/compiler-bugs.md b/compiler-bugs.md index a73c9f5dd..49aa90de7 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -348,6 +348,11 @@ signed char l2 = -66; `{ int32_t b = 0; out = b + ((int16_t)((uint16_t)a6[1])); }` with `a6[1]` holding -1 gives 65535, and `if (b >= ((int16_t)((uint16_t)a6[1])))` is not taken. Correct with `b` at function scope, or with one cast. + It is not about `a6`: `{ int32_t b = 0; if (((int16_t)g16) > b) r = 1; }` + with a `uint16_t g16` of 0x8000 takes the branch, as does + `b + ((int16_t)g16)` (32768, not -32768) and + `(int16_t)((uint16_t)(1 << (g1 & 15)))` with `g1` at 15 (seed 6006). The + `(int16_t)` of an unsigned 16-bit operand is lost. - A nested-block pointer to a `uint16_t` element with the top bit set is shifted and divided as signed (found by fuzzing at `83dc7a91`; seeds 4245 and later): `{ uint16_t *q = &a2[1]; r = (*q) >> (g1 & 15); }` with diff --git a/docs/docs/en/images/clang-oracle-testing.svg b/docs/docs/en/images/clang-oracle-testing.svg new file mode 100644 index 000000000..90a4f04fb --- /dev/null +++ b/docs/docs/en/images/clang-oracle-testing.svg @@ -0,0 +1,150 @@ + + How dcc is tested against clang + A nine-step loop. Step 1: a generator turns a seed into a C program that uses only fixed-width types. Step 2: clang builds it with a checking layer under ASan and UBSan; operations that trip are repaired, and a program whose clang builds disagree is discarded. Step 3: the same source is built by clang on the host, giving the expected output, and by the dcc toolchain (dcc, dccpeep, m80c, l80c) and run under the ntvcm Z80 emulator, with and without dccpeep. Step 4: the outputs are compared; a program that matches is dropped, and one that is rejected, prints the wrong thing or hangs is a failure. Step 5: the failing program is reduced automatically to a few lines. Step 6: the failure is confirmed by hand with one-statement variants. Step 7: a reproducer is parked in tests/known-bugs with clang's output as its baseline. Step 8: the bug is logged in compiler-bugs.md and reported as a GitHub issue. Step 9: the generator gets a switch that stops it producing that shape, so later runs find different bugs. The loop then repeats from step 1. + + + + + + + + How dcc is tested against clang + Generated C programs are built by both compilers. Any difference in what they print is investigated. + + + + + 1 + Generate a program + One seed makes one C program: + loops, switch, structs, pointers, + bit-fields, calls, nested blocks. + Fixed-width types only, so it + means the same on both targets. + scripts/clang_oracle_gen.py + + + + 2 + Prove it is defined C + Run a checked build with ASan + and UBSan. Operations that trip + are repaired and it is run again. + clang -O0, -O2 and sanitized + builds must print the same. + Otherwise the program is discarded + + + + 3a + Host: clang + Its output is the expected result. + + + + 3b + Target: dcc toolchain + dcc → dccpeep → m80c → l80c, + run under ntvcm, a Z80 emulator. + With and without dccpeep: 4 configs + + + + + + + + + + + + 4 + Compare the outputs + Same in every config: the + program passes and is dropped. + Different: dcc rejected valid C, + printed the wrong thing, or hung. + Stack or 64K overflow is a target + limit and is not counted + + + + 5 + Reduce the failure + Shrink the program automatically + to the few lines that still fail + the same way. + Every step must still pass the + clang checks in step 2. + Never drifts into an already-known bug + + + + 6 + Confirm by hand + Write one-statement variants and + run each on clang and dcc to find + exactly what triggers it. + A new shape of a filed bug becomes + a comment on that issue. + Fuzzer faults are fixed, not filed + + + + + + + + + 7 + Park a reproducer + tests/known-bugs/bugNNN.c with + failing cases and controls. + Its baseline is clang's output, + proven under ASan and UBSan. + Outside the suite, which stays green + + + + 8 + Log and report + An entry in compiler-bugs.md: + what fails and what does not. + A GitHub issue with the full + program, commands, expected + and actual output. + + + + 9 + Steer round the bug + The generator gets a switch that + stops it producing that shape. + Later runs spend their time on + bugs that are not yet known. + Removed again when the bug is fixed + + + + + The loop repeats: with known shapes switched off, the next run finds different bugs + + + Why a difference points at dcc + The program has no undefined behaviour and its meaning does not depend on the size of int, + so clang and dcc should print exactly the same output. + diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index cbcf23e74..dff10d5fc 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -548,6 +548,9 @@ def expr(e, root): if is_int(lt) and lv.op == 'var' and not e.a[1] and not root \ and id(e) not in steps: found.add('postincvalue') + elif op == 'cast': + if e.t is I16 and ty(e.k[0]) is U16: + found.add('blockcast') elif op == 'un': if e.a in ('-', '~') and (logic_value(e.k[0]) or ( e.k[0].op != 'const' and is_constant(e.k[0]))): @@ -1248,7 +1251,7 @@ def fresh(self): 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', - 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum')) + 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum', 'blockcast')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1284,6 +1287,7 @@ def fresh(self): 'blockwidecopy': 'BUG-031', 'constdivzero': 'BUG-033', 'peepsum': 'BUG-032', + 'blockcast': 'BUG-015', } @@ -1310,6 +1314,9 @@ def mk(self, op, k=(), a=None, t=None): # the checking layer only repairs a division that runs self.prog.nid += 1 k = [k[0], E('const', (), 1, I16, self.prog.nid)] + if op == 'cast' and not self.on('blockcast') and t is I16 and ty(k[0]) is U16: + # a narrowing cast of an unsigned 16-bit value goes through 32 bits + k = [self.mk('cast', [k[0]], t=I32)] self.prog.nid += 1 return E(op, k, a, t, self.prog.nid) diff --git a/tests/known-bugs/baselines/bug015.txt b/tests/known-bugs/baselines/bug015.txt index 77fc15e37..fcf2884b7 100644 --- a/tests/known-bugs/baselines/bug015.txt +++ b/tests/known-bugs/baselines/bug015.txt @@ -2,6 +2,10 @@ multiply 0003ae4e compare char 0 compare int 0 byte local 1 +cast compare 0 +cast shift compare 0 +cast sum -32768 +cast compare outer 0 shift u16 1 divide u16 136 shift outer 1 diff --git a/tests/known-bugs/bug015.c b/tests/known-bugs/bug015.c index 06c06b016..1cf1d5823 100644 --- a/tests/known-bugs/bug015.c +++ b/tests/known-bugs/bug015.c @@ -3,7 +3,8 @@ * is computed in 16 bits and `c <= *q` is compared as signed; a nested-block * unsigned char is compared as unsigned int. A nested-block pointer to a * uint16_t with the top bit set is shifted and divided as signed. Each is - * correct when the local is declared at function scope. */ + * correct when the local is declared at function scope. Beside a nested-block + * 32-bit local, `(int16_t)` of an unsigned 16-bit value is lost as an operand. */ #include #include @@ -11,6 +12,7 @@ struct S { uint16_t m5 : 6; }; static struct S s = { 24 }; static uint16_t a2[2] = { 21930, 0x8001 }; static uint8_t g1 = 239; +static uint16_t g16 = 0x8000; static uint32_t a4[8] = { 1, 0xaa55, 7, 100, 21930, 1, 0xf941ce65, 257 }; static void multiply_through_pointer(void) @@ -56,6 +58,48 @@ static int32_t byte_local(void) return 3; } +static int32_t cast_compare(void) +{ + int32_t r = 0; + { + int32_t b = 0; + if (((int16_t)g16) > b) + r = 1; + } + return r; +} + +static int32_t cast_shift_compare(void) +{ + int32_t r = 0; + { + int32_t b = 0; + if (((int16_t)((uint16_t)(((uint16_t)1) << (g1 & 15)))) > b) + r = 1; + } + return r; +} + +static int32_t cast_sum(void) +{ + int32_t r; + { + int32_t b = 0; + r = b + ((int16_t)g16); + } + return r; +} + +/* correct: the local is declared at function scope */ +static int32_t cast_compare_outer(void) +{ + int32_t r = 0; + int32_t b = 0; + if (((int16_t)g16) > b) + r = 1; + return r; +} + static uint8_t shift_through_u16_pointer(void) { uint8_t r; @@ -90,6 +134,10 @@ int main(void) printf("compare char %ld\n", (long)compare_through_pointer()); printf("compare int %ld\n", (long)compare_int_through_pointer()); printf("byte local %ld\n", (long)byte_local()); + printf("cast compare %ld\n", (long)cast_compare()); + printf("cast shift compare %ld\n", (long)cast_shift_compare()); + printf("cast sum %ld\n", (long)cast_sum()); + printf("cast compare outer %ld\n", (long)cast_compare_outer()); printf("shift u16 %u\n", (unsigned)shift_through_u16_pointer()); printf("divide u16 %u\n", (unsigned)divide_through_u16_pointer()); printf("shift outer %u\n", (unsigned)shift_through_outer_pointer()); From 0d80a62b885664dd98f37480aba24c314c809486 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 19:41:44 +1100 Subject: [PATCH 07/14] Narrow the blockcast generator check to the nested-block 32-bit operand shape Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/clang_oracle_gen.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index dff10d5fc..47593d940 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -469,6 +469,12 @@ def const_jump(s): for x in sub.stmts) +def has_block_wide(e): + if e.op == 'var': + return e.a.storage == 'block' and is_int(e.a.t) and e.a.t.bits == 32 + return any(has_block_wide(k) for k in e.k) + + def known_shapes(prog): """The features in prog.off whose bug shape the program contains anyway. @@ -548,9 +554,6 @@ def expr(e, root): if is_int(lt) and lv.op == 'var' and not e.a[1] and not root \ and id(e) not in steps: found.add('postincvalue') - elif op == 'cast': - if e.t is I16 and ty(e.k[0]) is U16: - found.add('blockcast') elif op == 'un': if e.a in ('-', '~') and (logic_value(e.k[0]) or ( e.k[0].op != 'const' and is_constant(e.k[0]))): @@ -558,6 +561,10 @@ def expr(e, root): elif op == 'bin': if same_variable(e.k[0], e.k[1]): found.add('selfoperand') + for i in (0, 1): + c, o = e.k[i], e.k[1 - i] + if c.op == 'cast' and c.t is I16 and ty(c.k[0]) is U16 and has_block_wide(o): + found.add('blockcast') if const_div_zero(e): found.add('constdivzero') elif op == 'call': @@ -1314,9 +1321,13 @@ def mk(self, op, k=(), a=None, t=None): # the checking layer only repairs a division that runs self.prog.nid += 1 k = [k[0], E('const', (), 1, I16, self.prog.nid)] - if op == 'cast' and not self.on('blockcast') and t is I16 and ty(k[0]) is U16: - # a narrowing cast of an unsigned 16-bit value goes through 32 bits - k = [self.mk('cast', [k[0]], t=I32)] + if op == 'bin' and not self.on('blockcast'): + for i in (0, 1): + c = k[i] + if c.op == 'cast' and c.t is I16 and ty(c.k[0]) is U16 and has_block_wide(k[1 - i]): + # compare and combine the unsigned value without the signed cast + k = list(k) + k[i] = self.mk('cast', [c.k[0]], t=I32) self.prog.nid += 1 return E(op, k, a, t, self.prog.nid) From 5641da133ad389213755fd96fa2fc8f7d983a197 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 19:57:09 +1100 Subject: [PATCH 08/14] Log BUG-036, widen BUG-015 with a wrapped nested u16, and steer the fuzzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 33 +++++++++- scripts/clang_oracle_gen.py | 18 +++++- tests/known-bugs/baselines/bug015.txt | 2 + tests/known-bugs/baselines/bug036.txt | 6 ++ tests/known-bugs/bug015.c | 21 +++++++ tests/known-bugs/bug036.c | 88 +++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 tests/known-bugs/baselines/bug036.txt create mode 100644 tests/known-bugs/bug036.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 49aa90de7..2e6464d2c 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -55,8 +55,9 @@ has been fixed by this work; this file is the log. | [BUG-033](#bug-033) | #253 | rejects valid C | A constant divided by a constant zero, in code that never runs | | [BUG-034](#bug-034) | #254 | wrong result (`dccpeep` only) | `dccpeep` turns `8 == v` into a test of bit 3 | | [BUG-035](#bug-035) | #255 | wrong result (`dccpeep` only) | `dccpeep` compares a 16-bit local with itself in `a > x \|\| x < k` after a struct member on the stack | +| [BUG-036](#bug-036) | #256 | wrong result | A 32-bit local assigned in a loop and passed directly to a call keeps its old value after the loop | -Thirty-three are open: twenty-one give wrong results with no diagnostic +Thirty-four are open: twenty-two give wrong results with no diagnostic (BUG-030 can also hang; BUG-032, BUG-034 and BUG-035 need `dccpeep`), one hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the @@ -359,6 +360,10 @@ signed char l2 = -66; `a2[1]` = 0x8001 and `g1` = 239 gives 255, not 1, and `(*q) / (g1 & 15)` gives 120, not 136. `*q >> 15` is wrong too; `r = (*q)` and `&` or `+` are right, and so is a pointer declared at function scope. +- A nested-block `uint16_t` that wraps is widened as signed (seed 6648): + `{ uint16_t b = 0; --b; g9 = b; }` with a 32-bit `g9` gives -1, not 65535; + `b--` and `(uint16_t)b` are wrong too, `b -= 1`, `b = 5; --b`, a store to a + 16-bit target and a function-scope `b` are right. ## BUG-016 @@ -1062,6 +1067,32 @@ What `dccpeep` makes of the first test (`pop hl` / `pop de` read the same word): instead of two. - Reproduces with `5f733050`, `32d3e46d` and `5bcac74e`. +## BUG-036 + +**A 32-bit local that is assigned inside a loop and passed directly to a call +keeps its old value after the loop.** The call gets the constant, but the +store to the local is dropped; the local is then reloaded from a stack slot +that was never written. + +```c +int32_t l = 0; +uint16_t i; +for (i = 5; i != 0; i--) { + l = 1; + take32(l); /* static uint32_t take32(int32_t x) */ +} +return l; /* want 1, dcc 0 */ +``` + +- Wrong for `for`, `while` and counted loops, a constant or a variable + (`l = i;`), and with the call result stored or ignored; `l == 1` after the + loop is false. +- Correct when the argument is an expression (`take32(l + 1)`), when the call + does not take `l`, or when it takes a 16-bit parameter (`take16((int16_t)l)`). +- Wrong with `dcc-peep` on and off. Found by fuzzing at `83dc7a91` (seed + 6826); the reduced program had a `for`-init declaration, but a counter + declared outside the loop fails the same. + --- ## Fixed upstream diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 47593d940..13fea5563 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -542,6 +542,8 @@ def expr(e, root): elif op == 'incdec': lv = e.k[0] lt = ty(lv) + if lv.op == 'var' and lv.a.storage == 'block' and lt is U16: + found.add('blockdec') local = lv.op == 'var' and lv.a.storage != 'global' if is_int(lt) and lt.bits == 32: if lv.op in ('idx', 'pidx') or (lv.op == 'fld' and lv.a.dims): @@ -568,6 +570,9 @@ def expr(e, root): if const_div_zero(e): found.add('constdivzero') elif op == 'call': + if any(a.op == 'var' and a.a.storage != 'global' and is_int(a.a.t) + and a.a.t.bits == 32 for a in e.k): + found.add('loopcallarg') if e.a.via_ptr and any(is_int(p.t) and ty(a) is not p.t for a, p in zip(e.k, e.a.params)): found.add('ptrcallconv') @@ -1258,7 +1263,8 @@ def fresh(self): 'blockwideneg', 'nestedwideless', 'bfpromo', 'widelocalinc', 'blockptr', 'inittype', 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', - 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum', 'blockcast')) + 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum', 'blockcast', + 'blockdec', 'loopcallarg')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1295,6 +1301,8 @@ def fresh(self): 'constdivzero': 'BUG-033', 'peepsum': 'BUG-032', 'blockcast': 'BUG-015', + 'blockdec': 'BUG-015', + 'loopcallarg': 'BUG-036', } @@ -1321,6 +1329,14 @@ def mk(self, op, k=(), a=None, t=None): # the checking layer only repairs a division that runs self.prog.nid += 1 k = [k[0], E('const', (), 1, I16, self.prog.nid)] + if op == 'incdec' and not self.on('blockdec') and k[0].op == 'var' \ + and k[0].a.storage == 'block' and ty(k[0]) is U16: + # a nested-block unsigned 16-bit local is changed with a compound assignment + return self.mk('asg', [k[0], self.small(1, 1)], a[0][0] + '=') + if op == 'call' and not self.on('loopcallarg'): + k = [self.mk('bin', [x, self.small(1, 1)], '^') if x.op == 'var' + and x.a.storage != 'global' and is_int(x.a.t) and x.a.t.bits == 32 else x + for x in k] if op == 'bin' and not self.on('blockcast'): for i in (0, 1): c = k[i] diff --git a/tests/known-bugs/baselines/bug015.txt b/tests/known-bugs/baselines/bug015.txt index fcf2884b7..9ba80f941 100644 --- a/tests/known-bugs/baselines/bug015.txt +++ b/tests/known-bugs/baselines/bug015.txt @@ -6,6 +6,8 @@ cast compare 0 cast shift compare 0 cast sum -32768 cast compare outer 0 +wrapped decrement 0000ffff +wrapped decrement outer 0000ffff shift u16 1 divide u16 136 shift outer 1 diff --git a/tests/known-bugs/baselines/bug036.txt b/tests/known-bugs/baselines/bug036.txt new file mode 100644 index 000000000..c92a138cb --- /dev/null +++ b/tests/known-bugs/baselines/bug036.txt @@ -0,0 +1,6 @@ +for 1 +while 1 +counted 7 +test 100 +expression 1 +narrow 1 diff --git a/tests/known-bugs/bug015.c b/tests/known-bugs/bug015.c index 1cf1d5823..43ed36f31 100644 --- a/tests/known-bugs/bug015.c +++ b/tests/known-bugs/bug015.c @@ -100,6 +100,25 @@ static int32_t cast_compare_outer(void) return r; } +static uint32_t wrapped_decrement(void) +{ + uint32_t r; + { + uint16_t b = 0; + --b; + r = b; + } + return r; +} + +/* correct: the local is declared at function scope */ +static uint32_t wrapped_decrement_outer(void) +{ + uint16_t b = 0; + --b; + return b; +} + static uint8_t shift_through_u16_pointer(void) { uint8_t r; @@ -138,6 +157,8 @@ int main(void) printf("cast shift compare %ld\n", (long)cast_shift_compare()); printf("cast sum %ld\n", (long)cast_sum()); printf("cast compare outer %ld\n", (long)cast_compare_outer()); + printf("wrapped decrement %08lx\n", (unsigned long)wrapped_decrement()); + printf("wrapped decrement outer %08lx\n", (unsigned long)wrapped_decrement_outer()); printf("shift u16 %u\n", (unsigned)shift_through_u16_pointer()); printf("divide u16 %u\n", (unsigned)divide_through_u16_pointer()); printf("shift outer %u\n", (unsigned)shift_through_outer_pointer()); diff --git a/tests/known-bugs/bug036.c b/tests/known-bugs/bug036.c new file mode 100644 index 000000000..5b196393e --- /dev/null +++ b/tests/known-bugs/bug036.c @@ -0,0 +1,88 @@ +/* BUG-036: a 32-bit local assigned a value inside a loop and passed directly + * to a call loses the assignment: after the loop it still holds its old value. + * Correct when the argument is an expression (`l + 1`), is another value, or + * when the local is not passed to a call. */ +#include +#include + +static uint32_t take32(int32_t x) { return (uint32_t)x + 1; } +static uint32_t take16(int16_t x) { return (uint32_t)x + 1; } +static uint32_t sink; + +static int32_t for_loop(void) +{ + int32_t l = 0; + uint16_t i; + for (i = 5; i != 0; i--) { + l = 1; + take32(l); + } + return l; +} + +static int32_t while_loop(void) +{ + int32_t l = 0; + uint16_t i = 5; + while (i != 0) { + i--; + l = 1; + sink = take32(l); + } + return l; +} + +static int32_t counted_loop(void) +{ + int32_t l = 0; + uint16_t i; + for (i = 0; i < 5; i++) { + l = 7; + take32(l); + } + return l; +} + +static int32_t used_in_test(void) +{ + int32_t l = 0; + uint16_t i; + for (i = 5; i != 0; i--) { + l = 1; + take32(l); + } + return l == 1 ? 100 : 200; +} + +static int32_t expression_argument(void) +{ + int32_t l = 0; + uint16_t i; + for (i = 5; i != 0; i--) { + l = 1; + take32(l + 1); + } + return l; +} + +static int32_t narrow_argument(void) +{ + int32_t l = 0; + uint16_t i; + for (i = 5; i != 0; i--) { + l = 1; + take16((int16_t)l); + } + return l; +} + +int main(void) +{ + printf("for %ld\n", (long)for_loop()); + printf("while %ld\n", (long)while_loop()); + printf("counted %ld\n", (long)counted_loop()); + printf("test %ld\n", (long)used_in_test()); + printf("expression %ld\n", (long)expression_argument()); + printf("narrow %ld\n", (long)narrow_argument()); + return 0; +} From a77e215dc10d9c146e8f00be706302d0d36ac39c Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 20:18:26 +1100 Subject: [PATCH 09/14] Log BUG-037, widen BUG-034, and steer the fuzzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 39 ++++++++++++++- scripts/clang_oracle_gen.py | 10 +++- tests/known-bugs/baselines/bug034.txt | 6 +++ tests/known-bugs/baselines/bug037.txt | 6 +++ tests/known-bugs/bug034.c | 12 +++++ tests/known-bugs/bug037.c | 69 +++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/known-bugs/baselines/bug037.txt create mode 100644 tests/known-bugs/bug037.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 2e6464d2c..956e40fbc 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -56,8 +56,9 @@ has been fixed by this work; this file is the log. | [BUG-034](#bug-034) | #254 | wrong result (`dccpeep` only) | `dccpeep` turns `8 == v` into a test of bit 3 | | [BUG-035](#bug-035) | #255 | wrong result (`dccpeep` only) | `dccpeep` compares a 16-bit local with itself in `a > x \|\| x < k` after a struct member on the stack | | [BUG-036](#bug-036) | #256 | wrong result | A 32-bit local assigned in a loop and passed directly to a call keeps its old value after the loop | +| [BUG-037](#bug-037) | #257 | wrong result | In a `for` or `while` loop with a 32-bit counter, `l = i;` copies the counter after its increment | -Thirty-four are open: twenty-two give wrong results with no diagnostic +Thirty-five are open: twenty-three give wrong results with no diagnostic (BUG-030 can also hang; BUG-032, BUG-034 and BUG-035 need `dccpeep`), one hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the @@ -1012,6 +1013,10 @@ static int16_t f(uint16_t x) `c == 8` on a `char` parameter or local in an `if`, a loop, a `switch` or as a value (the simple forms do not produce the three-instruction pattern, so `if (c == '\b')` is safe as far as these probes go). +- Also wrong when the 16-bit operand is a signed bit-field read through a + cast (seed 7161): `8 == (int16_t)s.m` with a 5-bit `m` of 9, 12 or 15, + and `uint32_t l = (8 == (int16_t)s.m) ? 0 : 1;`. A plain `int16_t` global + and `8 == s.m` without the cast are right. - The rewrite has been in the file since at least `3dd3b434` (2026-06-14). ## BUG-035 @@ -1093,6 +1098,38 @@ return l; /* want 1, dcc 0 */ 6826); the reduced program had a `for`-init declaration, but a counter declared outside the loop fails the same. +## BUG-037 + +**In a `for` or `while` loop whose counter is 32 bits wide, a copy of the +counter taken in the body is stored after the increment.** The copy holds the +next value, so it is one step ahead. A `do … while`, a 16-bit counter and +straight-line code are correct. + +```c +int32_t l = 0, i; +for (i = 2; i < 3; i += 1) + l = i; +return l; /* want 2, dcc 3 */ +``` + +dcc emits the increment first and then the copy, which reads the new `i`: + +``` + inc (ix-6) ; i += 1, in place + ... + ld l,(ix-6) ; l = i + ld h,(ix-5) + ld e,(ix-8) + ld d,(ix-7) + ld (ix-2),l +``` + +- Wrong for `i += 1` and `i++` in a `for`, and for `i += 1` at the end of a + `while` body (`while (i < 3) { l = i; i += 1; }`). +- Correct for `int16_t` locals, `do … while`, `i = i + 1` and `i += 2` in a + `do … while`, and a copy outside a loop. +- Found by fuzzing at `83dc7a91` (seed 7581), with and without `dcc-peep`. + --- ## Fixed upstream diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 13fea5563..8905276ac 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -512,6 +512,10 @@ def expr(e, root): if lv.a.storage == 'block' and lt.bits == 32 and rhs.op == 'var' \ and rhs.a.storage != 'global': found.add('blockwidecopy') + if lv.a.storage != 'global' and lt.bits == 32 and rhs.op == 'var' \ + and rhs.a.storage != 'global' and is_int(rhs.a.t) \ + and rhs.a.t.bits == 32: + found.add('loopcopy') if lv.a.storage != 'global' and lt.bits == 16 and cross_type(rhs, lt): found.add('crosstypeasg') elif is_int(lt): @@ -1264,7 +1268,7 @@ def fresh(self): 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum', 'blockcast', - 'blockdec', 'loopcallarg')) + 'blockdec', 'loopcallarg', 'loopcopy')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1303,6 +1307,7 @@ def fresh(self): 'blockcast': 'BUG-015', 'blockdec': 'BUG-015', 'loopcallarg': 'BUG-036', + 'loopcopy': 'BUG-037', } @@ -1704,6 +1709,9 @@ def stored(self, rhs, t, storage, init=False): if not self.on('blockwidecopy') and storage == 'block' and t.bits == 32 \ and rhs.op == 'var' and rhs.a.storage != 'global': rhs = self.mk('bin', [rhs, self.small(1, 1)], '^') + if not self.on('loopcopy') and t.bits == 32 and rhs.op == 'var' \ + and rhs.a.storage != 'global' and is_int(rhs.a.t) and rhs.a.t.bits == 32: + rhs = self.mk('bin', [rhs, self.small(1, 1)], '^') if not init and not self.on('crosstypeasg') and t.bits == 16 and cross_type(rhs, t): # a cast that changes width as well as type keeps the value rhs = self.mk('cast', [self.mk('cast', [rhs], t=I32)], t=t) diff --git a/tests/known-bugs/baselines/bug034.txt b/tests/known-bugs/baselines/bug034.txt index be5e2e4a8..adca5c970 100644 --- a/tests/known-bugs/baselines/bug034.txt +++ b/tests/known-bugs/baselines/bug034.txt @@ -1,6 +1,12 @@ +bit-field 8: 100 8: equal 100 not equal 85 sixteen 85 +bit-field 7: 85 7: equal 85 not equal 1 sixteen 85 +bit-field 9: 85 9: equal 85 not equal 1 sixteen 85 +bit-field 12: 85 12: equal 85 not equal 1 sixteen 85 +bit-field 15: 85 47: equal 85 not equal 1 sixteen 85 +bit-field 0: 85 16: equal 85 not equal 1 sixteen 100 diff --git a/tests/known-bugs/baselines/bug037.txt b/tests/known-bugs/baselines/bug037.txt new file mode 100644 index 000000000..c0f22f5e5 --- /dev/null +++ b/tests/known-bugs/baselines/bug037.txt @@ -0,0 +1,6 @@ +for 2 +for postinc 12 +while 2 +narrow 2 +do 2 +straight 2 diff --git a/tests/known-bugs/bug034.c b/tests/known-bugs/bug034.c index fe8f40d17..3a396a3e2 100644 --- a/tests/known-bugs/bug034.c +++ b/tests/known-bugs/bug034.c @@ -8,6 +8,7 @@ #include static uint8_t g8; +static struct { int16_t m : 5; } bf; static int16_t equal(uint16_t x) { @@ -34,12 +35,23 @@ static int16_t sixteen(uint16_t x) return x; } +/* wrong too: a signed bit-field read through a cast */ +static int16_t bitfield_equal(uint16_t x) +{ + if (8 == (int16_t)bf.m) { + x = 100; + } + return x; +} + int main(void) { static const uint8_t values[6] = { 8, 7, 9, 12, 47, 16 }; uint8_t i; for (i = 0; i < 6; i++) { g8 = values[i]; + bf.m = (int16_t)(values[i] & 15); + printf("bit-field %d: %d\n", (int)bf.m, bitfield_equal(85)); printf("%u: equal %d not equal %d sixteen %d\n", (unsigned)g8, equal(85), not_equal(85), sixteen(85)); } diff --git a/tests/known-bugs/bug037.c b/tests/known-bugs/bug037.c new file mode 100644 index 000000000..be6bbf588 --- /dev/null +++ b/tests/known-bugs/bug037.c @@ -0,0 +1,69 @@ +/* BUG-037: in a `for` or `while` loop whose 32-bit counter is incremented, + * a copy of the counter taken in the body (`l = i;`) is stored after the + * increment, so it holds the next value. Correct for 16-bit counters, for + * `do ... while`, and outside a loop. */ +#include +#include + +static int32_t for_loop(void) +{ + int32_t l = 0, i; + for (i = 2; i < 3; i += 1) + l = i; + return l; +} + +static int32_t for_loop_postinc(void) +{ + int32_t l = 0, i; + for (i = 10; i < 13; i++) + l = i; + return l; +} + +static int32_t while_loop(void) +{ + int32_t l = 0, i = 2; + while (i < 3) { + l = i; + i += 1; + } + return l; +} + +static int32_t narrow_counter(void) +{ + int16_t l = 0, i; + for (i = 2; i < 3; i += 1) + l = i; + return l; +} + +static int32_t do_loop(void) +{ + int32_t l = 0, i = 2; + do { + l = i; + i += 1; + } while (i < 3); + return l; +} + +static int32_t straight_line(void) +{ + int32_t l = 0, i = 2; + l = i; + i += 1; + return l; +} + +int main(void) +{ + printf("for %ld\n", (long)for_loop()); + printf("for postinc %ld\n", (long)for_loop_postinc()); + printf("while %ld\n", (long)while_loop()); + printf("narrow %ld\n", (long)narrow_counter()); + printf("do %ld\n", (long)do_loop()); + printf("straight %ld\n", (long)straight_line()); + return 0; +} From 910307ed197e1eed8c9ff02c3c0a551628f67f61 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Fri, 13 Nov 2026 21:08:50 +1100 Subject: [PATCH 10/14] Log BUG-038 and steer the fuzzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 36 +++++++++++- scripts/clang_oracle_gen.py | 23 +++++++- tests/known-bugs/baselines/bug038.txt | 4 ++ tests/known-bugs/bug038.c | 80 +++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/known-bugs/baselines/bug038.txt create mode 100644 tests/known-bugs/bug038.c diff --git a/compiler-bugs.md b/compiler-bugs.md index 956e40fbc..d5926c88d 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -57,8 +57,9 @@ has been fixed by this work; this file is the log. | [BUG-035](#bug-035) | #255 | wrong result (`dccpeep` only) | `dccpeep` compares a 16-bit local with itself in `a > x \|\| x < k` after a struct member on the stack | | [BUG-036](#bug-036) | #256 | wrong result | A 32-bit local assigned in a loop and passed directly to a call keeps its old value after the loop | | [BUG-037](#bug-037) | #257 | wrong result | In a `for` or `while` loop with a 32-bit counter, `l = i;` copies the counter after its increment | +| [BUG-038](#bug-038) | #258 | wrong result | A 16-bit local assigned in one arm of an `if … else`, then updated, loses the assignment when an early-return `if` follows | -Thirty-five are open: twenty-three give wrong results with no diagnostic +Thirty-six are open: twenty-four give wrong results with no diagnostic (BUG-030 can also hang; BUG-032, BUG-034 and BUG-035 need `dccpeep`), one hangs, ten stop the compile (BUG-007 can instead compile and crash), and one (BUG-022) turns a compile error into a broken program. BUG-026 is the @@ -1130,6 +1131,39 @@ dcc emits the increment first and then the copy, which reads the new `i`: `do … while`, and a copy outside a loop. - Found by fuzzing at `83dc7a91` (seed 7581), with and without `dcc-peep`. +## BUG-038 + +**A 16-bit local that is assigned in one arm of an `if … else` and then +updated loses the assignment when an `if` that can return early follows.** +The update is applied to the old value. It is the loss in BUG-027 with no +outer `if` and no loop. + +```c +unsigned short l = 0; +if (z == 0) { /* volatile int16_t z = 1 */ +} else { + l = 1; +} +l -= 129; +if (!arr[1]) { /* arr[1] is 5 */ + return 1; +} +sink = l; /* unsigned char sink: want 128, dcc 127 */ +return sink; +``` + +- Wrong for `l -= 129`, `l += 2` (want 3, dcc 2), an `int16_t` local, an + assignment that is the condition of a nested `if` (`if (l = 1) { }`), and + with and without `dcc-peep`. +- Correct without the update (`if (!arr[1]) return 1; sink = l;`). +- Without the early-return `if`, `dcc-peep` is still wrong and plain `dcc` is + correct in a standalone program. In a larger file with other functions the + same code gave different answers, so the result depends on what else is in + the file. +- Found by fuzzing at `83dc7a91` (seed 9056). The reduced program had + `if ((int32_t)0) { } else { if (l2 = 1) { } }`, which fails in every + configuration including the forced MIR candidates. + --- ## Fixed upstream diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 8905276ac..6588b03b2 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -403,6 +403,22 @@ def byte_arm(s): return False +def word_arm(s): + """s is an `if … else` with a plain assignment to a 16-bit local directly + in one of its arms, or as the condition of an `if` there: the BUG-038 + shape when the local is updated and an early return follows.""" + if s.op != 'if' or len(s.b) < 2 or s.b[1] is None: + return False + for arm in s.b: + for t in arm.stmts: + e = t.e[0] if t.e and t.op in ('expr', 'if') else None + if e is not None and e.op == 'asg' and e.a == '=' and e.k[0].op == 'var' \ + and e.k[0].a.storage != 'global' and is_int(e.k[0].a.t) \ + and e.k[0].a.t.bits == 16: + return True + return False + + def block_return(ret, e): """`return e;` to return type ret, where e is a nested-block object that needs converting: the BUG-025 shape.""" @@ -605,6 +621,8 @@ def decl(d): found.add('nestedifelse') if byte_arm(s): found.add('bytearm') + if word_arm(s): + found.add('wordarm') if s.op == 'for' and s.a is not None: decl(s.a) for e in s.e: @@ -1268,7 +1286,7 @@ def fresh(self): 'selfand', 'widecasgbf', 'widehelperparam', 'postincvalue', 'duplabel', 'crosstypeasg', 'constjump', 'blockret', 'selfoperand', 'nestedifelse', 'bytesarcount', 'bytearm', 'armfordecl', 'blockwidecopy', 'constdivzero', 'peepsum', 'blockcast', - 'blockdec', 'loopcallarg', 'loopcopy')) + 'blockdec', 'loopcallarg', 'loopcopy', 'wordarm')) # Features switched off by `--off known`, with the entry in compiler-bugs.md # that each one would otherwise keep rediscovering. Remove a feature from this @@ -1308,6 +1326,7 @@ def fresh(self): 'blockdec': 'BUG-015', 'loopcallarg': 'BUG-036', 'loopcopy': 'BUG-037', + 'wordarm': 'BUG-038', } @@ -1824,6 +1843,8 @@ def st_if(self, c, depth, b): blocks.append(self.gen_block(c, depth - 1, r.randint(1, 2), decls=r.random() < 0.2)) c.in_if -= 1 s = S('if', [cond], blocks) + if not self.on('wordarm') and word_arm(s): + del s.b[1:] if not self.on('bytearm') and byte_arm(s): # keep the assignment, lose the other arm del s.b[1:] diff --git a/tests/known-bugs/baselines/bug038.txt b/tests/known-bugs/baselines/bug038.txt new file mode 100644 index 000000000..245cc3f85 --- /dev/null +++ b/tests/known-bugs/baselines/bug038.txt @@ -0,0 +1,4 @@ +else arm 128 +add constant 3 +signed local 128 +no update 1 diff --git a/tests/known-bugs/bug038.c b/tests/known-bugs/bug038.c new file mode 100644 index 000000000..76d106420 --- /dev/null +++ b/tests/known-bugs/bug038.c @@ -0,0 +1,80 @@ +/* BUG-038: a 16-bit local assigned in one arm of an `if … else` and then + * updated loses the assignment when an `if` that may return early follows + * and the local is used after it: the update is applied to the old value. + * The same loss as BUG-027 without the outer `if` and without a loop. It is + * correct when the local is not updated. */ +#include +#include + +static int16_t arr[2] = { 0, 5 }; +volatile int16_t z = 1; +unsigned char sink; + +static int else_arm(void) +{ + unsigned short l = 0; + if (z == 0) { + } else { + l = 1; + } + l -= 129; + if (!arr[1]) { + return 1; + } + sink = l; + return sink; +} + +static int add_constant(void) +{ + unsigned short l = 0; + if (z == 0) { + } else { + l = 1; + } + l += 2; + if (!arr[1]) { + return 1; + } + sink = l; + return sink; +} + +static int signed_local(void) +{ + int16_t l = 0; + if (z == 0) { + } else { + l = 1; + } + l -= 129; + if (!arr[1]) { + return 1; + } + sink = l; + return sink; +} + +/* correct: the local is not updated */ +static int no_update(void) +{ + unsigned short l = 0; + if (z == 0) { + } else { + l = 1; + } + if (!arr[1]) { + return 1; + } + sink = l; + return sink; +} + +int main(void) +{ + printf("else arm %d\n", else_arm()); + printf("add constant %d\n", add_constant()); + printf("signed local %d\n", signed_local()); + printf("no update %d\n", no_update()); + return 0; +} From 949ef7473e1fb9c245979acf5e0fff1f599acde2 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Sat, 14 Nov 2026 01:48:17 +1100 Subject: [PATCH 11/14] Add the float fuzz generator and log BUG-039 to BUG-053 A float-typed program generator for clang-oracle-fuzz.py (--gen float), with repair, known-shape steering and per-expression sequencing so clang does not reject generated programs as unsequenced. Reproducers and baselines for BUG-039..053 under tests/known-bugs, and facets of BUG-015, 016, 019, 020, 026, 036 and 050 in compiler-bugs.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 532 +++++++++- scripts/README.md | 20 +- scripts/clang-oracle-fuzz.py | 27 +- scripts/clang_oracle_float_gen.py | 1341 +++++++++++++++++++++++++ scripts/clang_oracle_gen.py | 118 ++- tests/known-bugs/baselines/bug039.txt | 404 ++++++++ tests/known-bugs/baselines/bug040.txt | 3 + tests/known-bugs/baselines/bug041.txt | 10 + tests/known-bugs/baselines/bug042.txt | 10 + tests/known-bugs/baselines/bug043.txt | 3 + tests/known-bugs/baselines/bug044.txt | 12 + tests/known-bugs/baselines/bug045.txt | 2 + tests/known-bugs/baselines/bug046.txt | 3 + tests/known-bugs/baselines/bug047.txt | 14 + tests/known-bugs/baselines/bug048.txt | 6 + tests/known-bugs/baselines/bug049.txt | 5 + tests/known-bugs/baselines/bug050.txt | 4 + tests/known-bugs/baselines/bug051.txt | 1 + tests/known-bugs/baselines/bug052.txt | 9 + tests/known-bugs/baselines/bug053.txt | 6 + tests/known-bugs/bug039.c | 47 + tests/known-bugs/bug040.c | 24 + tests/known-bugs/bug041.c | 39 + tests/known-bugs/bug042.c | 37 + tests/known-bugs/bug043.c | 36 + tests/known-bugs/bug044.c | 44 + tests/known-bugs/bug045.c | 35 + tests/known-bugs/bug046.c | 30 + tests/known-bugs/bug047.c | 54 + tests/known-bugs/bug048.c | 36 + tests/known-bugs/bug049.c | 22 + tests/known-bugs/bug050.c | 32 + tests/known-bugs/bug051.c | 30 + tests/known-bugs/bug052.c | 46 + tests/known-bugs/bug053.c | 96 ++ 35 files changed, 3119 insertions(+), 19 deletions(-) create mode 100644 scripts/clang_oracle_float_gen.py create mode 100644 tests/known-bugs/baselines/bug039.txt create mode 100644 tests/known-bugs/baselines/bug040.txt create mode 100644 tests/known-bugs/baselines/bug041.txt create mode 100644 tests/known-bugs/baselines/bug042.txt create mode 100644 tests/known-bugs/baselines/bug043.txt create mode 100644 tests/known-bugs/baselines/bug044.txt create mode 100644 tests/known-bugs/baselines/bug045.txt create mode 100644 tests/known-bugs/baselines/bug046.txt create mode 100644 tests/known-bugs/baselines/bug047.txt create mode 100644 tests/known-bugs/baselines/bug048.txt create mode 100644 tests/known-bugs/baselines/bug049.txt create mode 100644 tests/known-bugs/baselines/bug050.txt create mode 100644 tests/known-bugs/baselines/bug051.txt create mode 100644 tests/known-bugs/baselines/bug052.txt create mode 100644 tests/known-bugs/baselines/bug053.txt create mode 100644 tests/known-bugs/bug039.c create mode 100644 tests/known-bugs/bug040.c create mode 100644 tests/known-bugs/bug041.c create mode 100644 tests/known-bugs/bug042.c create mode 100644 tests/known-bugs/bug043.c create mode 100644 tests/known-bugs/bug044.c create mode 100644 tests/known-bugs/bug045.c create mode 100644 tests/known-bugs/bug046.c create mode 100644 tests/known-bugs/bug047.c create mode 100644 tests/known-bugs/bug048.c create mode 100644 tests/known-bugs/bug049.c create mode 100644 tests/known-bugs/bug050.c create mode 100644 tests/known-bugs/bug051.c create mode 100644 tests/known-bugs/bug052.c create mode 100644 tests/known-bugs/bug053.c diff --git a/compiler-bugs.md b/compiler-bugs.md index d5926c88d..72f1ba4a6 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -58,13 +58,30 @@ has been fixed by this work; this file is the log. | [BUG-036](#bug-036) | #256 | wrong result | A 32-bit local assigned in a loop and passed directly to a call keeps its old value after the loop | | [BUG-037](#bug-037) | #257 | wrong result | In a `for` or `while` loop with a 32-bit counter, `l = i;` copies the counter after its increment | | [BUG-038](#bug-038) | #258 | wrong result | A 16-bit local assigned in one arm of an `if … else`, then updated, loses the assignment when an early-return `if` follows | - -Thirty-six are open: twenty-four give wrong results with no diagnostic -(BUG-030 can also hang; BUG-032, BUG-034 and BUG-035 need `dccpeep`), one -hangs, ten stop the compile (BUG-007 can instead compile and crash), and -one (BUG-022) turns a compile error into a broken program. BUG-026 is the -one most likely to be hit by ordinary code, then BUG-030 (it corrupts -memory in C99-style code), BUG-003, BUG-013, BUG-016 and BUG-025. +| [BUG-039](#bug-039) | #259 | wrong result | Float division (`__fdiv`) is 1 ulp off in about a fifth of cases, in both directions | +| [BUG-040](#bug-040) | #260 | compile error | A static-storage float initializer must be a bare constant: `(2.5f)`, `-(2.5f)` and `1.0f + 2.0f` are rejected | +| [BUG-041](#bug-041) | #261 | compile error | The value of a float `++`/`--` is rejected (`g = f++`, `if (f-- > x)`, `while (f-- > 0)`) | +| [BUG-042](#bug-042) | #262 | compile error | A float constant that must be folded or tested for truth is rejected: `-(-2.5f)`, `!2.5f`, `2.5f && n`, `if (2.3f)` | +| [BUG-043](#bug-043) | #263 | compile error | Compound assignment to an integer lvalue with a float right operand is rejected (`l *= f`, `n += 1.5f`) | +| [BUG-044](#bug-044) | #264 | wrong result | `++`/`--` on a float is a no-op on a scalar and a 16-bit integer increment on an array element | +| [BUG-045](#bug-045) | #265 | compile error | A comparison against a negative float constant combined with another operand (`x < -1.0f - n`) stops the compile with `MIR emission is required` | +| [BUG-046](#bug-046) | #266 | compile error | `return` of a 32-bit integer from a `float` function, or of a float from a 32-bit integer function, is rejected; the cast is implicit everywhere else | +| [BUG-047](#bug-047) | #267 | wrong result | `f + (x * y)` drops the product when `x` is an integer, or when `y` is a local/parameter and `x` is a global, array element, member or sub-expression | +| [BUG-048](#bug-048) | #268 | wrong result | `(uint32_t)x` converted to float, with `x` a signed 32-bit expression, converts the signed value (`-72` → `-72.0f`, not `4294967224.0f`) | +| [BUG-049](#bug-049) | #269 | wrong result | `!f` is 0 when `f` is negative zero; `if (f)`, `f ? :` and `f == 0.0f` treat `-0.0f` as zero correctly | +| [BUG-050](#bug-050) | #270 | wrong result | `(int32_t)(uint32_t)x` converted to float, with `x` a signed 8- or 16-bit value, converts the unsigned value (`-121` → `4294967175.0f`) | +| [BUG-051](#bug-051) | #271 | compile error | A macro parameter named like a constant's suffix (`f`, `u`, `L`) is substituted inside the constant: `#define T(f) g = 9.0f; f(1)` gives `g = 9.0a1` | +| [BUG-052](#bug-052) | #272 | wrong result | In a loop, `a[1] = (0 ? K : x) - 1.0f` with a 32-bit parameter `x` assigned after the loop subtracts 1 from `x` on every iteration | +| [BUG-053](#bug-053) | #273 | wrong result (`dccpeep` only) | `float *p = &x; { for (...) {} } x = 1.0f; return *p;` returns the old parameter: `dccpeep` forgets the escaped address at its own `LS:` label and deletes the store | + +Fifty-one are open: thirty-two give wrong results with no diagnostic +(BUG-030 can also hang; BUG-032, BUG-034, BUG-035 and BUG-053 need `dccpeep`; +BUG-039 is in the runtime), one hangs, seventeen stop the compile (BUG-007 +can instead compile and crash), and one (BUG-022) turns a compile error into +a broken program. BUG-026 is the +one most likely to be hit by ordinary code, then BUG-044 (every float `++` +is wrong), BUG-047 (`f + n * g` is `f`), BUG-030 (it corrupts memory in C99-style code), BUG-003, +BUG-013, BUG-016 and BUG-025. Two more were found and then fixed upstream before they were reported; see [Fixed upstream](#fixed-upstream). @@ -366,6 +383,41 @@ signed char l2 = -66; `{ uint16_t b = 0; --b; g9 = b; }` with a 32-bit `g9` gives -1, not 65535; `b--` and `(uint16_t)b` are wrong too, `b -= 1`, `b = 5; --b`, a store to a 16-bit target and a function-scope `b` are right. +- Unary `-` of a nested-block `float` is computed on its low 16 bits as an + integer (float generator, seed 30): `{ float b = 1.0f; fg = -b; }` stores + 0.0f, `{ float b = 1.1f; fg = -b; }` stores 13107.0f (the negated low word + 0xcccd of 0x3f8ccccd), `-b - b` is -1.0f and `-(-b)` is 0.0f. Right: + `0.0f - b`, `-(b + z)`, `float c = -b;`, `-b` of a function-scope float. +- A nested-block integer local used directly as the operand of float + arithmetic is not converted: its bits are taken as a float (so a small + integer is a denormal, about zero). `{ int16_t b = 3; fg = b + 1.0f; }` + gives 1.0f, `b * 2.0f` gives 0.0f, `b - 1.0f` gives -1.0f, and + `b + c * 1.0f` with two block locals gives the integer 7 in the low word. + The same for `int32_t` and `uint8_t` locals and for a C99 `for + (int16_t i = 0; …)` loop variable (`fg = i + 1.0f`). `(float)b + 1.0f`, + `(b + 0) * 1.0f` and `fg = b;` are right. Found by fuzzing (float + generator, seeds 20 and 41). +- The same missing conversion in other float contexts: `fg *= b` gives 0 + and `fg += b` leaves `fg` unchanged; a store to a float array element, + struct member or through a pointer (`fa[0] = b`, `s.m = b`, `*p = b`) + stores the integer bits (`00000003`); `b == 3.0f` is 0; `fg > 0.0f ? b : + fg` is `40400003`. A store to a plain float global or local (`fg = b`, + `float l = b`), a float call argument, `(float)b` and `b < 3.5f` are + right. Found by fuzzing (float generator, seeds 79 and 108). +- The mirror case: a nested-block *float* local in arithmetic with an + integer operand (constant or variable) is wrong — `{ float b = 1.0f; fg + = b * 2; }` gives 0.0f, `1 + b` gives 1.0f, `b + 2` gives 1.0f, + `(int32_t)1 - b` gives -1.0f, `b - (uint32_t)1` gives 1.0f, `b * gl` + (32-bit global) gives 0.0f, and `b -= (int32_t)1` leaves `80000001`. + `b * 2.0f`, `b * (float)2` and a function-scope `b` are right. Found by + fuzzing (float generator, 20 of 1200 seeds, e.g. 292, 303, 370, 381, + 435, 682, 689, 698, 722, 789, 1031, 1036, 1052, 1105, 1131, 1144, 1154, + 1248, 1328, 1428). +- The value of an assignment to a nested-block float behaves the same way: + `{ float b = 1.0f; fg = -(b = 1.18f); }` stores `c523d000` (-2621.0f, the + negated low word `0x0a3d` of `0x3f970a3d`), and `fg = (b = 1.0f) - 1;` + stores 1.0f. `fg = (b = 1.18f);` and a function-scope `b` are right. + Found by fuzzing (float generator, seeds 1490 and 1861). ## BUG-016 @@ -398,6 +450,19 @@ r = x / l; /* uint16_t x = 50000: want -16666, dcc 0 */ unsigned constant: `{ short b = 0; b = (uint16_t)0xb840; out = (b % three); }` with a 32-bit `three` of 3 gives 2 (`47168 % 3`), not -2. Correct with `b` at function scope or a signed constant. +- With floats: `{ uint32_t b = (int32_t)1 - 78; fg = b; }` converts the + signed value (`c29a0000` instead of `4f800000`), as does `float c = b;` + in the block; and `{ uint16_t b = h(); fg = (float)b; }` with `h()` + returning `int16_t -180` gives `-180.0f` instead of `65356.0f` (also in + `8.1f > (float)b`). Found by fuzzing (float generator, seeds 456, 863, + 1203 and 1215). +- An `int32_t` local initialised from a `uint32_t` converts to float as + unsigned even after it has been read correctly as an integer: + `{ int32_t t = (int32_t)gu; fg = (float)t; }` with `gu = 0xffffffbd` + gives `4f800000`, not `c2860000` (`-67.0f`); the same with `int32_t t = + gu;` and `fg = t;`, and after `printf("%ld", (long)t)` has shown -67. + Right: `int32_t t; t = (int32_t)gu;` or `(float)(t + 0)`. Found by + fuzzing (float generator, seed 3792). ## BUG-017 @@ -466,6 +531,10 @@ if (sel) { block); dead code with no declaration (`return 1; { low += 1; }`); a `for` loop whose counter is declared at function scope; a conditional jump on a variable (`if (sel) continue;`, `if (1 || sel) break;`). +- The constant may be a folded cast: `if ((int16_t)(-1.0f)) { return 1; }` + or `if ((int16_t)((int32_t)1000)) { return 1.0f; }` followed by `{ float + b = 1.0f; }` in the same block is fatal. Found by fuzzing (float + generator, seed 1957). - Dead code like this is rare in hand-written programs, so this is low priority; it is still valid C. @@ -496,6 +565,23 @@ static int32_t f(int32_t x) - Upstream commit `81937302` ("Don't pair a wide helper operand with an unpushed parameter or constant") fixed `if (x < n / x)`, which fails with the same message; this looks like a neighbouring shape. +- An `if … else` whose *then* branch contains a loop triggers it as well + (the `switch` is not essential), and floats are affected: after + `if (n) { while (n) n--; } else { }`, `if (fg2 < x0 * fg1)` with `x0` a + float parameter and `fg1` a float global is fatal, as is + `if (gl < xl * gl)` with 32-bit `xl`. `x0 * 2.0f`, `x0 - 2.0f`, `xl * 2` + (a constant right factor), `fg1 * x0` (parameter on the right), the + operands swapped (`x0 * fg1 > fg2`), no `else`, a loop in the `else` + instead, or a non-loop `then` body are all fine. Found by fuzzing (float + generator, seed 26). +- No loop or `switch` is needed either: a braced early return is enough. + `int f(float x) { float l = -2.0f; if (x) { return 1; } return 1.0f >= + (x - l); }` is fatal, with any condition (`if (n)`, `if (g)`, `if ((int) + (-1.0f))`) and with `>=`, `>`, `==`, `+` or `*`. Fine: `if (x) return 1;` + without braces, `return (x - l) <= 1.0f;` (constant on the right), `(l - + x)` (parameter on the right), `x >= l`, both operands parameters, `l` a + local assigned instead of returned, and the same with `int` or `long` + operands. Found by fuzzing (float generator, seed 2017). ## BUG-021 @@ -703,6 +789,21 @@ dcc emits, for `sq`: - Not a regression: the v3.6.0 compiler (`20290235`) emits the same code. - BUG-017 (`x &= x` on a parameter) also has one variable as both operands and may be the same defect. +- A nested-block `float` or `long` local initialised from a global or a + converted integer has it too, and there the result is the first operand + rather than garbage: `{ float b = fg; g = b + b; }` with `fg` 2.0f gives + 2.0f; `{ float b = c8; g = b + b; }` with `signed char c8` 3 gives 3.0f, + `b - b` gives 3.0f and `b * b` is garbage; `{ long b = c8; g = b + b; }` + is garbage. Right: `float b = 1.0f;`, `float b; b = c8;` (assigned, not + initialised), `(b + 0.0f) + b`, a call between the two statements, and a + function-scope `b`. Found by fuzzing (float generator, seed 1681). +- Also for a plain (function-scope) `float` local whose last assignment was + a converted integer: `float l = 3.14159f; l = (uint16_t)0xdcc7; + return (l * l) + (int32_t)1;` returns 1.0f (the product reads as 0) and + `(l + l) + (int32_t)1` is wrong; `(l * l) + 1.0f`, `(l * l) + (int16_t)1`, + `((int32_t)1) + (l * l)` and `l * l` alone are right, as is `l = 56519.0f` + (the same value as a float constant) or dropping the dead `float l1` + declaration next to `l`. Found by fuzzing (float generator, seed 3223). ## BUG-027 @@ -1098,6 +1199,18 @@ return l; /* want 1, dcc 0 */ - Wrong with `dcc-peep` on and off. Found by fuzzing at `83dc7a91` (seed 6826); the reduced program had a `for`-init declaration, but a counter declared outside the loop fails the same. +- The `float` form is worse than stale: after `for (...) id((l0 = fg2));`, + `{ l0 = fg2; id(l0); }`, `g = id((l0 = fg2));` or `l0 += fg2` inside the + argument, `l0` is byte-scrambled (`7f74f6c5` for `c57f7400`) or stale. + Right: no loop, a constant assigned (`l0 = 7.0f`), no initialiser on + `l0`, `id((l0 = fg2) + 0.0f)`, or a `long` local in the same shape. Found + by fuzzing (float generator, seed 3025). +- A compound assignment leaves the local equal to the constant operand: + `while (n > 0) { l *= 2.0f; id(l); n = 0; }` with `l = 0.5f` returns + 2.0f (`l += 1.0f` returns 1.0f), for `float` and `long` and in `for` and + `while` loops. Right when the call result is used (`fa[1] = id(l);`), + when the argument is an expression (`id(l + 0.0f)`), with no call, or + with no loop. Found by fuzzing (float generator, seed 3288). ## BUG-037 @@ -1164,6 +1277,411 @@ return sink; `if ((int32_t)0) { } else { if (l2 = 1) { } }`, which fails in every configuration including the forced MIR candidates. +## BUG-039 + +**Float division is off by one unit in the last place in about a fifth of +cases, in both directions.** Addition, subtraction, multiplication, the +comparisons and every integer/float conversion are bit-exact against IEEE +single precision (484 operand pairs each), so this is the rounding in the +runtime's `__fdiv` (`DCCRTL.MAC`), not a general precision limit. + +```c +volatile float va = 1.0f, vb = 3.0f; +union { float f; uint32_t u; } b; +b.f = va / vb; /* want 3eaaaaab, dcc 3eaaaaaa */ +va = 10.0f; /* 10/3: want 40555555, dcc 40555556 */ +va = 1.0f; vb = 1.85f; /* want 3f0a60dd, dcc 3f0a60de */ +``` + +- 80 of 400 quotients over a 20-value table are wrong, all by exactly one + ulp; 44 are too low and 36 too high, so it is not a missing round-up. +- Division by a power of two is exact. +- `1.0f / 3.0f` with constant operands is wrong too: dcc does not fold float + division, it calls the runtime (`__fdf`, the fast-call entry of `__fdiv`), + so every float quotient goes through the one routine. +- Also at v3.6.0 (`20290235`) with today's runtime. +- Found by hand probing (the float generator excludes `/` as `fdivround`). + +## BUG-040 + +**The initializer of a float with static storage must be a bare floating +constant, optionally negated, or `(float)` of an integer constant.** A +parenthesised constant, a negated parenthesised constant or a constant +expression is rejected: `DCC-E1102 expected ';'` at file scope, `DCC-E0912 +float initializer must be constant` for an element of a static array. + +```c +static float a = (2.5f); /* rejected */ +static float b = -(2.5f); /* rejected */ +static float c = 1.0f + 2.0f; /* rejected */ +static float arr[2] = { (0.5f), 1.0f - 0.75f }; /* rejected */ +static int ok = (-2) + 1 * 3; /* accepted */ +``` + +- Accepted: `-2.5f`, `(float)3`, every form as an automatic initializer + (`float la = (2.5f);`) and as an assignment. +- The conformance table lists global initializers as supported without a + float exception. +- Also at v3.6.0 (`20290235`). +- Found by fuzzing (float generator, seeds 1, 2, 4, 5, …): it rendered + negative constants in parentheses. + +## BUG-041 + +**The value of a float `++` or `--` expression is rejected.** `DCC-E1002` +("unsupported expression statement", "unsupported if condition or branch", +"unsupported while condition or body"), reported against the enclosing +statement, wherever the value is used. + +```c +g = f++; /* rejected; float f, g */ +g = ++f + 1.0f; +if (f-- > 3.0f) g = 7.0f; +if (--f) n = 1; +n = f++ > 1.0f; +g = (f-- > 10.0f) ? 1.0f : 2.0f; +while (f-- > 0.0f) g += 1.0f; +if (fa[1]++ > 1.0f) g = 9.0f; /* array element, and a local float too */ +``` + +- Accepted: `f++;` as a statement; the value of `f += 1.0f`; the value of + an integer `++`/`--` in every one of these positions. +- The limitation is not documented. +- Also at v3.6.0 (`20290235`). +- Found by fuzzing (float generator, seed 3). + +## BUG-042 + +**A floating constant that has to be folded or tested for truth is +rejected.** `DCC-E1002`, reported against the enclosing statement: unary `-` +of a negated or computed float constant, `!`, `&&` or `||` with a float +constant operand, and a float constant as the condition of `if` or `while`. + +```c +g = -(-2.5f); /* rejected; -(2.5f) is accepted */ +g = -(1.0f + 2.0f); /* rejected; -(1.0f + f) is accepted */ +n = !255.0f; /* rejected; !f is accepted */ +n = 2.5f && 1; /* rejected; 2.5f ? 1 : 2 is accepted */ +if (2.3f) n = 7; /* rejected; if (f), if (1.0f + f), if (1.0f > 2.0f) accepted */ +while (1.0f) { break; } /* rejected */ +``` + +- Integer constants in every one of these positions are accepted, so it is + the float constant folder that lacks `-` of a folded value and the truth + test; compare BUG-012 (`-`/`~` of `!`, `&&`, `||` of integer constants). +- `&&`/`||` whose operands are *all* constant is also rejected when at least + one operand is a float comparison: `if ((1.0f > 2.0f) && (0.0f == 0.0f))`, + `if ((1.0f > 2.0f) && (1 == 1))`, `n = (1.0f > 2.0f) && 1`. Each + comparison alone (`if (1.0f > 2.0f)`), a non-constant operand + (`(1.0f > 2.0f) && n`), and the all-integer form (`(1 > 2) && (0 == 0)`) + are accepted. Found by fuzzing (float generator, seed 30). +- Also at v3.6.0 (`20290235`). +- Found by fuzzing (float generator, seeds 3, 13, 16). + +## BUG-043 + +**A compound assignment to an integer lvalue whose right operand is a float +is rejected.** `DCC-E1002 unsupported expression statement`, for every +integer width (8, 16 and 32 bits, signed and unsigned), every operator tried +(`+= -= *= /=`), a global or a local lvalue, and a variable or constant +right operand. + +```c +int32_t l = 5; float f = 2.5f; +l *= f; /* rejected; want 12 */ +n += f; /* int n */ +u -= f; /* uint32_t u */ +c8 += f; /* int8_t c8 */ +l -= 1.5f; +``` + +- Accepted: the expanded form `l = l * f`; a float lvalue with an integer + right operand (`f *= l`); `l %= 2`. +- Also at v3.6.0 (`20290235`). +- Found by fuzzing (float generator, seeds 7, 10, 25). + +## BUG-044 + +**`++` and `--` on a float do the wrong thing with no diagnostic.** On a +scalar (a local, a global, a struct member, `*p`) the statement is a no-op. +On an array element (`fa[1]++`, `fa[i]--`, `p[3]++`, a local array too) the +element is treated as a 16-bit integer: the low word is incremented or +decremented and the result sign-extended over the high word. + +```c +float l = 1.0f; +l++; /* want 2.0f (40000000), dcc 1.0f (unchanged) */ +fg++; /* static float fg = 1.0f: unchanged */ +s.m++; /* unchanged */ +(*p)++; /* unchanged */ +fa[1]++; /* 2.0f: want 40400000, dcc 00000001 */ +fa[2]--; /* 3.0f: want 40000000, dcc ffffffff (a NaN) */ +``` + +- Correct: `l += 1.0f`, `l = l + 1`. +- The value of a float `++` (`g = f++`) is rejected (BUG-041), so only the + statement form reaches code generation, and it is always wrong. +- With and without `dcc-peep`. +- Also at v3.6.0 (`20290235`), tag 3.8.0 and `32d3e46d`. +- Found by fuzzing (float generator, seed 14: `la3[3]++; fg1 = la3[3];`). + +## BUG-045 + +**A comparison whose right operand is a negative float constant combined +with a second operand stops the compile.** `dcc: fatal: MIR emission is +required`, with no line number. The shape is `x REL (-C OP y)` where `-C` +is a negative float constant (including `-0.0f`, `(-1.0f)` and `-(1.0f)`), +`OP` is `+ - * /` and `y` is an integer variable or constant of any width, a +cast, or a float global. The relation can be any of `< <= > >= == !=` and +the expression can be an `if`/`while` condition, a `?:` test or an +assigned value. + +```c +if (x < -1.0f - n) r = 1; /* fatal: n is int, int32_t, uint16_t, char... */ +if (x < -1.0f - 2) r = 1; /* fatal: even an integer constant */ +if (x < -1.0f - fg) r = 1; /* fatal: fg a float global */ +if (x < -1.0f - (float)n) r = 1; /* fatal */ +if (x == -1.0f * n) r = 1; /* fatal: so are + and / */ +r = (x < -1.0f - n); /* fatal */ +``` + +- Accepted: a positive constant (`x < 1.0f - n`), the constant on the right + (`x < n - 1.0f`, `x < -n - 1.0f`), the whole thing on the left + (`-1.0f - n < x`), a float local/parameter/array element as `y` + (`x < -1.0f - fl`), a wider `y` (`x < -1.0f - 2 * n`), or storing to a + float first (`t = -1.0f - n; if (x < t)`). The bare value `-1.0f - n` + outside a comparison is fine too. +- Also at v3.6.0 (`20290235`) and every later compiler checked. +- Found by fuzzing (float generator, seed 28: + `(la3[3] - 100.0f) < ((-108.885f) - g1)`). +- The constant need not be negative: any comparison whose right operand is + a float binop with a *constant* left operand is fatal — `1.0f <= (3.0f * + fg2)`, `fg < (3.0f * fg2)`, `2 < (3.0f * fg2)`, `1.0f < (1174.3f + 1)`. + `1.0f < (fg2 + 1)`, `1.0f < (fg2 * fg)`, `(3.0f * fg2) >= 1.0f` and + `1.0f < -fg2` are accepted. Found by fuzzing (float generator, seeds 477 + and 1347). + +## BUG-046 + +**`return` does not convert between `float` and a 32-bit integer.** A +function returning `float` cannot `return` any `int32_t`/`uint32_t` +expression, and a function returning a 32-bit integer cannot `return` a +float expression: `DCC-E1002: unsupported return expression`. When the +`return` is inside an `if`, the error is reported against the `if`. + +```c +static float a(int32_t x) { return x; } /* rejected */ +static float c(void) { return gl; } /* rejected: int32_t global */ +static float g(void) { return ff(); } /* rejected: ff returns int32_t */ +static float b(uint32_t x) { return x + 1; } /* rejected */ +static int32_t d(float x) { return x; } /* rejected */ +static uint32_t e(float x) { return x * 2.0f; } /* rejected */ +``` + +- Accepted: `return (float)x;`, `float r = x; return r;`, `fg = x; return fg;`, + and 8- and 16-bit integers in either direction (`float j(int16_t x) + { return x; }`, `int16_t k(float x) { return x; }`). +- The same implicit conversion is accepted in assignments, initialisers and + arguments, so only the return-value path lacks the long/float widening. +- Also at v3.6.0 (`20290235`) and every later compiler checked. +- Found by fuzzing (float generator, seeds 30 and 35: + `return h0(...)` with `h0` returning `int32_t` in a `float` function). + +## BUG-047 + +**`f + (x * y)` drops the product.** When the right operand of a float `+` +is a product, the result is just the left operand (whatever `f` is: global, +local, constant or parameter) in two families of factor kinds: + +- the left factor `x` is an integer (a constant, `int`, `int32_t`, + `uint16_t`, or `(float)128`), with any float right factor; or +- the right factor `y` is a local or parameter and `x` is a global, array + element, struct member or sub-expression (`fv * l1`, `fa[1] * l1`, + `s.m * p`, `fv * l1 * l1`). A few forms give other garbage instead of + `f` (`(fv + 0.0f) * l1` is `-inf` in one context; the sum in `t()` of the + reproducer is `5.0028`). + +```c +fg + (2 * 3.0f) /* want 7.0f, dcc 1.0f (fg) */ +fg + (i * 3.0f) /* 1.0f */ +1.0f + (128 * 1.0f) /* want 129.0f, dcc 1.0f */ +r = fg; r += 2 * 3.0f; /* 1.0f */ +fg + (fv * l1) /* l1 a local: want 10.0f, dcc 1.0f */ +fg + (fv * p) /* p a parameter: 1.0f */ +``` + +- Right: `fg - (x * y)`, `fg * (x * y)`, `(x * y) + fg` (product on the + left), `fg + (l1 * fv)` and `fg + (p * fv)` (local/parameter left + factor), `fg + (fv * fv)`, `fg + (2.0f * fv)`, `fg + (float)i * 3.0f`, + `fg + (x * y) * 1.0f`, `fg + (x + y)`, `fg + (x - y)`, `fg + (x / y)`. +- It is a per-function matter: the same expressions all compute correctly + inside one 256-statement function, so the operands' register or stack + placement decides it. +- With and without `dcc-peep`. +- Also at tag 3.8.0 (`5f733050`) and `32d3e46d`; v3.6.0 (`20290235`) cannot + compile the reproducer at all (`MIR emission is required`). +- Found by fuzzing (float generator, seeds 7, 27, 31, 42 and 49). + +## BUG-048 + +**`(uint32_t)x` converted to float converts the signed value.** With `x` a +signed 32-bit expression, `(float)(uint32_t)x`, `m = (uint32_t)x;`, +`(uint32_t)x * 1.0f` and `0.0f + (uint32_t)x` all produce the signed +conversion: `-72` becomes `-72.0f` (`c2900000`) instead of +`4294967224.0f` (`4f800000`). + +```c +m = (uint32_t)g2; /* g2 = -72: want 4f800000, dcc c2900000 */ +m = (uint32_t)(g2 + 1); /* want 4f800000, dcc c28e0000 */ +``` + +- Right: the cast stored in a `uint32_t` variable first (`u = (uint32_t)g2; + m = u;`), a `uint32_t` global, `(uint32_t)gs` with a 16- or 8-bit signed + operand, `(uint16_t)gs`, and integer uses of the cast (`(uint32_t)g2 / 2` + is 2147483612). +- With and without `dcc-peep`. +- Also at v3.6.0 (`20290235`), tag 3.8.0 and `32d3e46d`. +- Found by fuzzing (float generator, seeds 12, 33, 56 and 60). + +## BUG-049 + +**`!f` is 0 when `f` is negative zero.** `!` on a float tests the raw bits, +so `-0.0f` (`80000000`) counts as true. The other truth tests are right: +`if (f)`, `f ? 1 : 0`, `f && 1`, `f || 0` and `f == 0.0f` all treat +`-0.0f` as zero. + +```c +m0 = -((float)g5); /* g5 == 0, so m0 is -0.0f */ +n = !m0; /* want 1, dcc 0 */ +if (!m0) puts("yes"); /* want yes, dcc no */ +``` + +- `-0.0f` arises from negating a zero and from `0.0f * -1.0f`, so a + `!` guard after a multiplication or negation can misfire. +- With and without `dcc-peep`. +- Also at v3.6.0 (`20290235`), tag 3.8.0 and `32d3e46d`. +- Found by fuzzing (float generator, seed 45). + +## BUG-050 + +**`(int32_t)(uint32_t)x` converted to float converts the unsigned value.** +With `x` a signed 8- or 16-bit value, the outer `(int32_t)` cast is lost +when the result is converted to float: `-121` becomes `4294967175.0f` +(`4f800000`) instead of `-121.0f` (`c2f20000`). The mirror image of +BUG-048, where `(uint32_t)` of a signed 32-bit expression converts as +signed. + +```c +fg = (int32_t)((uint32_t)g4); /* int8_t g4 = -121: want c2f20000, dcc 4f800000 */ +fg = (int32_t)((uint32_t)g5); /* int16_t g5: same */ +fg = (int32_t)((uint32_t)g4 + 1); /* want c2f00000, dcc 4f800000 */ +``` + +- Right: a 32-bit `x` (`(int32_t)(uint32_t)g6`), `(int32_t)g4` alone, and + `(uint32_t)g4` alone (which is correctly `4f800000`). +- With and without `dcc-peep`. +- Found by fuzzing (float generator, seed 911). +- Also wrong when `x` is 32-bit arithmetic, including arithmetic that folds: + `(float)((int32_t)(2147483647L + 1UL))` gives `4f000000` + (2147483648.0f), not `cf000000`; `(float)((int32_t)(gl + 2147483647UL))` + with `long gl` 7 is wrong too. Only a bare 32-bit variable, + `(int32_t)(uint32_t)gl`, is right. Found by fuzzing (float generator, seed + 2036). +- A bare `uint32_t` variable with its top bit set is wrong too: + `fg = (float)(int32_t)gu;` with `uint32_t gu = 0xffffffbd` (so -67) + gives `4f800000` (4294967229.0f), not `c2860000`, for a global, a local + or a parameter, with the cast explicit or implicit (`fg = (int32_t)gu;`); + `g2 = (float)(int32_t)gu;` then stores -1 in the `int32_t`. Found by + fuzzing (float generator, seed 3792). + +## BUG-051 + +**A function-like macro parameter whose name is the suffix of a numeric +constant in the replacement list is substituted inside that constant.** The +suffix letters are lexed as an identifier separate from the number, so +`f`, `u`, `l`, `L`, `U`, `F` and their combinations (`ul`) are all open to +substitution. + +```c +#define T(f) g = 9.0f; f(1) +#define U(u) n = 10u; u +T(a1); /* expands to `g = 9.0a1; a1(1);`: error DCC-E1003 malformed statement near 'g' */ +U(n += 2;) /* expands to `n = 10n += 2;; n += 2;` */ +``` + +- A suffix that is not a parameter name is fine, and so are `1e5` with a + parameter `e` and `0x10` with a parameter `x`: only the trailing + integer/float suffix letters are detached. +- The result is normally a compile error; a replacement that happens to + stay valid would silently change meaning. +- Found while writing a probe for the float fuzzer's seed 2247 (a `T(f)` + test macro with `9.0f` in its body). + +## BUG-052 + +**In a loop, an expression with a folded `?:` on a 32-bit parameter is +accumulated into the parameter when the parameter is assigned after the +loop.** `fa[1] = (0 ? 2.0f : x) - 1.0f;` run three times stores `x - 3` +instead of `x - 1`: the parameter is treated as the expression's own +temporary and the subtraction is written back into it. In a `for` loop the +float's low word is overwritten by the counter as well. + +```c +static void f1(float x) { int i; for (i = 0; i < 3; ++i) fa[1] = (0 ? 2.0f : x) - 1.0f; x = -1.0f; } +/* f1(-0.35f): want bfaccccd (-1.35f); dcc c0560002 (about -3.35f with the low word 0002) */ +static void f3(float x) { int i; i = 0; while (i < 3) { fa[1] = (0 ? 2.0f : x) - 1.0f; i++; } x = g; } +/* dcc c0566666 (-3.35f) */ +static void f4(long x) { int i; for (i = 0; i < 3; ++i) la[1] = (0 ? 2L : x) - 1L; x = -1L; } +/* f4(5): want 4; dcc 1 */ +``` + +- Wrong with `0 ? K : x` and `1 ? x : K`, for `float` and `long` + parameters, with a `for` or `while` loop, storing to an array element or + a struct member, and whatever the parameter is assigned afterwards (a + constant or a global). +- Right: an `int` parameter; a local copy of the parameter in place of it; + `(x + 0.0f) - 1.0f` or `(float)x - 1.0f` (no `?:`); a store target that + varies with the counter (`fa[i]`); a plain global target; no loop; and + reading `x` after the loop (`fa[2] = x;` or `x = x + 1.0f;`) instead of + only assigning it. +- With and without `dcc-peep`. +- Found by fuzzing (float generator, seed 2247). + +## BUG-053 + +**With `dccpeep`, a 32-bit parameter whose address is held in a pointer, +assigned after an empty counted loop and then read through the pointer, +returns its old value.** `pass_elim_dead_ix_stores` records that `&x0` +escaped (`push ix / pop hl / ld de,4 / add hl,de`) and protects stores to +`(ix+4..7)` from the escape onwards, but any label that is not `L:` +is taken as a new function and resets that protection. The empty loop's +signed compare is rewritten by `fold_signed_cmp_via_bytes` with an `LS:` +label, so at `ret` the four `ld (ix+4..7)` stores of `x0 = 1.0f` look +dead and are deleted; `*p` then reads the caller's argument. + +```c +static float f(float x0) +{ + float *p = &x0; + { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + x0 = 1.0f; + return *p; /* f(0.3f): want 3f800000 (1.0f); dccpeep 3e99999a (0.3f) */ +} +``` + +- Wrong for `float` and `int32_t` parameters, with the loop in a plain + block, under `if ((int32_t)12)`, `if (1)` or `if (g == 0.0f)`. +- Right: an `int16_t` parameter; no loop; a loop with a body (`s += i;`) + or a `while` loop (no `LS` label is produced); `return x0` instead of + `*p`; `*p = 1.0f; return x0;`. +- Right without `dccpeep` (`dcc-peep=false`). Any peep pass that + introduces an `LS`, `LI` or `LR` label between the escape and the store + should expose the same thing. +- Found by fuzzing (float generator, seed 3036). + --- ## Fixed upstream diff --git a/scripts/README.md b/scripts/README.md index 3f5027d8e..a63a69619 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -75,6 +75,16 @@ arguments, bounded recursion, calls through function pointers, compound assignment and `++`/`--` on every kind of lvalue, and assignments used as values. The generator and renderers are in `clang_oracle_gen.py`. +`--gen float` switches to `clang_oracle_float_gen.py`, which builds on the +same model to exercise dcc's only floating type: `float` globals, locals, +parameters, arrays and struct members; arithmetic, negation, comparison, +truth tests, `?:`, `++`/`--`, compound assignment, conversions to and from +every integer width, and mixed integer/float arithmetic. Every operation is +clamped so no value overflows, and floats are printed and checksummed as +their IEEE bits with `-0` folded to `0`, so a difference is a difference in +the arithmetic rather than in formatting. Its failures and reductions go +under `build/clang-oracle-fuzz-float/`. + The same source is compiled by clang and by dcc. It can be, because it never spells `int` or `long`, and the renderer inserts the casts and masks that make an expression mean the same with a 16-bit and a 32-bit `int`. What a @@ -96,6 +106,8 @@ python3 scripts/clang-oracle-fuzz.py --seeds 1-300 --off known --configs all python3 scripts/clang-oracle-fuzz.py --reduce 17,42 --config nopeep --off known python3 scripts/clang-oracle-fuzz.py --generate 17 prog.c --off known python3 scripts/clang-oracle-fuzz.py --list-configs +python3 scripts/clang-oracle-fuzz.py --gen float --seeds 1-300 --off known +python3 scripts/clang-oracle-fuzz.py --gen float --reduce 36 --config nopeep --off known ``` - `--configs production` builds with and without `dccpeep` and with and @@ -105,8 +117,12 @@ python3 scripts/clang-oracle-fuzz.py --list-configs latent, not shipped behaviour. - `--off known` leaves out every form that hits a bug already logged in `compiler-bugs.md`, so a run reports new failures instead of the same ones - again. `KNOWN_BUGS` in `clang_oracle_gen.py` maps each such form to its - entry; delete a line when its bug is fixed. `--off` also takes individual + again. `KNOWN_BUGS` in `clang_oracle_gen.py` (and in + `clang_oracle_float_gen.py` for `--gen float`) maps each such form to its + entry; delete a line when its bug is fixed. The float generator also + rewrites a few logged shapes after generation (`Gen.steer`) when they are + easier to see in the finished tree, for example by swapping the operands + of a `+` whose right side is a product. `--off` also takes individual generator features (`-h` lists them) to narrow a search by hand. A program whose undefined-behaviour repair rebuilt one of those forms is dropped and counted as `INVALID`. diff --git a/scripts/clang-oracle-fuzz.py b/scripts/clang-oracle-fuzz.py index 31b7fd503..369c72bd5 100644 --- a/scripts/clang-oracle-fuzz.py +++ b/scripts/clang-oracle-fuzz.py @@ -41,7 +41,8 @@ import clang_oracle_gen as gen # noqa: E402 REPO = Path(__file__).resolve().parent.parent -HOST_FLAGS = ['-std=gnu99', '-w', '-fsigned-char', '-fno-common'] +# -ffp-contract=off: clang would otherwise fuse a*b+c into one rounding +HOST_FLAGS = ['-std=gnu99', '-w', '-fsigned-char', '-fno-common', '-ffp-contract=off'] SANITIZE = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all'] CANDIDATES = ['homed', 'homed-lazy', 'hybrid', 'regional', 'spilled-baseline', 'spilled-all', 'spilled-all-no-prepack', 'spilled-address-remat', 'spilled-boolean-phi-branch', @@ -576,7 +577,7 @@ def simpler_expressions(prog): for i, (slot, parent, index) in enumerate(parented_slots(prog)): node = gen.slot_get(slot) t = gen.ty(node) - if not gen.is_int(t) or is_lvalue_slot(parent, index) or node.op == 'const': + if not gen.is_scalar(t) or is_lvalue_slot(parent, index) or node.op == 'const': continue options = [] # a cast is only ever dropped ('plain'): replacing it by a cast of @@ -585,7 +586,7 @@ def simpler_expressions(prog): for k, kid in enumerate(node.k): if node.op in ('asg', 'incdec') and k == 0: continue - if gen.is_int(gen.ty(kid)): + if gen.is_scalar(gen.ty(kid)): options.append(('kid', k)) options += [('const', 1), ('const', 0)] if node.op == 'cast' or (node.op == 'asg' and node.a != '='): @@ -600,8 +601,7 @@ def mutate(p, i=i, what=what, k=k): if gen.ty(new) is not t: new = gen.E('cast', [new], t=t) elif what == 'const': - ct = t if t.bits >= 16 else (gen.I16 if t.signed else gen.U16) - new = gen.mk_const(k, ct) + new = gen.simple_const(k, t) elif node.op == 'cast': new = node.k[0] else: @@ -663,11 +663,10 @@ def plainer_decls(prog): out = [] for bi, b in enumerate(gen.all_blocks(prog)): for di, d in enumerate(b.decls): - if isinstance(d.init, gen.E) and d.init.op != 'const' and gen.is_int(d.t): + if isinstance(d.init, gen.E) and d.init.op != 'const' and gen.is_scalar(d.t): def plain(p, bi=bi, di=di): dd = gen.all_blocks(p)[bi].decls[di] - ct = dd.t if dd.t.bits >= 16 else (gen.I16 if dd.t.signed else gen.U16) - dd.init = gen.mk_const(1, ct) + dd.init = gen.simple_const(1, dd.t) out.append(plain) return out @@ -732,8 +731,13 @@ def parse_seeds(text): def main(): + global gen parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--gen', default='int', choices=('int', 'float'), + help='program generator: integer (clang_oracle_gen) or float ' + '(clang_oracle_float_gen); float defaults --work to ' + 'build/clang-oracle-fuzz-float') parser.add_argument('--seeds', default='1-100', help='seeds to run, e.g. 1-100,250') parser.add_argument('--configs', default='default', help='production, default (production + no exact schedules), all, ' @@ -748,7 +752,7 @@ def main(): parser.add_argument('--jobs', type=int, default=os.cpu_count() or 1) parser.add_argument('--reduce-jobs', type=int, default=16, help='candidate edits tried at once per reduction') - parser.add_argument('--work', type=Path, default=REPO / 'build' / 'clang-oracle-fuzz') + parser.add_argument('--work', type=Path, default=None) parser.add_argument('--emulator', default='ntvcm') parser.add_argument('--stack', type=int, default=4096, help='target stack bytes') parser.add_argument('--timeout', type=int, default=20, help='seconds to run a program') @@ -761,6 +765,11 @@ def main(): parser.add_argument('--reduce', help='seeds whose failure to reduce') parser.add_argument('--config', default='nopeep', help='config to reduce under') args = parser.parse_args() + if args.gen == 'float': + import clang_oracle_float_gen + gen = clang_oracle_float_gen + if args.work is None: + args.work = REPO / 'build' / ('clang-oracle-fuzz' + ('-float' if args.gen == 'float' else '')) args.off = frozenset(x for x in args.off.split(',') if x) if 'known' in args.off: args.off = (args.off - {'known'}) | frozenset(gen.KNOWN_BUGS) diff --git a/scripts/clang_oracle_float_gen.py b/scripts/clang_oracle_float_gen.py new file mode 100644 index 000000000..a7596f199 --- /dev/null +++ b/scripts/clang_oracle_float_gen.py @@ -0,0 +1,1341 @@ +#!/usr/bin/env python3 +"""Float-centric program generator for `clang-oracle-fuzz.py --gen float`. + +dcc's `float` is a 32-bit IEEE single implemented by the runtime, and it is +the only floating type. This module reuses the program model, renderer and +driver of clang_oracle_gen.py but generates programs whose values flow +through `float`: float globals, arrays, struct members and pointers; float +parameters and results; arithmetic, compound assignment and ++/--; +comparisons and logic on floats; `?:` with float arms; and conversions in +both directions between `float` and every integer type. + +Results are compared by IEEE bit pattern (see FLOAT_HARNESS), so the host +must compute in strict single precision: the driver passes +`-ffp-contract=off`. Programs are kept away from infinities, NaNs and +subnormals by the checked build's ck_f, and every float-to-integer +conversion is range checked; a tripped node is repaired like an integer one. + +The integer side is kept deliberately plain (function-scope locals, +same-signedness initialisers, no bit-fields, no post-increment values) so the +logged integer defects do not drown float findings. +""" +from __future__ import annotations + +import random + +from clang_oracle_gen import ( # noqa: F401 (re-exported for the driver) + init_keeps_type, + ARITH, CMP, LOGIC, DIV, E, S, Block, Func, Program, Decl, Field, StructType, PtrType, + IntType, I8, U8, I16, U16, I32, U32, F32, INT_TYPES, is_int, is_float, is_scalar, ty, + promote, common, Renderer, clone, expr_slots, slot_get, slot_set, all_blocks, mk_const, + float_text, is_bitfield, same_variable) +import clang_oracle_gen as base + +FARITH = ('+', '-', '*') + +FPOOL = [0.0, 1.0, -1.0, 0.5, -0.5, 2.0, 0.25, 0.1, 0.2, 0.3, 3.0, 4.0, 10.0, 100.0, 1000.0, + 1e4, 65536.0, 32768.0, 32767.0, 65535.0, 0.001, 1.5, 7.0, 123.456, 3.14159, 2.71828, + 1e6, 255.0, 256.0, 16777216.0, -32768.0, 0.999, 1e9, 0.75, 1e-3, -2.5, 6.02e5, + 2147483648.0, 4294967296.0, 0.0625, 1e-6] + +FEATURES = frozenset(( + # program shape + 'farr', 'fstruct', 'fptr', 'fcall', 'fparam', 'fret', 'fblock', 'fif', 'ffor', 'fwhile', + 'fret_early', 'iparam', + # expression forms + 'fdiv', 'fneg', 'fcond', 'fcmp', 'flogic', 'fnot', 'ftruth', 'f2i', 'i2f', 'fcassign', + 'fincdec', 'fincdecval', 'fasgval', 'fconst', 'mixarith', 'f2u', 'f2i8', 'i8', 'u32', + 'fchain', 'fcmpconst', + # forms that hit a logged dcc defect; see KNOWN_BUGS + 'fdivround', 'fconstfold', 'icasgf', 'fblockneg', 'fnegcmp', 'fretconv', + 'fintprodadd', 'fu32cast', 'fnegzero_not', 'fparamcmp', 'fblockint', 'finittype', + 'fconstjump', 'fselfop', 'fconstcond', 'floopcallarg', 'fptrparam')) + +# Features switched off by `--off known`, with the entry in compiler-bugs.md +# that each one would otherwise keep rediscovering. +KNOWN_BUGS = { + 'fdivround': 'BUG-039', + 'fincdecval': 'BUG-041', + 'fincdec': 'BUG-044', + 'fconstfold': 'BUG-042', + 'icasgf': 'BUG-043', + 'fblockneg': 'BUG-015', + 'fnegcmp': 'BUG-045', + 'fretconv': 'BUG-046', + 'fintprodadd': 'BUG-047', + 'fu32cast': 'BUG-048', + 'fnegzero_not': 'BUG-049', + 'fparamcmp': 'BUG-020', + 'fblockint': 'BUG-015', + 'finittype': 'BUG-016', + 'fconstjump': 'BUG-019', + 'fselfop': 'BUG-026', + 'fconstcond': 'BUG-052', + 'floopcallarg': 'BUG-036', + 'fptrparam': 'BUG-053', +} + + +def is_fconst(e): + """A float expression made only of constants (dcc has to fold it).""" + if e.op == 'const': + return True + if e.op == 'cast' and e.t is F32: + return is_fconst(e.k[0]) + if e.op in ('un', 'bin') and ty(e) is F32: + return all(is_fconst(k) for k in e.k) + return False + + +def is_anyconst(e, seen_float=None): + """e is made only of constants; seen_float (a list) gets a mark when a + float constant is among them.""" + if e.op == 'const': + if ty(e) is F32 and seen_float is not None: + seen_float.append(True) + return True + if e.op in ('cast', 'un', 'bin'): + return all(is_anyconst(k, seen_float) for k in e.k) + return False + + +def needs_fconst_fold(e): + """e is a shape BUG-042 rejects: `-`/`!` of a float constant, `&&`/`||` + with a float constant operand, or `&&`/`||` of nothing but constants + when a float constant is involved (`(1.0f > 2.0f) && (1 == 1)`).""" + if e.op == 'un': + return ty(e.k[0]) is F32 and is_fconst(e.k[0]) + if e.op == 'bin' and e.a in LOGIC: + if any(is_fconst(k) for k in e.k): + return True + seen = [] + return is_anyconst(e, seen) and bool(seen) + return False + + +def is_neg_fconst(e): + """A negative float constant as written: `-1.0f`, `(-1.0f)`, `-(1.0f)`.""" + if e.op == 'const': + return ty(e) is F32 and e.a < 0 + return e.op == 'un' and e.a == '-' and is_fconst(e.k[0]) + + +def is_negcmp(e): + """e is a shape BUG-045 rejects: a comparison whose right operand is a + float binop with a constant left operand (`x < -1.0f - n`, + `1.0f <= 3.0f * f`).""" + if e.op != 'bin' or e.a not in CMP: + return False + rhs = e.k[1] + return rhs.op == 'bin' and rhs.a in ('+', '-', '*', '/') and ty(rhs) is F32 \ + and is_anyconst(rhs.k[0]) + + +def is_retconv(fn, e): + """`return e` in fn needs the long<->float conversion BUG-046 lacks.""" + if fn.ret is None or e is None: + return False + t = ty(e) + if fn.ret is F32: + return is_int(t) and t.bits == 32 + return fn.ret.bits == 32 and t is F32 + + +FLIP = {'<': '>', '>': '<', '<=': '>=', '>=': '<=', '==': '==', '!=': '!='} + + +def is_fprod(e): + """A float product.""" + return e.op == 'bin' and e.a == '*' and ty(e) is F32 + + +def is_intprodadd(e): + """e is a shape BUG-047 miscompiles: `f + (x * y)` or `f += x * y`. + Only some operand kinds fail (an integer x, or a local/parameter y + with a non-local x), but the kinds depend on register placement, so + every float product on the right of a `+` counts.""" + if e.op == 'bin' and e.a == '+' and ty(e) is F32: + return is_fprod(e.k[1]) + return e.op == 'asg' and e.a == '+=' and ty(e.k[0]) is F32 and is_fprod(e.k[1]) + + +def is_u32s(e): + """`(uint32_t)x` with x a signed 32-bit expression (BUG-048), or + `(int32_t)(uint32_t)x` with x signed and narrow (BUG-050).""" + if e.op != 'cast': + return False + if e.t is U32 and ty(e.k[0]) is I32: + return True + if e.t is not I32: + return False + inner = e.k[0] + while inner.op == 'cast' and inner.t is I32: + inner = inner.k[0] # `(int32_t)(int32_t)…` + if ty(inner) is U32: + return True # arithmetic, folded or not (seed 2036), or a variable (3792) + while inner.op == 'bin' and inner.a in ('+', '-') and is_anyconst(inner.k[1]): + inner = inner.k[0] + return inner.op == 'cast' and inner.t is U32 \ + and is_int(ty(inner.k[0])) and ty(inner.k[0]).signed and ty(inner.k[0]).bits < 32 + + +def is_wide(t): + return t is F32 or (is_int(t) and t.bits == 32) + + +def is_paramcmp(e): + """e is the BUG-020 facet shape: `v REL (param OP x)` with a 32-bit + binop on the right, a parameter as its left operand and a non-constant + as its right operand. An `if … else` with a loop, or a braced early + return (seed 2017), before it makes the compile fail; any function + counts.""" + if e.op != 'bin' or e.a not in CMP: + return False + rhs = e.k[1] + return rhs.op == 'bin' and rhs.a in ('+', '-', '*', '/') and is_wide(ty(rhs)) \ + and rhs.k[0].op == 'var' and rhs.k[0].a.storage == 'param' \ + and not is_anyconst(rhs.k[1]) + + +def block_object(e): + """The nested-block variable e reads: a variable, an element, or the + value of an assignment to one (`(b = 1.18f)` behaves like `b`, seeds + 1490 and 1861); else None.""" + if e.op == 'asg': + e = e.k[0] + if e.op in ('var', 'idx') and e.a.storage == 'block': + return e.a + return None + + +def is_blockint(e): + """A nested-block integer variable used as it stands.""" + d = block_object(e) + return d is not None and is_int(d.t) + + +def is_blockfloat(e): + d = block_object(e) + return d is not None and d.t is F32 + + +def is_ptrparam(e): + """The address of a parameter: with dccpeep, a later assignment to the + parameter can be dropped as dead (BUG-053, seed 3036).""" + return e.op == 'addrof' and e.a.storage == 'param' + + +def is_loopcallarg(e): + """A call (including the repair's fclamp) with a non-global float + variable, or an assignment to one, as an argument: inside a loop the + BUG-036 shape for floats (seeds 3025 and 3288), and the local is garbage + or the assigned constant afterwards.""" + if e.op not in ('call', 'fclamp'): + return False + for a in e.k: + v = a.k[0] if a.op == 'asg' else a + if v.op == 'var' and v.a.storage != 'global' and v.a.t is F32: + return True + return False + + +def is_selfop(e): + """`b OP b` or `b OP= b` with one non-global float or 32-bit variable + as both operands: the BUG-026 shape for floats (seeds 1681 and 3223).""" + if e.op not in ('bin', 'asg') or e.a in LOGIC: + return False + a, b = e.k + return same_variable(a, b) and a.a.storage != 'global' and is_wide(a.a.t) + + +def blockint_operands(e): + """Indexes of e's operands that are the BUG-015 facet shapes: a + nested-block integer variable converted to float by its context + (arithmetic, comparison, `?:` arm, compound assignment, or a store to + a float element, member or pointee), or an integer operand of float + arithmetic/comparison/compound assignment whose other operand is a + nested-block float variable. A call argument and a store to a plain + float variable convert correctly, and so does `(float)b`.""" + if e.op in ('call', 'cast'): + return [] + if e.op == 'asg' and e.a == '=' and e.k[0].op == 'var': + return [] + out = [i for i, (k, want) in enumerate(zip(e.k, float_contexts(e))) + if want is F32 and is_blockint(k)] + if (e.op == 'bin' and e.a in ('+', '-', '*', '/') + CMP) or \ + (e.op == 'asg' and e.a != '='): + if any(is_blockfloat(k) for k in e.k): + out += [i for i, k in enumerate(e.k) if is_int(ty(k)) and i not in out] + return out + + +def simple_const(v, t): + """A constant of type t worth v (the reducer's replacement value).""" + if t is F32: + return E('const', a=float(v), t=F32) + ct = t if t.bits >= 16 else (I16 if t.signed else U16) + return mk_const(v, ct) + + +# ---------------------------------------------------------------- shapes + +def known_shapes(prog): + """Features of KNOWN_BUGS whose shape prog contains.""" + found = set() + for slot in expr_slots(prog): + e = slot_get(slot) + if e.op == 'bin' and e.a == '/' and ty(e) is F32: + found.add('fdivround') + if e.op == 'asg' and e.a == '/=' and (ty(e.k[0]) is F32 or ty(e.k[1]) is F32): + found.add('fdivround') + # the value of a float ++/--: an incdec that is not a whole expression + # statement or a for step + def fincdec(e): + return e is not None and e.op == 'incdec' and ty(e) is F32 + + for slot in expr_slots(prog): + e = slot_get(slot) + if fincdec(e): + found.add('fincdec') + if any(fincdec(k) for k in e.k): + found.add('fincdecval') + if needs_fconst_fold(e): + found.add('fconstfold') + if e.op == 'asg' and e.a != '=' and is_int(ty(e.k[0])) and ty(e.k[1]) is F32: + found.add('icasgf') + if e.op == 'un' and e.a == '-' and is_blockfloat(e.k[0]): + found.add('fblockneg') + if is_selfop(e): + found.add('fselfop') + if is_loopcallarg(e): + found.add('floopcallarg') + if is_ptrparam(e): + found.add('fptrparam') + if e.op == 'cond' and base.is_constant(e.k[0]): + found.add('fconstcond') + if is_paramcmp(e): + found.add('fparamcmp') + if is_negcmp(e): + found.add('fnegcmp') + if is_intprodadd(e): + found.add('fintprodadd') + if e.op == 'un' and e.a == '!' and ty(e.k[0]) is F32: + found.add('fnegzero_not') + if blockint_operands(e): + found.add('fblockint') + for k, want in zip(e.k, float_contexts(e)): + if want is F32 and is_u32s(k): + found.add('fu32cast') + for b in all_blocks(prog): + for d in b.decls: + if d.t is F32 and isinstance(d.init, E) and is_u32s(d.init): + found.add('fu32cast') + if d.storage == 'block' and is_int(d.t) and isinstance(d.init, E) \ + and not init_keeps_type(d.init, d.t): + found.add('finittype') + for b in all_blocks(prog): + if any(fincdec(d.init) for d in b.decls if isinstance(d.init, E)): + found.add('fincdecval') + for st in b.stmts: + if base.const_jump(st): + found.add('fconstjump') + roots = [st.e[0]] if st.op == 'for' else ([] if st.op == 'expr' else st.e) + if st.op == 'for' and st.a is not None and isinstance(st.a.init, E): + roots.append(st.a.init) + if any(fincdec(e) for e in roots): + found.add('fincdecval') + tests = [st.e[1]] if st.op == 'for' else (st.e if st.op in ('if', 'while') else []) + if any(e is not None and is_fconst(e) for e in tests): + found.add('fconstfold') + for fn in prog.funcs: + stack = [fn.body] + while stack: + b = stack.pop() + for st in b.stmts: + if st.op == 'ret' and st.e and is_retconv(fn, st.e[0]): + found.add('fretconv') + stack.extend(sub for sub in st.b if sub is not None) + return found + + +def float_contexts(e): + """For each operand of e, the type it is converted to (F32 when the + operand is implicitly or explicitly made a float), else None.""" + if e.op == 'cast': + return [e.t] + if e.op == 'bin' and e.a in ('+', '-', '*', '/'): + t = ty(e) + return [t, t] + if e.op == 'bin' and e.a in CMP: + t = common(ty(e.k[0]), ty(e.k[1])) + return [t, t] + if e.op == 'asg': + return [None, ty(e.k[0])] + if e.op == 'cond': + t = ty(e) + return [None, t, t] + if e.op == 'call': + return [p.t for p in e.a.params] + return [None] * len(e.k) + + +# ---------------------------------------------------------------- repair + +def clamp(x, bound): + """x limited to [-bound, bound] through the program's fclamp helper. + A bare non-global variable is passed as `x + 0.0f`: fclamp is a call, and + a 32-bit local passed directly to a call in a loop is BUG-036 (seed + 3288), which the repair must not build.""" + if x.op == 'var' and x.a.storage != 'global' and ty(x) is F32: + x = E('bin', [x, E('const', a=0.0, t=F32)], '+') + return E('fclamp', [x, E('const', a=bound, t=F32)]) + + +def has_effect(e): + if e.op in ('asg', 'incdec', 'call'): + return True + return any(has_effect(k) for k in e.k) + + +def mentions(e, decl): + if e.op in ('var', 'idx', 'pidx', 'aend', 'addrof') and e.a is decl: + return True + if e.op == 'addr' and e.a[0] is decl: + return True + return any(mentions(k, decl) for k in e.k) + + +def tame(x, bound): + if x.op == 'fclamp': + x.k[1] = E('const', a=min(bound, x.k[1].a) / 4.0, t=F32) + return x + return clamp(x, bound) + + +def f2i_bound(t): + return {8: 100.0, 16: 30000.0, 32: 2.0e9}[t.bits] + + +def repair_node(node): + """Make a tripped float node defined. Returns False when it cannot.""" + op = node.op + t = ty(node) + if op == 'bin' and t is F32: + node.k[1] = E('const', a=1.5 if node.a != '*' else 0.5, t=F32) + return True + if op == 'cast' and is_int(t) and ty(node.k[0]) is F32: + node.k[0] = tame(node.k[0], f2i_bound(t)) + return True + if op == 'asg': + lv, rhs = node.k + lt = ty(lv) + if lt is F32: + if node.a == '=': + return False + node.a, node.k = '=', [lv, E('const', a=2.5, t=F32)] + return True + if ty(rhs) is F32 and is_int(lt): + if node.a != '=': + node.a = '=' + node.k[1] = tame(rhs, f2i_bound(lt)) + return True + return base.repair_node(node) + if op == 'incdec' and ty(node.k[0]) is F32: + node.op, node.a, node.k = 'asg', '=', [node.k[0], E('const', a=1.0, t=F32)] + return True + return base.repair_node(node) + + +def repair(prog, trips): + """Repair every tripped node still present; nodes a parent's repair + removed from the tree are not failures. Returns the ids left tripping.""" + trips = set(trips) + failed = set() + for slot in expr_slots(prog): + node = slot_get(slot) + if node.id in trips and not repair_node(node): + failed.add(node.id) + present = {slot_get(slot).id for slot in expr_slots(prog)} + return failed & present + + +# -------------------------------------------------------------- rendering + +class FRenderer(Renderer): + """The integer renderer, with float-typed nodes rendered plainly in the + natural source and through ck_f / ck_f2* in the checked one.""" + + def f2i(self, t, s, node_id): + if self.checked: + return 'ck_f2%s%d(%d, %s)' % ('i' if t.signed else 'u', t.bits, node_id, s) + return '((%s)%s)' % (t.cname, s) + + def ex_const(self, e): + if e.t is F32: + return float_text(e.a) + return super().ex_const(e) + + def ex_cast(self, e): + src = ty(e.k[0]) + if e.t is F32: + return '((float)%s)' % self.ex(e.k[0]) + if src is F32: + return self.f2i(e.t, self.ex(e.k[0]), e.id) + return super().ex_cast(e) + + def ex_un(self, e): + if ty(e.k[0]) is F32: + return '(%s%s)' % (e.a, self.ex(e.k[0])) + return super().ex_un(e) + + def ex_bin(self, e): + ka, kb = e.k + if ty(ka) is not F32 and ty(kb) is not F32: + return super().ex_bin(e) + s = '(%s %s %s)' % (self.ex(ka), e.a, self.ex(kb)) + if self.checked and e.a in FARITH + ('/',): + return 'ck_f(%d, %s)' % (e.id, s) + return s + + def ex_fclamp(self, e): + return 'fclamp(%s, %s)' % (self.ex(e.k[0]), self.ex(e.k[1])) + + def ex_cond(self, e): + if ty(e) is F32: + return '(%s ? %s : %s)' % (self.ex(e.k[0]), self.ex(e.k[1]), self.ex(e.k[2])) + return super().ex_cond(e) + + def ex_asg(self, e): + lv, rhs = e.k + lt, rt = ty(lv), ty(rhs) + if lt is not F32 and rt is not F32: + return super().ex_asg(e) + left, right = self.lv(lv), self.ex(rhs) + if lt is F32: + if e.a == '=' or not self.checked: + return '(%s %s %s)' % (left, e.a, right) + return '(%s = ck_f(%d, %s %s %s))' % (left, e.id, left, e.a[:-1], right) + # integer target, float value: the conversion is on the stored value + if e.a == '=': + if not self.checked: + return '(%s = %s)' % (left, right) + return '(%s = %s)' % (left, self.f2i(lt, right, e.id)) + if not self.checked: + return '(%s %s %s)' % (left, e.a, right) + return '(%s = %s)' % (left, self.f2i(lt, '(%s %s %s)' % (left, e.a[:-1], right), e.id)) + + def ex_incdec(self, e): + lv = e.k[0] + if ty(lv) is not F32: + return super().ex_incdec(e) + op, prefix = e.a + left = self.lv(lv) + if not self.checked: + return '(%s%s)' % (op, left) if prefix else '(%s%s)' % (left, op) + d = '1.0f' if op == '++' else '-1.0f' + if prefix: + return '(%s = ck_f(%d, %s + %s))' % (left, e.id, left, d) + return 'ck_fpost(%d, &%s, %s)' % (e.id, left, d) + + +# -------------------------------------------------------------- generation + +class Ctx: + def __init__(self, fn, pure): + self.fn, self.pure = fn, pure + self.floats, self.ints, self.farrs, self.fptrs, self.svars = [], [], [], [], [] + self.prot = set() + self.loop = 0 + self.names = 0 + self.fresh() + + def fresh(self): + """Start a full expression: nothing read (used) or written (banned).""" + self.used, self.banned = set(), set() + + def mark(self): + return [len(x) for x in (self.floats, self.ints, self.farrs, self.fptrs, self.svars)] + + def release(self, mark): + for lst, n in zip((self.floats, self.ints, self.farrs, self.fptrs, self.svars), mark): + del lst[n:] + + def can_write(self, d): + if d in self.prot: + return False + return d.storage != 'global' or not self.pure + + def name(self, prefix): + self.names += 1 + return '%s%d' % (prefix, self.names) + + +class Gen: + def __init__(self, seed, tests=6, size=10, depth=3, off=frozenset()): + self.rng = random.Random(seed) + self.prog = Program(seed, off) + self.prog.floats = True + self.n_tests, self.size, self.depth = tests, size, depth + self.off = frozenset(off) + self.helpers = [] + + def on(self, feature): + return feature not in self.off + + def mk(self, op, k=(), a=None, t=None): + self.prog.nid += 1 + return E(op, list(k), a, t, self.prog.nid) + + def pick(self, weighted): + total = sum(w for _, w in weighted) + x = self.rng.random() * total + for item, w in weighted: + x -= w + if x < 0: + return item + return weighted[-1][0] + + # ---- values + + def fvalue(self): + r = self.rng + if r.random() < 0.6: + return F32.wrap(r.choice(FPOOL)) + mag = 10.0 ** r.uniform(-3, 5) + v = mag if r.random() < 0.7 else -mag + if r.random() < 0.5: + v = round(v, r.randint(0, 3)) + return F32.wrap(v) + + def fconst(self): + return self.mk('const', a=self.fvalue(), t=F32) + + def int_type(self): + return self.pick([(I8, 2 if self.on('i8') else 0), (U8, 2 if self.on('i8') else 0), + (I16, 5), (U16, 4), (I32, 4), (U32, 3 if self.on('u32') else 0)]) + + def ivalue(self, t): + r = self.rng + x = r.random() + if x < 0.5: + v = r.choice([0, 1, 2, 3, 7, 10, 100, 127, 128, 255, 256, 1000, 32767, 32768, + 65535, 65536, 16777216, 16777217, 2147483647, -1, -2, -100, -128, + -32768, -32769, 1234567]) + elif x < 0.8: + v = r.randint(-300, 300) + else: + v = r.getrandbits(32) + return t.wrap(v) + + def iconst(self, t): + if t.bits < 16: + return self.mk('const', a=self.rng.randint(0, 100), t=I16) + return self.mk('const', a=self.ivalue(t), t=t) + + def small(self, lo, hi): + return self.mk('const', a=self.rng.randint(lo, hi), t=I16) + + # ---- skeleton + + def gen_structs(self): + r = self.rng + fields = [Field('m0', F32), Field('m1', self.int_type())] + if self.on('farr'): + fields.append(Field('m2', F32, dims=(2,))) + fields.append(Field('m3', F32)) + r.shuffle(fields) + self.prog.structs.append(StructType('S0', fields)) + + def init_value(self, t, dims): + if dims: + return [self.init_value(t, dims[1:]) for _ in range(dims[0])] + if isinstance(t, StructType): + return [self.init_value(f.t, f.dims) for f in t.fields] + if t is F32: + return self.fvalue() + return self.ivalue(t) + + def gen_globals(self): + r = self.rng + g = self.prog.globals + for n in range(r.randint(2, 4)): + d = Decl('fg%d' % n, F32, 'global', init=self.fvalue()) + d.static = r.random() < 0.6 + g.append(d) + if self.on('farr'): + d = Decl('fa', F32, 'global', dims=(8,), init=self.init_value(F32, (8,))) + d.static = r.random() < 0.6 + g.append(d) + for n, t in enumerate((I16, U16, I32, U32, I8, U8)): + if t.bits == 8 and not self.on('i8') or t is U32 and not self.on('u32'): + continue + d = Decl('g%d' % n, t, 'global', init=self.ivalue(t)) + d.static = r.random() < 0.6 + g.append(d) + d = Decl('ia', I16, 'global', dims=(8,), init=self.init_value(I16, (8,))) + g.append(d) + if self.on('fstruct'): + s0 = self.prog.structs[0] + d = Decl('s0', s0, 'global', init=self.init_value(s0, ())) + d.static = True + g.append(d) + d = Decl('s1', s0, 'global', dims=(4,), init=self.init_value(s0, (4,))) + d.static = True + g.append(d) + + def ctx(self, fn, pure): + c = Ctx(fn, pure) + for d in self.prog.globals: + self.expose(c, d) + return c + + def expose(self, c, d): + if isinstance(d.t, PtrType): + c.fptrs.append(d) + elif isinstance(d.t, StructType): + if not d.dims: + c.svars.append(d) + else: + c.svars.append(d) + elif d.dims: + if d.t is F32: + c.farrs.append(d) + elif d.t is F32: + c.floats.append(d) + else: + c.ints.append(d) + + # ---- lvalues and leaves + + def index(self, c, n): + r = self.rng + ints = self.sequenced(c, c.ints, False) + if r.random() < 0.5 or not ints: + return self.small(0, n - 1) + return self.mk('var', a=self.touch(c, r.choice(ints), False)) + + def sequenced(self, c, decls, write): + """The objects in decls that this full expression may still read + (not written in it) or write (not touched in it), so that clang's + -Wunsequenced never rejects the program.""" + if write: + return [d for d in decls if c.can_write(d) and d not in c.used] + return [d for d in decls if d not in c.banned] + + def touch(self, c, d, write): + """Record a read or write of d in the current full expression.""" + c.used.add(d) + if write: + c.banned.add(d) + return d + + def flvalue(self, c, write): + """A float lvalue; write asks for one that may be stored to.""" + r = self.rng + cands = self.sequenced(c, c.floats, write) + opts = [] + if cands: + opts.append(('var', 6)) + arrs = self.sequenced(c, c.farrs, write) + if arrs and self.on('farr'): + opts.append(('arr', 2)) + if c.fptrs and self.on('fptr') and (not write or not c.pure): + opts.append(('ptr', 2)) + svars = self.sequenced(c, c.svars, write) + if svars and self.on('fstruct'): + opts.append(('fld', 2)) + if not opts: + return None + kind = self.pick(opts) + if kind == 'var': + return self.mk('var', a=self.touch(c, r.choice(cands), write)) + if kind == 'arr': + d = self.touch(c, r.choice(arrs), write) + return self.mk('idx', [self.index(c, d.dims[0])], a=d) + if kind == 'ptr': + return self.mk('deref', [self.mk('var', a=r.choice(c.fptrs))]) + d = self.touch(c, r.choice(svars), write) + ffields = [f for f in d.t.fields if f.t is F32] + f = r.choice(ffields) + if d.dims: + basee = self.mk('idx', [self.index(c, d.dims[0])], a=d) + else: + basee = self.mk('var', a=d) + kids = [basee] + if f.dims: + kids.append(self.small(0, f.dims[0] - 1)) + return self.mk('fld', kids, a=f) + + def ilvalue(self, c, write): + cands = self.sequenced(c, c.ints, write) + if not cands: + return None + return self.mk('var', a=self.touch(c, self.rng.choice(cands), write)) + + # ---- expressions + + def fexpr(self, c, depth): + """A float-typed expression.""" + r = self.rng + if depth <= 0 or r.random() < 0.25: + opts = [('lv', 6), ('const', 4 if self.on('fconst') else 0)] + ints = self.sequenced(c, c.ints, False) + if ints and self.on('i2f'): + opts.append(('i2f', 3)) + kind = self.pick(opts) + if kind == 'lv': + lv = self.flvalue(c, False) + if lv is not None: + return lv + kind = 'const' + if kind == 'const': + return self.fconst() + return self.mk('cast', [self.mk('var', a=self.touch(c, r.choice(ints), False))], t=F32) + opts = [('arith', 8), ('div', 2 if self.on('fdiv') and self.on('fdivround') else 0), + ('neg', 2 if self.on('fneg') else 0), + ('cond', 2 if self.on('fcond') else 0), + ('call', 3 if self.on('fcall') and self.helpers else 0), + ('mix', 3 if self.on('mixarith') and c.ints else 0), + ('i2f', 2 if self.on('i2f') else 0), + ('asg', 1 if self.on('fasgval') else 0), + ('incdec', 1 if self.on('fincdecval') and self.on('fincdec') else 0), + ('chain', 2 if self.on('fchain') else 0)] + kind = self.pick(opts) + if kind == 'arith': + return self.mk('bin', [self.fexpr(c, depth - 1), self.fexpr(c, depth - 1)], + r.choice(FARITH)) + if kind == 'div': + return self.mk('bin', [self.fexpr(c, depth - 1), self.fexpr(c, depth - 1)], '/') + if kind == 'neg': + e = self.fexpr(c, depth - 1) + if is_fconst(e) and not self.on('fconstfold'): + return e + if is_blockfloat(e) and not self.on('fblockneg'): + return e + return self.mk('un', [e], '-') + if kind == 'cond': + test = self.cond(c, depth - 1) + if not self.on('fconstcond') and base.is_constant(test): + return self.fexpr(c, depth - 1) # BUG-052: a folded `?:` + return self.mk('cond', [test, self.fexpr(c, depth - 1), self.fexpr(c, depth - 1)]) + if kind == 'call': + return self.call(c, depth, F32) + if kind == 'mix': + # one operand is an integer: the usual conversions make it float + a = self.fexpr(c, depth - 1) + b = self.iexpr(c, depth - 1) + k = [a, b] if r.random() < 0.5 else [b, a] + return self.mk('bin', k, r.choice(FARITH)) + if kind == 'i2f': + return self.mk('cast', [self.iexpr(c, depth - 1)], t=F32) + if kind == 'asg': + lv = self.flvalue(c, True) + if lv is None: + return self.fconst() + return self.mk('asg', [lv, self.rhs_for(lv, self.fexpr(c, depth - 1))], '=') + if kind == 'incdec': + lv = self.flvalue(c, True) + if lv is None: + return self.fconst() + return self.mk('incdec', [lv], (r.choice(('++', '--')), r.random() < 0.5)) + # chain: a left-deep sum/product of three or four terms + e = self.fexpr(c, 0) + for _ in range(r.randint(2, 3)): + e = self.mk('bin', [e, self.fexpr(c, 0)], r.choice(FARITH)) + return e + + def iexpr(self, c, depth, t=None): + """An integer-typed expression, plain enough to be portable.""" + r = self.rng + if t is None: + t = self.int_type() + if depth <= 0 or r.random() < 0.3: + ints = self.sequenced(c, c.ints, False) + cands = [d for d in ints if d.t is t] or ints + if cands and r.random() < 0.7: + d = self.touch(c, r.choice(cands), False) + e = self.mk('var', a=d) + return e if d.t is t else self.mk('cast', [e], t=t) + return self.iconst(t) + opts = [('f2i', 5 if self.on('f2i') else 0), ('arith', 3), ('cmp', 2 if self.on('fcmp') else 0), + ('call', 2 if self.on('fcall') and self.helpers else 0)] + kind = self.pick(opts) + if kind == 'f2i': + if (t.bits == 8 and not self.on('f2i8')) or (not t.signed and not self.on('f2u')): + return self.iconst(t) + return self.mk('cast', [self.fexpr(c, depth - 1)], t=t) + if kind == 'arith': + # computed in 32 bits so neither 16-bit int can overflow + a = self.mk('cast', [self.iexpr(c, depth - 1)], t=I32) + b = self.mk('cast', [self.small(0, 100)], t=I32) + e = self.mk('bin', [a, b], r.choice(('+', '-'))) + return self.mk('cast', [e], t=t) + if kind == 'cmp': + e = self.cond(c, depth - 1) + return self.mk('cast', [e], t=t) if t is not I16 else e + e = self.call(c, depth, None) + return e if ty(e) is t else self.mk('cast', [e], t=t) + + def cond(self, c, depth): + """A controlling expression (dcc type int).""" + r = self.rng + opts = [('fcmp', 6 if self.on('fcmp') else 0), + ('fcmpconst', 3 if self.on('fcmp') and self.on('fcmpconst') else 0), + ('ftruth', 1 if self.on('ftruth') else 0), + ('fnot', 1 if self.on('fnot') else 0), + ('logic', 2 if self.on('flogic') and depth > 0 else 0), + ('icmp', 2)] + kind = self.pick(opts) + if kind == 'fcmp': + return self.cmp_ok(self.fexpr(c, depth), self.fexpr(c, depth), r.choice(CMP)) + if kind == 'fcmpconst': + return self.cmp_ok(self.fexpr(c, depth), self.fconst(), r.choice(CMP)) + if kind in ('ftruth', 'fnot'): + e = self.fexpr(c, depth) + if is_fconst(e) and not self.on('fconstfold'): + return self.mk('bin', [e, self.fconst()], r.choice(CMP)) + return e if kind == 'ftruth' else self.mk('un', [e], '!') + if kind == 'logic': + e = self.mk('bin', [self.cond(c, depth - 1), self.cond(c, depth - 1)], + r.choice(LOGIC)) + if needs_fconst_fold(e) and not self.on('fconstfold'): + return e.k[0] + return e + a = self.iexpr(c, depth, I32) + return self.mk('bin', [a, self.mk('cast', [self.small(-50, 50)], t=I32)], r.choice(CMP)) + + def cmp_ok(self, a, b, rel): + """a REL b, swapped when the right operand is the BUG-045 shape.""" + e = self.mk('bin', [a, b], rel) + if is_negcmp(e) and not self.on('fnegcmp'): + e = self.mk('bin', [b, a], FLIP[rel]) + if is_negcmp(e): + e = self.mk('bin', [b, a.k[1]], FLIP[rel]) + return e + + def call(self, c, depth, want): + r = self.rng + fns = [f for f in self.helpers if f.ret is want] if want is not None else self.helpers + fns = [f for f in fns if f is not c.fn] or [f for f in self.helpers if f is not c.fn] + if not fns: + return self.fconst() if want is F32 else self.iconst(want or I16) + fn = r.choice(fns) + args = [] + for p in fn.params: + if p.t is F32: + args.append(self.fexpr(c, depth - 1)) + else: + args.append(self.iexpr(c, depth - 1, p.t)) + e = self.mk('call', args, a=fn) + if not self.on('floopcallarg') and is_loopcallarg(e): + e.k = [self.mk('bin', [a, E('const', a=0.0, t=F32)], '+') + if (a.k[0] if a.op == 'asg' else a).op == 'var' and ty(a) is F32 else a + for a in e.k] + return e + + # ---- statements + + def gen_stmt(self, c, depth, block): + r = self.rng + c.fresh() + opts = [('fasg', 10), ('fcasg', 4 if self.on('fcassign') else 0), + ('iasg', 4), ('fincdec', 2 if self.on('fincdec') else 0), + ('if', 4 if self.on('fif') and depth > 0 else 0), + ('for', 3 if self.on('ffor') and depth > 0 and c.loop < 2 else 0), + ('while', 2 if self.on('fwhile') and depth > 0 and c.loop < 2 else 0), + ('block', 2 if self.on('fblock') and depth > 0 else 0), + ('ret', 1 if self.on('fret_early') and c.fn.ret is not None and c.loop == 0 + and depth < self.depth else 0), + ('call', 1 if self.on('fcall') and self.helpers else 0)] + kind = self.pick(opts) + if kind == 'fasg': + lv = self.flvalue(c, True) + if lv is None: + return + rhs = self.rhs_for(lv, self.fexpr(c, depth + 1)) + block.stmts.append(S('expr', [self.mk('asg', [lv, rhs], '=')])) + elif kind == 'fcasg': + lv = self.flvalue(c, True) + if lv is None: + return + ops = ['+=', '-=', '*='] + if self.on('fdiv') and self.on('fdivround'): + ops.append('/=') + rhs = self.fexpr(c, depth) if r.random() < 0.7 or not c.ints \ + else self.iexpr(c, depth) + rhs = self.rhs_for(lv, rhs) + block.stmts.append(S('expr', [self.mk('asg', [lv, rhs], r.choice(ops))])) + elif kind == 'iasg': + lv = self.ilvalue(c, True) + if lv is None: + return + t = ty(lv) + if r.random() < 0.6 and self.on('f2i') and (t.bits > 8 or self.on('f2i8')) \ + and (t.signed or self.on('f2u')): + # implicit conversion on the store + rhs = self.rhs_for(lv, self.fexpr(c, depth)) + if self.on('fcassign') and self.on('icasgf') and r.random() < 0.3: + block.stmts.append(S('expr', [self.mk('asg', [lv, rhs], r.choice(('+=', '-=', '*=')))])) + return + else: + rhs = self.rhs_for(lv, self.iexpr(c, depth, t)) + block.stmts.append(S('expr', [self.mk('asg', [lv, rhs], '=')])) + elif kind == 'fincdec': + lv = self.flvalue(c, True) + if lv is None: + return + block.stmts.append(S('expr', [self.mk('incdec', [lv], (r.choice(('++', '--')), + r.random() < 0.5))])) + elif kind == 'if': + then = Block() + self.body(c, depth - 1, then, r.randint(1, 3)) + blocks = [then] + if r.random() < 0.5: + els = Block() + self.body(c, depth - 1, els, r.randint(1, 2)) + blocks.append(els) + c.fresh() + block.stmts.append(S('if', [self.cond(c, 1)], blocks)) + elif kind == 'for': + cnt = Decl(c.name('i'), I16, 'block') + body = Block() + c.loop += 1 + m = c.mark() + c.ints.append(cnt) + c.prot.add(cnt) + self.body(c, depth - 1, body, r.randint(1, 3)) + c.release(m) + c.prot.discard(cnt) + c.loop -= 1 + n = r.randint(1, 8) + cnt.rng = n - 1 + init = self.mk('asg', [self.mk('var', a=cnt), self.small(0, 0)], '=') + test = self.mk('bin', [self.mk('var', a=cnt), self.small(n, n)], '<') + step = self.mk('incdec', [self.mk('var', a=cnt)], ('++', True)) + if r.random() < 0.5: + cnt.init = self.small(0, 0) + block.stmts.append(S('for', [None, test, step], [body], cnt)) + else: + cnt.storage = 'local' + c.fn.body.decls.append(cnt) + block.stmts.append(S('for', [init, test, step], [body])) + elif kind == 'while': + # while (f < limit) { ...; f += step; } with f fresh and the bound + # proved by an integer counter + cnt = Decl(c.name('n'), I16, 'local', init=self.small(1, 6)) + c.fn.body.decls.append(cnt) + acc = Decl(c.name('w'), F32, 'local', init=self.fconst()) + c.fn.body.decls.append(acc) + body = Block() + c.loop += 1 + m = c.mark() + c.floats.append(acc) + c.prot.add(cnt) + self.body(c, depth - 1, body, r.randint(1, 3)) + c.release(m) + c.prot.discard(cnt) + c.loop -= 1 + dec = self.mk('asg', [self.mk('var', a=cnt), + self.mk('bin', [self.mk('var', a=cnt), self.small(1, 1)], '-')], '=') + body.stmts.append(S('expr', [dec])) + test = self.mk('bin', [self.mk('var', a=cnt), self.small(0, 0)], '>') + if r.random() < 0.5 and self.on('flogic'): + test = self.mk('bin', [test, self.mk('bin', [self.mk('var', a=acc), self.fconst()], + r.choice(('<', '>', '!=')))], '&&') + block.stmts.append(S('while', [test], [body])) + elif kind == 'block': + inner = Block() + m = c.mark() + for _ in range(r.randint(1, 2)): + c.fresh() + if r.random() < 0.7: + d = Decl(c.name('b'), F32, 'block', init=self.fexpr(c, 1)) + c.floats.append(d) + else: + t = self.int_type() + init = self.iexpr(c, 1, t) + d = Decl(c.name('b'), t, 'block', init=init) + c.ints.append(d) + if not self.on('finittype') and not init_keeps_type(init, t): + # BUG-016: initialise with a constant, assign after + d.init = mk_const(0, t if t.bits >= 16 else I16) + inner.stmts.append(S('expr', [self.mk('asg', [self.mk('var', a=d), init], '=')])) + inner.decls.append(d) + self.body(c, depth - 1, inner, r.randint(1, 3)) + c.release(m) + block.stmts.append(S('block', [], [inner])) + elif kind == 'ret': + cond = self.cond(c, 1) + for _ in range(8): + if self.on('fconstjump') or not base.is_constant(cond): + break + c.fresh() + cond = self.cond(c, 1) + else: + return # BUG-019: `if (K) { return …; }` before a declaring block + c.fresh() + then = Block() + then.stmts.append(S('ret', [self.result(c)])) + block.stmts.append(S('if', [cond], [then])) + elif kind == 'call': + lv = self.flvalue(c, True) + e = self.call(c, 2, F32) + if lv is not None: + e = self.mk('asg', [lv, e], '=') + block.stmts.append(S('expr', [e])) + + def rhs_for(self, lv, rhs): + """rhs, unless it has a side effect on lv's object: that is + unsequenced against the store.""" + if not has_effect(rhs): + return rhs + base_e = lv + while base_e.op in ('fld', 'deref'): + base_e = base_e.k[0] + decl = base_e.a if base_e.op in ('var', 'idx') else None + if decl is None or mentions(rhs, decl) or (lv.op == 'deref'): + return self.fconst() if ty(rhs) is F32 else self.iconst(ty(rhs)) + return rhs + + def body(self, c, depth, block, n): + for _ in range(n): + self.gen_stmt(c, depth, block) + + def result(self, c): + t = c.fn.ret + c.fresh() + e = self.fexpr(c, 2) if t is F32 else self.iexpr(c, 2, t) + if is_retconv(c.fn, e) and not self.on('fretconv'): + return self.mk('cast', [e], t=t) + return e + + # ---- functions + + def params_for(self, c, fn, n): + r = self.rng + for i in range(n): + if r.random() < 0.65 and self.on('fparam') or not self.on('iparam'): + d = Decl('x%d' % i, F32, 'param') + c.floats.append(d) + else: + t = self.int_type() + d = Decl('x%d' % i, t, 'param') + c.ints.append(d) + fn.params.append(d) + + def locals_for(self, c, fn, n): + r = self.rng + for i in range(n): + x = r.random() + if x < 0.55: + d = Decl('l%d' % i, F32, 'local', init=self.fvalue()) + c.floats.append(d) + elif x < 0.65 and self.on('fptr') and (c.floats or c.farrs): + tgts = c.floats + c.farrs + if not self.on('fptrparam'): + tgts = [t for t in tgts if t.storage != 'param'] or c.farrs + if not tgts: + d = Decl('l%d' % i, F32, 'local', init=self.fvalue()) + c.floats.append(d) + continue + tgt = r.choice(tgts) + if tgt.dims: + k = r.randint(0, tgt.dims[0] - 1) + init = self.mk('addr', [self.small(k, k)], a=(tgt, 1)) + else: + init = self.mk('addrof', a=tgt) + tgt.addr_taken = True + d = Decl('p%d' % i, PtrType(F32), 'local', init=init) + d.tgt_local = tgt.storage != 'global' + c.fptrs.append(d) + elif x < 0.75 and self.on('farr'): + d = Decl('la%d' % i, F32, 'local', dims=(4,), init=self.init_value(F32, (4,))) + c.farrs.append(d) + else: + t = self.int_type() + # an initialiser of the local's own type (BUG-016 is logged) + d = Decl('l%d' % i, t, 'local', init=self.ivalue(t)) + c.ints.append(d) + fn.body.decls.append(d) + + def ret_type(self): + return F32 if self.rng.random() < 0.7 or not self.on('fret') else self.int_type() + + def gen_helper(self, n): + r = self.rng + fn = Func('h%d' % n, self.ret_type(), [], 'pure') + c = self.ctx(fn, True) + self.params_for(c, fn, r.randint(1, 3)) + self.locals_for(c, fn, r.randint(0, 2)) + for _ in range(r.randint(0, 3)): + self.gen_stmt(c, 1, fn.body) + fn.body.stmts.append(S('ret', [self.result(c)])) + return fn + + def gen_test(self, n): + r = self.rng + fn = Func('f%d' % n, self.ret_type(), [], 'test') + fn.static = r.random() < 0.7 + c = self.ctx(fn, False) + self.params_for(c, fn, r.randint(0, 4)) + self.locals_for(c, fn, r.randint(2, 5)) + for _ in range(r.randint(max(2, self.size // 2), self.size)): + self.gen_stmt(c, self.depth, fn.body) + fn.body.stmts.append(S('ret', [self.result(c)])) + for _ in range(3): + args = [] + for p in fn.params: + if p.t is F32: + args.append(self.mk('const', a=self.fvalue(), t=F32)) + else: + t = p.t if p.t.bits >= 16 else (I16 if p.t.signed else U16) + args.append(self.mk('const', a=t.wrap(self.ivalue(p.t)), t=t)) + fn.inputs.append(args) + return fn + + def generate(self): + r = self.rng + self.gen_structs() + self.gen_globals() + for n in range(r.randint(2, 3)): + fn = self.gen_helper(n) + self.prog.funcs.append(fn) + self.helpers.append(fn) + for n in range(self.n_tests): + self.prog.funcs.append(self.gen_test(n)) + self.steer() + return self.prog + + # ---- post-generation steering away from logged shapes + + def fcast(self, e): + return self.mk('cast', [e], t=F32) + + def fix_negrhs(self, rhs): + """`C OP y` rewritten so the constant is not the left operand, + exactly: `y + C`, `y * C`, `-(y - C)`, `(C / y) + 0.0f`. rhs is + edited in place so the slot walk still visits its operands.""" + c, y = rhs.k + if rhs.a in ('+', '*'): + rhs.k[0], rhs.k[1] = y, c + return rhs + if rhs.a == '-': + rhs.k[0], rhs.k[1] = y, c + return self.mk('un', [rhs], '-') + return self.mk('bin', [rhs, E('const', a=0.0, t=F32)], '+') + + def strip_u32s(self, e): + """The BUG-048/050 cast chain e with the offending cast removed: + `(uint32_t)x` -> `x`; `(int32_t)((uint32_t)n + 1)` -> `(int32_t)(n + 1)`; + `(int32_t)(u32 expression)` -> `(int32_t)(uint16_t)(u32 expression)`.""" + if e.t is U32: + return e.k[0] + outer = e + while outer.k[0].op == 'cast' and outer.k[0].t is I32: + outer = outer.k[0] + inner = outer.k[0] + if ty(inner) is U32: + outer.k[0] = self.mk('cast', [inner], t=U16) + return e + if inner.op == 'cast': + e.k[0] = inner.k[0] + else: + while inner.k[0].op != 'cast': + inner = inner.k[0] + inner.k[0] = inner.k[0].k[0] + return e + + def steer(self): + """Rewrite the shapes of KNOWN_BUGS that are easier to recognise in + the finished tree than at the point of generation. Each rewrite + keeps the program valid C with the same meaning under clang. A + rewrite can expose another shape, so the pass runs to a fixpoint.""" + for _ in range(4): + before = self.prog.nid + self.steer_pass() + if self.prog.nid == before: + break + + def steer_pass(self): + for fn in self.prog.funcs: + paramcmp = not self.on('fparamcmp') + for slot in expr_slots(self.prog, [fn]): + e = slot_get(slot) + # the child lists are edited in place so that the slots + # collected above stay valid + if paramcmp and is_paramcmp(e): + # `v REL (p OP x)` -> `(p OP x) FLIP v`, else `v REL ((p OP x) + 0)` + self.prog.nid += 1 + swapped = self.mk('bin', [e.k[1], e.k[0]], FLIP[e.a]) + if not is_anyconst(e.k[0]) and not is_paramcmp(swapped): + e.k[0], e.k[1] = e.k[1], e.k[0] + e.a = FLIP[e.a] + else: + rhs = e.k[1] + zero = E('const', a=0.0, t=F32) if ty(rhs) is F32 \ + else mk_const(0, ty(rhs)) + e.k[1] = self.mk('bin', [rhs, zero], '+') + if not self.on('fnegcmp') and is_negcmp(e): + self.prog.nid += 1 + if is_anyconst(e.k[1]): + # `x REL (C1 OP C2)` -> `(C1 OP C2) FLIP x` + e.k[0], e.k[1] = e.k[1], e.k[0] + e.a = FLIP[e.a] + elif e.k[1].k[0].op == 'const': + e.k[1] = self.fix_negrhs(e.k[1]) + else: + # `x REL ((C) OP y)` -> `x REL (((C) OP y) * 1.0f)` + e.k[1] = self.mk('bin', [e.k[1], E('const', a=1.0, t=F32)], '*') + if not self.on('fintprodadd') and is_intprodadd(e): + if e.op == 'bin' and not is_fprod(e.k[0]): + self.prog.nid += 1 + e.k[0], e.k[1] = e.k[1], e.k[0] # `(x * y) + f` + else: # `f + ((x * y) + 0.0f)` + e.k[1] = self.mk('bin', [e.k[1], E('const', a=0.0, t=F32)], '+') + if not self.on('fnegzero_not') and e.op == 'un' and e.a == '!' \ + and ty(e.k[0]) is F32: + e2 = self.mk('bin', [], '==') + e2.k = e.k + e2.k.append(E('const', a=0.0, t=F32)) + slot_set(slot, e2) + e = e2 + if not self.on('fblockint'): + for i in blockint_operands(e): + e.k[i] = self.fcast(e.k[i]) + if not self.on('fselfop') and is_selfop(e): + # `b OP b` -> `b OP (b + 0)` + t = ty(e.k[1]) + zero = E('const', a=0.0, t=F32) if t is F32 else mk_const(0, t) + e.k[1] = self.mk('bin', [e.k[1], zero], '+') + if not self.on('fu32cast'): + for i, (k, want) in enumerate(zip(e.k, float_contexts(e))): + if want is F32 and is_u32s(k): + self.prog.nid += 1 + e.k[i] = self.strip_u32s(k) + if not self.on('fu32cast'): + for b in all_blocks_of(fn): + for d in b.decls: + if d.t is F32 and isinstance(d.init, E) and is_u32s(d.init): + self.prog.nid += 1 + d.init = self.strip_u32s(d.init) + + +def all_blocks_of(fn): + out = [] + + def walk(b): + out.append(b) + for st in b.stmts: + for sub in st.b: + if sub is not None: + walk(sub) + walk(fn.body) + return out + + +def generate(seed, tests=6, size=10, depth=3, off=frozenset()): + return Gen(seed, tests, size, depth, off).generate() + + +def render(prog, checked=False, title=''): + return FRenderer(checked, prog.off).program(prog, title) diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index 6588b03b2..b84d8496a 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -33,6 +33,7 @@ from __future__ import annotations import random +import struct CMP = ('<', '>', '<=', '>=', '==', '!=') LOGIC = ('&&', '||') @@ -86,6 +87,57 @@ def __repr__(self): UNSIGNED_OF = {I8: U8, I16: U16, I32: U32, U8: U8, U16: U16, U32: U32} +class FloatType: + """dcc's only floating type: 32-bit IEEE single. Only clang_oracle_float_gen + creates values of it; integer programs never contain one.""" + __slots__ = ('name', 'bits', 'signed', 'cnames') + + def __init__(self): + self.name, self.bits, self.signed, self.cnames = 'f32', 32, True, ('float',) + + def __deepcopy__(self, memo): + return self + + def __copy__(self): + return self + + @property + def cname(self): + return 'float' + + def wrap(self, v): + return struct.unpack('f', struct.pack('f', float(v)))[0] + + def __repr__(self): + return self.name + + +F32 = FloatType() + + +def is_float(t): + return t is F32 + + +def is_scalar(t): + return isinstance(t, IntType) or t is F32 + + +def float_text(v): + """A float constant: shortest decimal that round-trips to the single v.""" + v = F32.wrap(v) + if v == 0: + return '0.0f' + for digits in range(6, 10): + s = '%.*g' % (digits, v) + if F32.wrap(float(s)) == v: + break + if 'e' not in s and '.' not in s: + s += '.0' + s += 'f' + return '(%s)' % s if v < 0 else s + + class PtrType: __slots__ = ('to',) @@ -124,6 +176,8 @@ def promote(t): def common(a, b): """dcc's usual arithmetic conversions (int 16 bits, long 32 bits).""" + if a is F32 or b is F32: + return F32 a, b = promote(a), promote(b) if a.bits == 32 or b.bits == 32: return U32 if U32 in (a, b) else I32 @@ -213,13 +267,14 @@ def __init__(self, name, ret, params, kind): class Program: - __slots__ = ('seed', 'structs', 'globals', 'funcs', 'nid', 'off') + __slots__ = ('seed', 'structs', 'globals', 'funcs', 'nid', 'off', 'floats') def __init__(self, seed, off=frozenset()): self.seed = seed self.off = frozenset(off) # generator features left out; the renderer honours some self.structs, self.globals, self.funcs = [], [], [] self.nid = 1 + self.floats = False # uses F32: emit the float harness and checks @property def tests(self): @@ -257,6 +312,8 @@ def ty(e): return common(ta, ty(e.k[2])) if is_int(ta) else ta if op == 'call': return e.a.ret + if op == 'fclamp': + return F32 if op in ('asg', 'incdec'): return ty(e.k[0]) raise ValueError(op) @@ -661,6 +718,12 @@ def mk_const(v, t): return E('const', a=t.wrap(v), t=t) +def simple_const(v, t): + """A constant of type t worth v (the reducer's replacement value).""" + ct = t if t.bits >= 16 else (I16 if t.signed else U16) + return mk_const(v, ct) + + def wrap_unsigned(node): """Rewrite a signed bin/un node in place as the same operation carried out in the unsigned type of its width, converted back to the signed type.""" @@ -794,7 +857,49 @@ def repair(prog, trips): ''' +FLOAT_CHECK_PRELUDE = r''' +/* float programs: a result must be finite and, unless zero, of moderate + * magnitude, so neither compiler meets infinities, NaNs or subnormals; a + * conversion to an integer type must be in range (C leaves it undefined). */ +#include +static float ck_f(int id, float v) +{ + if (!isfinite(v) || (v != 0.0f && (fabsf(v) < 1e-18f || fabsf(v) > 1e18f))) ck_hit(id); + return v; +} +#define CK_F2I(name, type, lo, hi) \ + static type name(int id, float v) { if (!(v > lo && v < hi)) { ck_hit(id); return 0; } return (type)v; } +CK_F2I(ck_f2i8, int8_t, -129.0f, 128.0f) +CK_F2I(ck_f2u8, uint8_t, -1.0f, 256.0f) +CK_F2I(ck_f2i16, int16_t, -32769.0f, 32768.0f) +CK_F2I(ck_f2u16, uint16_t, -1.0f, 65536.0f) +CK_F2I(ck_f2i32, int32_t, -2147483904.0f, 2147483648.0f) +CK_F2I(ck_f2u32, uint32_t, -1.0f, 4294967296.0f) +static float ck_fpost(int id, float *p, float d) { float v = *p; *p = ck_f(id, v + d); return v; } +''' + +FLOAT_HARNESS = r''' +static float fclamp(float x, float b) { return x > b ? b : (x < -b ? -b : x); } +/* a float is shown by its IEEE bits; -0 is shown as 0 */ +static void showf(uint16_t id, float f) +{ + union { float f; uint32_t u; } u; + u.f = (f == 0.0f) ? 0.0f : f; + show(id, u.u); +} + +static void mixf(float f) +{ + union { float f; uint32_t u; } u; + u.f = (f == 0.0f) ? 0.0f : f; + mix(u.u); +} +''' + + def const_text(v, t): + if t is F32: + return float_text(v) if t is I16 or t.bits < 16: if t is not I16: return '((%s)%d)' % (t.cname, v) @@ -814,6 +919,9 @@ def const_text(v, t): def static_text(v, t): """A constant for a static initializer.""" + if t is F32: + # bare, not parenthesised: BUG-040 + return float_text(v).strip('()') if t.signed: if v == -(1 << 31): return '(-2147483647 - 1)' @@ -1160,6 +1268,8 @@ def leaf(text, t, dims, ind, var): for f in t.fields: leaf('%s.%s' % (text, f.name), f.t, f.dims, ind + ' ', 'j') out.append(ind + '}') + elif t is F32: + out.append('%smixf(%s);' % (ind, text)) else: out.append('%smix((uint32_t)%s);' % (ind, text)) @@ -1176,6 +1286,8 @@ def program(self, prog, title=''): if self.checked: out.append(CHECK_PRELUDE % {'nid': prog.nid}) out.append('#define ck_idx(v, n) ((v) < 0 || (v) >= (n) ? (abort(), 0) : (v))') + if prog.floats: + out.append(FLOAT_CHECK_PRELUDE) out.append('') for st in prog.structs: out.append('struct %s {' % st.name) @@ -1188,6 +1300,8 @@ def program(self, prog, title=''): for g in prog.globals: out.append('%s;' % self.decl(g)) out.append(HARNESS) + if prog.floats: + out.append(FLOAT_HARNESS) self.state(prog, out) out.append('') for f in prog.funcs: @@ -1214,6 +1328,8 @@ def program(self, prog, title=''): call = '%s(%s)' % (f.name, ', '.join(self.ex(a) for a in args)) if is_int(f.ret): out.append(' show(%d, (uint32_t)%s);' % (n, call)) + elif f.ret is F32: + out.append(' showf(%d, %s);' % (n, call)) else: out.append(' %s;' % call) out.append(' show(%d, state());' % (n + 0x4000)) diff --git a/tests/known-bugs/baselines/bug039.txt b/tests/known-bugs/baselines/bug039.txt new file mode 100644 index 000000000..affba8961 --- /dev/null +++ b/tests/known-bugs/baselines/bug039.txt @@ -0,0 +1,404 @@ +1/3 3eaaaaab +3/7.8 3ec4ec4e +1/1.85 3f0a60dd +10/3 40555555 +0/0 3f800000 +0/1 3eaaaaab +0/2 41200000 +0/3 40a00000 +0/4 3f06bca2 +0/5 3ea2f98c +0/6 3ebc5aba +0/7 3707e575 +0/8 45fe10a6 +0/9 be088889 +0/10 47c35000 +0/11 37800080 +0/12 38000100 +0/13 2edbe6ff +0/14 501502f9 +0/15 33800000 +0/16 40555555 +0/17 3fb6db6e +0/18 3c04b614 +0/19 3dccccda +1/0 40400000 +1/1 3f800000 +1/2 41f00000 +1/3 41700000 +1/4 3fca1af3 +1/5 3f747652 +1/6 3f8d440b +1/7 37cbd830 +1/8 46be8c7c +1/9 becccccd +1/10 48927c00 +1/11 384000c0 +1/12 38c00180 +1/13 2fa4ed3f +1/14 50df8475 +1/15 34400000 +1/16 41200000 +1/17 40892492 +1/18 3cc7111f +1/19 3e9999a3 +2/0 3dcccccd +2/1 3d088889 +2/2 3f800000 +2/3 3f000000 +2/4 3d579436 +2/5 3d02613d +2/6 3d16aefb +2/7 35596f22 +2/8 444b4085 +2/9 bc5a740e +2/10 461c4000 +2/11 35cccd9a +2/12 364cce67 +2/13 2d2febff +2/14 4e6e6b28 +2/15 31cccccd +2/16 3eaaaaaa +2/17 3e124925 +2/18 3a545687 +2/19 3c23d715 +3/0 3e4ccccd +3/1 3d888889 +3/2 40000000 +3/3 3f800000 +3/4 3dd79436 +3/5 3d82613d +3/6 3d96aefb +3/7 35d96f22 +3/8 44cb4085 +3/9 bcda740e +3/10 469c4000 +3/11 364ccd9a +3/12 36ccce67 +3/13 2dafebff +3/14 4eee6b28 +3/15 324ccccd +3/16 3f2aaaaa +3/17 3e924925 +3/18 3ad45687 +3/19 3ca3d715 +4/0 3ff33333 +4/1 3f222222 +4/2 41980000 +4/3 41180000 +4/4 3f800000 +4/5 3f1ad378 +4/6 3f32efca +4/7 378119fc +4/8 46715c9d +4/9 be81b4e8 +4/10 48398c00 +4/11 37f33426 +4/12 38733519 +4/13 2f50e83f +4/14 508d8fa0 +4/15 33f33333 +4/16 40caaaaa +4/17 402db6db +4/18 3c7c26c0 +4/19 3e428f68 +5/0 40490fd0 +5/1 3f860a8b +5/2 41fb53c4 +5/3 417b53c4 +5/4 3fd3a4db +5/5 3f800000 +5/6 3f93eede +5/7 37d5771c +5/8 46c78ac4 +5/9 bed67744 +5/10 489965e1 +5/11 38491099 +5/12 38c91162 +5/13 2facb5f3 +5/14 50ea1113 +5/15 34490fd0 +5/16 41278d2d +5/17 408f9d95 +5/18 3cd07651 +5/19 3ea0d97d +6/0 402df84d +6/1 3f67f5bc +6/2 41d97660 +6/3 41597660 +6/4 3fb72051 +6/5 3f5d816d +6/6 3f800000 +6/7 37b8b3bf +6/8 46aca7ad +6/9 beb99163 +6/10 4884ba80 +6/11 382df8fb +6/12 38adf9a9 +6/13 2f957064 +6/14 50ca8714 +6/15 342df84d +6/16 4110f995 +6/17 4078874a +6/18 3cb45f88 +6/19 3e8b2d13 +7/0 47f12000 +7/1 4720c000 +7/2 4996b400 +7/3 4916b400 +7/4 477dd0d8 +7/5 4719814c +7/6 473168f4 +7/7 3f800000 +7/8 4e6f4d6e +7/9 c680999a +7/10 5037f6ba +7/11 3ff120f1 +7/12 407121e2 +7/13 374f1ff3 +7/14 588c5a6d +7/15 3bf12000 +7/16 48c8efff +7/17 482c3b6e +7/18 447a0000 +7/19 4640e672 +8/0 3900f990 +8/1 382bf76b +8/2 3aa137f4 +8/3 3a2137f4 +8/4 3887c354 +8/5 3824374d +8/6 383dc9f6 +8/7 3088ee6a +8/8 3f800000 +8/9 b78992bc +8/10 4144ccce +8/11 3100fa11 +8/12 3180fa92 +8/13 285d93be +8/14 49962580 +8/15 2d00f990 +8/16 39d6f545 +8/17 39383ff2 +8/18 3585b8d4 +8/19 374e5c26 +9/0 c0f00000 +9/1 c0200000 +9/2 c2960000 +9/3 c2160000 +9/4 c07ca1af +9/5 c018c9f3 +9/6 c030950e +9/7 b87ece3c +9/8 c76e2f9b +9/9 3f800000 +9/10 c9371b00 +9/11 b8f000f0 +9/12 b97001e0 +9/13 b04e288f +9/14 d18bb2c9 +9/15 b4f00000 +9/16 c1c7ffff +9/17 c12b6db7 +9/18 bd78d566 +9/19 bf40000c +10/0 3727c5ac +10/1 365fb23b +10/2 38d1b717 +10/3 3851b717 +10/4 36b09a2e +10/5 36559d44 +10/6 3676e13f +10/7 2eb21f3d +10/8 3da6810a +10/9 b5b2f4fc +10/10 3f800000 +10/11 2f27c654 +10/12 2fa7c6fc +10/13 26901d7d +10/14 47c35000 +10/15 2b27c5ac +10/16 380bcf64 +10/17 376facad +10/18 33adf281 +10/19 358637c5 +11/0 477fff00 +11/1 46aaaa00 +11/2 491fff60 +11/3 489fff60 +11/4 4706bc1b +11/5 46a2f8e9 +11/6 46bc59fd +11/7 3f07e4ed +11/8 4dfe0fa8 +11/9 c6088800 +11/10 4fc34f3d +11/11 3f800000 +11/12 40000080 +11/13 36dbe623 +11/14 58150264 +11/15 3b7fff00 +11/16 4855547f +11/17 47b6dab7 +11/18 4404b590 +11/19 45cccc0d +12/0 46fffe00 +12/1 462aa955 +12/2 489ffec0 +12/3 481ffec0 +12/4 4686bb94 +12/5 4622f846 +12/6 463c5941 +12/7 3e87e465 +12/8 4d7e0eaa +12/9 c5888777 +12/10 4f434e7a +12/11 3effff00 +12/12 3f800000 +12/13 365be547 +12/14 579501cf +12/15 3afffe00 +12/16 47d553aa +12/17 4736da00 +12/18 4384b50b +12/19 454ccb40 +13/0 501502f9 +13/1 4f46aea1 +13/2 51ba43b7 +13/3 513a43b7 +13/4 4f9cdab5 +13/5 4f3dba46 +13/6 4f5b45fc +13/7 479e3442 +13/8 5693e2a4 +13/9 ce9ef21b +13/10 58635faa +13/11 4815038e +13/12 48950423 +13/13 3f800000 +13/14 60ad78ec +13/15 441502f9 +13/16 50f85a49 +13/17 5054dfad +13/18 4c9a7f09 +13/19 4e6e6b37 +14/0 2edbe6ff +14/1 2e1299ff +14/2 3089705f +14/3 3009705f +14/4 2e6779e4 +14/5 2e0bfe89 +14/6 2e21cb92 +14/7 266977d6 +14/8 355a3d7e +14/9 ad6a8fff +14/10 3727c5ad +14/11 26dbe7db +14/12 275be8b7 +14/13 1e3ce509 +14/14 3f800000 +14/15 22dbe6ff +14/16 2fb7407f +14/17 2f1d12b6 +14/18 2b63ff06 +14/19 2d2fec0a +15/0 4b800000 +15/1 4aaaaaab +15/2 4d200000 +15/3 4ca00000 +15/4 4b06bca2 +15/5 4aa2f98c +15/6 4abc5aba +15/7 4307e575 +15/8 51fe10a6 +15/9 ca088889 +15/10 53c35000 +15/11 43800080 +15/12 44000100 +15/13 3adbe6ff +15/14 5c1502f9 +15/15 3f800000 +15/16 4c555555 +15/17 4bb6db6e +15/18 4804b614 +15/19 49ccccda +16/0 3e99999a +16/1 3dcccccd +16/2 40400000 +16/3 3fc00000 +16/4 3e21af29 +16/5 3dc391dc +16/6 3de20679 +16/7 3623135a +16/8 45187064 +16/9 bd23d70b +16/10 46ea6001 +16/11 36999a34 +16/12 37199acd +16/13 2e03f100 +16/14 4f32d05e +16/15 3299999a +16/16 3f800000 +16/17 3edb6db8 +16/18 3b1f40e6 +16/19 3cf5c29f +17/0 3f333333 +17/1 3e6eeeef +17/2 40e00000 +17/3 40600000 +17/4 3ebca1af +17/5 3e642a2a +17/6 3e83d91b +17/7 36be413d +17/8 45b1d874 +17/9 bdbf258c +17/10 4788b800 +17/11 373333e6 +17/12 37b33499 +17/13 2e99ee7f +17/14 4fd09dc3 +17/15 33333333 +17/16 40155555 +17/17 3f800000 +17/18 3bb9cbb6 +17/19 3d8f5c32 +18/0 42f6e979 +18/1 42249ba6 +18/2 449a51eb +18/3 441a51eb +18/4 4281f425 +18/5 421d306f +18/6 4235aaf6 +18/7 3a83126f +18/8 49750bb5 +18/9 c183afb8 +18/10 4b3c6100 +18/11 3af6ea70 +18/12 3b76eb67 +18/13 32541886 +18/14 538fb8c1 +18/15 36f6e979 +18/16 43cdc28f +18/17 43305da0 +18/18 3f800000 +18/19 414587a0 +19/0 411ffff6 +19/1 40555548 +19/2 42c7fff3 +19/3 4247fff3 +19/4 40a86bc0 +19/5 404bb7e2 +19/6 406b7159 +19/7 38a9dec8 +19/8 479eca5e +19/9 bfaaaaa0 +19/10 497423f1 +19/11 39200096 +19/12 39a00136 +19/13 30897057 +19/14 51ba43ab +19/15 351ffff6 +19/16 4205554d +19/17 4164923b +19/18 3da5e38f +19/19 3f800000 diff --git a/tests/known-bugs/baselines/bug040.txt b/tests/known-bugs/baselines/bug040.txt new file mode 100644 index 000000000..40007fc6e --- /dev/null +++ b/tests/known-bugs/baselines/bug040.txt @@ -0,0 +1,3 @@ +25 -25 3 15 +50 -25 25 1 +25 -25 3 diff --git a/tests/known-bugs/baselines/bug041.txt b/tests/known-bugs/baselines/bug041.txt new file mode 100644 index 000000000..7e10fe499 --- /dev/null +++ b/tests/known-bugs/baselines/bug041.txt @@ -0,0 +1,10 @@ +2 3 +5 4 +7 3 +1 2 +1 3 +2 2 +4 -1 +9 5 +11 3 +4 diff --git a/tests/known-bugs/baselines/bug042.txt b/tests/known-bugs/baselines/bug042.txt new file mode 100644 index 000000000..3491824f0 --- /dev/null +++ b/tests/known-bugs/baselines/bug042.txt @@ -0,0 +1,10 @@ +25 +-3 +0 +0 +1 +7 +7 +9 +10 +-5 0 diff --git a/tests/known-bugs/baselines/bug043.txt b/tests/known-bugs/baselines/bug043.txt new file mode 100644 index 000000000..d17e4dd18 --- /dev/null +++ b/tests/known-bugs/baselines/bug043.txt @@ -0,0 +1,3 @@ +12 5 4 22 4 17 +4 +10 25 diff --git a/tests/known-bugs/baselines/bug044.txt b/tests/known-bugs/baselines/bug044.txt new file mode 100644 index 000000000..ca4146ef5 --- /dev/null +++ b/tests/known-bugs/baselines/bug044.txt @@ -0,0 +1,12 @@ +local ++ 40000000 +local -- 3f800000 +global ++ 40000000 +member ++ 40000000 +deref ++ 40000000 +element ++ 40400000 +element -- 40000000 +pointer index ++ c01f6c8b +local element ++ 40600000 +local element ++ 3f800000 ++= (correct) 40000000 ++ 1 (correct) 40400000 diff --git a/tests/known-bugs/baselines/bug045.txt b/tests/known-bugs/baselines/bug045.txt new file mode 100644 index 000000000..ac4bf63a9 --- /dev/null +++ b/tests/known-bugs/baselines/bug045.txt @@ -0,0 +1,2 @@ +255 +1 1 1 diff --git a/tests/known-bugs/baselines/bug046.txt b/tests/known-bugs/baselines/bug046.txt new file mode 100644 index 000000000..02424b4ce --- /dev/null +++ b/tests/known-bugs/baselines/bug046.txt @@ -0,0 +1,3 @@ +100000 8 100000 +2 6 100000 +100000 100000 9 4 diff --git a/tests/known-bugs/baselines/bug047.txt b/tests/known-bugs/baselines/bug047.txt new file mode 100644 index 000000000..c1d2a6801 --- /dev/null +++ b/tests/known-bugs/baselines/bug047.txt @@ -0,0 +1,14 @@ +40e00000 +40e00000 +40e00000 +40e00000 +43010000 +41000000 +40e00000 +40000000 +41200000 +41e00000 +41200000 +41a00000 +41200000 +c0a00000 40e00000 40e00000 40e00000 diff --git a/tests/known-bugs/baselines/bug048.txt b/tests/known-bugs/baselines/bug048.txt new file mode 100644 index 000000000..906f42236 --- /dev/null +++ b/tests/known-bugs/baselines/bug048.txt @@ -0,0 +1,6 @@ +4f800000 +4f800000 +4f800000 +4f800000 +4f800000 +4f800000 4f800000 477fb800 diff --git a/tests/known-bugs/baselines/bug049.txt b/tests/known-bugs/baselines/bug049.txt new file mode 100644 index 000000000..56bbe7593 --- /dev/null +++ b/tests/known-bugs/baselines/bug049.txt @@ -0,0 +1,5 @@ +1 +1 +1 +yes +1 0 0 0 0 diff --git a/tests/known-bugs/baselines/bug050.txt b/tests/known-bugs/baselines/bug050.txt new file mode 100644 index 000000000..41064f840 --- /dev/null +++ b/tests/known-bugs/baselines/bug050.txt @@ -0,0 +1,4 @@ +c2f20000 +c2f20000 +c2f00000 +c2f20000 c2f20000 4f800000 diff --git a/tests/known-bugs/baselines/bug051.txt b/tests/known-bugs/baselines/bug051.txt new file mode 100644 index 000000000..d9e167991 --- /dev/null +++ b/tests/known-bugs/baselines/bug051.txt @@ -0,0 +1 @@ +10 12 44 diff --git a/tests/known-bugs/baselines/bug052.txt b/tests/known-bugs/baselines/bug052.txt new file mode 100644 index 000000000..83eaa2a9f --- /dev/null +++ b/tests/known-bugs/baselines/bug052.txt @@ -0,0 +1,9 @@ +f1 bfaccccd +f2 bfaccccd +f3 bfaccccd +f4 4 +f5 4 +ok1 bfaccccd +ok2 bfaccccd +ok3 bfaccccd +ok3 beb33333 diff --git a/tests/known-bugs/baselines/bug053.txt b/tests/known-bugs/baselines/bug053.txt new file mode 100644 index 000000000..3c93d3869 --- /dev/null +++ b/tests/known-bugs/baselines/bug053.txt @@ -0,0 +1,6 @@ +block_loop 3f800000 +float_test 3f800000 +plain_block 3f800000 +no_loop 3f800000 +read_param 3f800000 +store_thru 3f800000 diff --git a/tests/known-bugs/bug039.c b/tests/known-bugs/bug039.c new file mode 100644 index 000000000..c111eb725 --- /dev/null +++ b/tests/known-bugs/bug039.c @@ -0,0 +1,47 @@ +/* BUG-039: float division is off by one unit in the last place in about one + * fifth of cases, in both directions. Addition, subtraction, multiplication + * and every conversion are bit-exact. The operands are volatile so that the + * host compiler cannot fold the division; dcc never folds a float division + * and calls the runtime's __fdiv for every quotient. */ +#include +#include + +typedef union { float f; uint32_t u; } bits; + +static const float vals[] = { + 1.0f, 3.0f, 0.1f, 0.2f, 1.9f, 3.14159f, 2.71828f, 123456.0f, 0.000123f, + -7.5f, 1e-5f, 65535.0f, 32767.0f, 1e10f, 1e-10f, 16777216.0f, 0.3f, 0.7f, + 123.456f, 9.99999f +}; +#define N (sizeof(vals) / sizeof(vals[0])) + +volatile float va, vb; + +static void show(const char *tag, float f) +{ + bits b; + b.f = f; + printf("%s %08lx\n", tag, (unsigned long)b.u); +} + +int main(void) +{ + unsigned i, j; + char tag[16]; + + /* well-known cases: want 3eaaaaab 3ec4ec4e 3f0a60dd 40555555 */ + va = 1.0f; vb = 3.0f; show("1/3", va / vb); + va = 3.0f; vb = 7.8f; show("3/7.8", va / vb); + va = 1.0f; vb = 1.85f; show("1/1.85", va / vb); + va = 10.0f; vb = 3.0f; show("10/3", va / vb); + + for (i = 0; i < N; i++) { + for (j = 0; j < N; j++) { + va = vals[i]; + vb = vals[j]; + sprintf(tag, "%u/%u", i, j); + show(tag, va / vb); + } + } + return 0; +} diff --git a/tests/known-bugs/bug040.c b/tests/known-bugs/bug040.c new file mode 100644 index 000000000..a01ff6b78 --- /dev/null +++ b/tests/known-bugs/bug040.c @@ -0,0 +1,24 @@ +/* BUG-040: the initializer of a float with static storage must be a bare + * floating constant, optionally negated, or `(float)` of an integer. A + * parenthesised constant (`(2.5f)`), a negated parenthesised constant and a + * constant expression (`1.0f + 2.0f`) are rejected with DCC-E1102 "expected + * ';'" (file scope) or DCC-E0912 "float initializer must be constant" (an + * array element). Integer initializers and automatic float initializers + * accept all of these. */ +#include + +static float a = (2.5f); +static float b = -(2.5f); +static float c = 1.0f + 2.0f; +static float d = 3 * 0.5f; +static float arr[3] = { (0.5f), -(0.25f), 1.0f - 0.75f }; +static int ok = (-2) + 1 * 3; + +int main(void) +{ + float la = (2.5f), lb = -(2.5f), lc = 1.0f + 2.0f; + printf("%d %d %d %d\n", (int)(a * 10.0f), (int)(b * 10.0f), (int)c, (int)(d * 10.0f)); + printf("%d %d %d %d\n", (int)(arr[0] * 100.0f), (int)(arr[1] * 100.0f), (int)(arr[2] * 100.0f), ok); + printf("%d %d %d\n", (int)(la * 10.0f), (int)(lb * 10.0f), (int)lc); + return 0; +} diff --git a/tests/known-bugs/bug041.c b/tests/known-bugs/bug041.c new file mode 100644 index 000000000..2c6981b85 --- /dev/null +++ b/tests/known-bugs/bug041.c @@ -0,0 +1,39 @@ +/* BUG-041: the value of a float `++` or `--` expression is rejected with + * DCC-E1002 "unsupported expression statement" / "unsupported if condition + * or branch" / "unsupported while condition or body" wherever it is used: + * assigned, compared, tested for truth, in `?:`, on a global, an array + * element or a local. `f++;` as a statement and the value of `f += 1.0f` + * are accepted, and so is the value of an integer `++`. */ +#include + +static float f, g, fa[4]; +static int n; + +int main(void) +{ + float l = 2.0f; + + f = 2.0f; + g = f++; /* 2, f 3 */ + printf("%d %d\n", (int)g, (int)f); + g = ++f + 1.0f; /* 5, f 4 */ + printf("%d %d\n", (int)g, (int)f); + if (f-- > 3.0f) g = 7.0f; /* taken, f 3 */ + printf("%d %d\n", (int)g, (int)f); + if (--f) n = 1; /* 2 is true */ + printf("%d %d\n", n, (int)f); + n = f++ > 1.0f; /* 1, f 3 */ + printf("%d %d\n", n, (int)f); + g = (f-- > 10.0f) ? 1.0f : 2.0f; /* 2, f 2 */ + printf("%d %d\n", (int)g, (int)f); + while (f-- > 0.0f) g += 1.0f; /* two passes: g 4, f -1 */ + printf("%d %d\n", (int)g, (int)f); + fa[1] = 1.5f; + if (fa[1]++ > 1.0f) g = 9.0f; /* taken, fa[1] 2.5 */ + printf("%d %d\n", (int)g, (int)(fa[1] * 2.0f)); + if (l++ > 1.0f) g = 11.0f; /* taken, l 3 */ + printf("%d %d\n", (int)g, (int)l); + g = (l += 1.0f); /* accepted: 4 */ + printf("%d\n", (int)g); + return 0; +} diff --git a/tests/known-bugs/bug042.c b/tests/known-bugs/bug042.c new file mode 100644 index 000000000..75a3cf759 --- /dev/null +++ b/tests/known-bugs/bug042.c @@ -0,0 +1,37 @@ +/* BUG-042: a floating constant that has to be folded or tested for truth + * is rejected with DCC-E1002: the operand of unary `-` when it is itself a + * negated or computed constant, the operand of `!`, `&&` or `||`, and the + * condition of `if` or `while`. `-(2.5f)`, `2.5f ? a : b` and the same + * shapes with a variable or with integer constants are accepted. */ +#include + +static float f, g; +static int n; + +int main(void) +{ + f = 2.0f; + g = -(-2.5f); /* 2.5 */ + printf("%d\n", (int)(g * 10.0f)); + g = -(1.0f + 2.0f); /* -3 */ + printf("%d\n", (int)g); + n = !255.0f; /* 0 */ + printf("%d\n", n); + n = !(1.0f + 2.0f); /* 0 */ + printf("%d\n", n); + n = 2.5f && 1; /* 1 */ + printf("%d\n", n); + if (2.3f) n = 7; /* taken */ + printf("%d\n", n); + if (0.0f) n = 8; /* not taken */ + printf("%d\n", n); + if (1.0f + 2.0f) n = 9; /* taken */ + printf("%d\n", n); + while (1.0f) { n++; break; } /* 10 */ + printf("%d\n", n); + /* accepted */ + g = -(2.5f) + (f ? 1.0f : 0.0f) + (2.5f ? 1.0f : 0.0f); + if (f) n = !f; + printf("%d %d\n", (int)(g * 10.0f), n); + return 0; +} diff --git a/tests/known-bugs/bug043.c b/tests/known-bugs/bug043.c new file mode 100644 index 000000000..91871d819 --- /dev/null +++ b/tests/known-bugs/bug043.c @@ -0,0 +1,36 @@ +/* BUG-043: a compound assignment to an integer lvalue whose right operand + * is a float is rejected with DCC-E1002 for every integer width and + * operator (`l *= f`, `n += f`, `u -= f`, `c8 += f`, `l -= 1.5f`). The + * expanded form `l = l * f` and a float lvalue with an integer operand + * (`f *= l`) are accepted. */ +#include +#include + +static float f; +static int n; +static int32_t l; +static uint32_t u; +static uint16_t us; +static int8_t c8; + +int main(void) +{ + int16_t s = 7; + f = 2.5f; + n = 3; l = 5; u = 7; us = 9; c8 = 2; + l *= f; /* 12 */ + n += f; /* 5 */ + u -= f; /* 4 */ + us *= f; /* 22 */ + c8 += f; /* 4 */ + s *= f; /* 17 */ + printf("%ld %d %lu %u %d %d\n", (long)l, n, (unsigned long)u, us, c8, s); + l -= 1.5f; /* 10 */ + l /= f; /* 4 */ + printf("%ld\n", (long)l); + /* accepted */ + l = l * f; /* 10 */ + f *= l; /* 25 */ + printf("%ld %d\n", (long)l, (int)f); + return 0; +} diff --git a/tests/known-bugs/bug044.c b/tests/known-bugs/bug044.c new file mode 100644 index 000000000..2247e514f --- /dev/null +++ b/tests/known-bugs/bug044.c @@ -0,0 +1,44 @@ +/* BUG-044: `++` and `--` on a float do the wrong thing with no diagnostic. + * On a scalar (local, global, struct member, `*p`) the statement is a + * no-op: the value is unchanged. On an array element (`fa[1]++`, `p[1]++`, + * constant or variable index) the element is treated as a 16-bit integer: + * its low word is incremented and the high word cleared, so 2.0f becomes + * the denormal 0x00000001. `f += 1.0f` and `f = f + 1` are correct. */ +#include +#include + +static float fg = 1.0f; +static float fa[4] = { 1.0f, 2.0f, 3.0f, -3.491f }; +static float *p = fa; +static struct S { float m; } s = { 1.0f }; + +static void show(const char *tag, float f) +{ + union { float f; uint32_t u; } u; + u.f = f; + printf("%s %08lx\n", tag, (unsigned long)u.u); +} + +int main(void) +{ + float l = 1.0f; + int i = 2; + + l++; show("local ++", l); /* 40000000 */ + --l; show("local --", l); /* 3f800000 */ + fg++; show("global ++", fg); /* 40000000 */ + s.m++; show("member ++", s.m); /* 40000000 */ + (*p)++; show("deref ++", *p); /* 40000000 */ + fa[1]++; show("element ++", fa[1]); /* 40400000 */ + fa[i]--; show("element --", fa[i]); /* 40000000 */ + p[3]++; show("pointer index ++", p[3]); /* c01f6c8b */ + { + float la[2] = { 2.5f, 0.0f }; + la[0]++; show("local element ++", la[0]); /* 40600000 */ + la[1]++; show("local element ++", la[1]); /* 3f800000 */ + } + l = 1.0f; + l += 1.0f; show("+= (correct)", l); /* 40000000 */ + l = l + 1; show("+ 1 (correct)", l); /* 40400000 */ + return 0; +} diff --git a/tests/known-bugs/bug045.c b/tests/known-bugs/bug045.c new file mode 100644 index 000000000..529db9c24 --- /dev/null +++ b/tests/known-bugs/bug045.c @@ -0,0 +1,35 @@ +/* BUG-045: a comparison whose right operand is a negative float constant + * combined with a second operand (`x < -1.0f - n`, `x == -1.0f * n`) stops + * the compile with `dcc: fatal: MIR emission is required`. The same + * expression assigned to a float first, or written with the constant on the + * right (`x < n - 1.0f`), or with a positive constant, compiles. */ +#include +#include + +static float fg = -5.0f; +static int n = 3; +static int32_t l = 3; + +static int f(float x) +{ + int r = 0; + if (x < -1.0f - n) r |= 1; /* -5 < -4: taken */ + if (x < -1.0f + n) r |= 2; /* -5 < 2: taken */ + if (x == -1.0f * 5) r |= 4; /* taken */ + if (x < -1.0f - l) r |= 8; /* taken */ + if (x < -1.0f - 2) r |= 16; /* taken */ + if (x < -1.0f - fg) r |= 32; /* -5 < 4: taken */ + while (x < -1.0f / n) { r |= 64; break; } + r |= (x < -0.0f - n) ? 128 : 0; + return r; +} + +int main(void) +{ + float t; + printf("%d\n", f(fg)); + /* accepted forms */ + t = -1.0f - n; + printf("%d %d %d\n", fg < t, fg < n - 1.0f, fg < 1.0f - n); + return 0; +} diff --git a/tests/known-bugs/bug046.c b/tests/known-bugs/bug046.c new file mode 100644 index 000000000..f0904f586 --- /dev/null +++ b/tests/known-bugs/bug046.c @@ -0,0 +1,30 @@ +/* BUG-046: `return` of a 32-bit integer expression from a function that + * returns `float`, or of a float expression from a function that returns a + * 32-bit integer, is rejected with DCC-E1002 "unsupported return + * expression". The implicit conversion works through an assignment or an + * explicit cast, and 8/16-bit integers convert implicitly. */ +#include +#include + +static int32_t gl = 100000; + +static float a(int32_t x) { return x; } +static float b(uint32_t x) { return x + 1; } +static float c(void) { return gl; } +static int32_t d(float x) { return x; } +static uint32_t e(float x) { return x * 2.0f; } +static int32_t ff(void) { return gl; } +static float g(void) { return ff(); } +/* accepted */ +static float h(int32_t x) { float r = x; return r; } +static float i(int32_t x) { return (float)x; } +static float j(int16_t x) { return x; } +static int16_t k(float x) { return x; } + +int main(void) +{ + printf("%ld %ld %ld\n", (long)a(gl), (long)b(7), (long)c()); + printf("%ld %lu %ld\n", (long)d(2.5f), (unsigned long)e(3.0f), (long)g()); + printf("%ld %ld %d %d\n", (long)h(gl), (long)i(gl), (int)j(9), k(4.75f)); + return 0; +} diff --git a/tests/known-bugs/bug047.c b/tests/known-bugs/bug047.c new file mode 100644 index 000000000..3989599e1 --- /dev/null +++ b/tests/known-bugs/bug047.c @@ -0,0 +1,54 @@ +/* BUG-047: `f + (x * y)`, a float plus a product, drops the product (the + * result is just `f`) when the left factor x is an integer, or when the + * right factor y is a local or parameter and x is a global, array element, + * struct member or sub-expression. `-` and `*` in place of the outer `+`, + * the product on the left (`(x * y) + f`), or a local/param/constant left + * factor with any right factor are fine. Compound `f += x * y` is affected + * too. */ +#include +#include + +static float fg = 1.0f, fv = 3.0f; +static int32_t gl = 2; +static uint16_t gu = 2; + +static float fa[2] = {0.0f, 3.0f}; + +static float t(float p) { float l1 = 3.0f; return fg + (fv * p) + (fg + (fa[1] * l1)); } /* 20 */ + +static unsigned long bits(float f) +{ + union { float f; uint32_t u; } u; + u.f = f; + return u.u; +} + +int main(void) +{ + int i = 2; + float r; + printf("%08lx\n", bits(fg + (2 * 3.0f))); /* 7: 40e00000 */ + printf("%08lx\n", bits(fg + (i * 3.0f))); /* 7 */ + printf("%08lx\n", bits(fg + (gl * fv))); /* 7 */ + printf("%08lx\n", bits(fg + gu * fv)); /* 7 */ + printf("%08lx\n", bits(1.0f + (128 * 1.0f))); /* 129: 43010000 */ + printf("%08lx\n", bits(fg + (2 * 3.0f) + 1.0f)); /* 8 */ + r = fg; + r += 2 * 3.0f; /* 7 */ + printf("%08lx\n", bits(r)); + r = fg; + r += (int32_t)1 * 1.0f; /* 2 */ + printf("%08lx\n", bits(r)); + { + float l1 = 3.0f; + printf("%08lx\n", bits(fg + (fv * l1))); /* 10: 41200000 */ + printf("%08lx\n", bits(fg + (fv * l1 * l1))); /* 28: 41e00000 */ + printf("%08lx\n", bits(fg + ((fv + 0.0f) * l1))); /* 10, dcc gives -inf */ + printf("%08lx\n", bits(t(3.0f))); /* 20: 41a00000 */ + printf("%08lx\n", bits(fg + (l1 * fv))); /* 10 (right) */ + } + /* accepted forms */ + printf("%08lx %08lx %08lx %08lx\n", bits(fg - (2 * 3.0f)), bits((2 * 3.0f) + fg), + bits(fg + fv * i), bits(fg + (float)i * 3.0f)); /* c0a00000 40e00000 40e00000 40e00000 */ + return 0; +} diff --git a/tests/known-bugs/bug048.c b/tests/known-bugs/bug048.c new file mode 100644 index 000000000..b7e8034fb --- /dev/null +++ b/tests/known-bugs/bug048.c @@ -0,0 +1,36 @@ +/* BUG-048: converting `(uint32_t)x` to float, where x is a signed 32-bit + * expression, converts the signed value instead: (float)(uint32_t)-72 is + * -72.0f rather than 4294967224.0f. Storing the cast in a uint32_t variable + * first, or casting a signed 8- or 16-bit value, converts correctly. */ +#include +#include + +static int32_t g2 = -72; +static int16_t gs = -72; +static float m; + +static unsigned long bits(float f) +{ + union { float f; uint32_t u; } u; + u.f = f; + return u.u; +} + +int main(void) +{ + uint32_t u; + m = (uint32_t)g2; /* 4294967224: 4f800000 */ + printf("%08lx\n", bits(m)); + m = (float)(uint32_t)g2; + printf("%08lx\n", bits(m)); + m = (uint32_t)(g2 + 1); + printf("%08lx\n", bits(m)); + m = (uint32_t)g2 * 1.0f; + printf("%08lx\n", bits(m)); + printf("%08lx\n", bits((uint32_t)(int32_t)gs)); + /* accepted forms */ + u = (uint32_t)g2; + m = u; + printf("%08lx %08lx %08lx\n", bits(m), bits((uint32_t)gs), bits((uint16_t)gs)); /* 4f800000 4f800000 477fb800 */ + return 0; +} diff --git a/tests/known-bugs/bug049.c b/tests/known-bugs/bug049.c new file mode 100644 index 000000000..2151354df --- /dev/null +++ b/tests/known-bugs/bug049.c @@ -0,0 +1,22 @@ +/* BUG-049: `!f` is 0 when the float f is negative zero; it should be 1. + * The truth test in `if (f)`, `f ? :`, `&&`, `||` and the comparison + * `f == 0.0f` all treat -0.0f as zero correctly; only `!` looks at the + * raw bits. -0.0f arises from `-(float)0`, `0.0f * -1.0f` and similar. */ +#include +#include + +static float m0, one = 1.0f, zero = 0.0f, minus = -1.0f; +static uint8_t g5 = 0; + +int main(void) +{ + m0 = -((float)g5); + printf("%d\n", !m0); /* 1 */ + m0 = -0.0f; + printf("%d\n", !m0); /* 1 */ + printf("%d\n", !(zero * minus)); /* 1 */ + if (!m0) printf("yes\n"); else printf("no\n"); /* yes */ + /* correct */ + printf("%d %d %d %d %d\n", m0 == 0.0f, m0 ? 1 : 0, m0 && 1, m0 || 0, !one); /* 1 0 0 0 0 */ + return 0; +} diff --git a/tests/known-bugs/bug050.c b/tests/known-bugs/bug050.c new file mode 100644 index 000000000..f51b506c9 --- /dev/null +++ b/tests/known-bugs/bug050.c @@ -0,0 +1,32 @@ +/* BUG-050: `(int32_t)(uint32_t)x`, with x a signed 8- or 16-bit value, + * converted to float gives the unsigned value: -121 becomes + * 4294967175.0f instead of -121.0f. With a 32-bit x, or without the + * inner cast, the conversion is right. Mirror image of BUG-048. */ +#include +#include + +static int8_t g4 = -121; +static int16_t g5 = -121; +static int32_t g6 = -121; +static float fg; + +static unsigned long bits(float f) +{ + union { float f; uint32_t u; } u; + u.f = f; + return u.u; +} + +int main(void) +{ + fg = (int32_t)((uint32_t)g4); + printf("%08lx\n", bits(fg)); /* -121: c2f20000 */ + fg = (int32_t)((uint32_t)g5); + printf("%08lx\n", bits(fg)); /* c2f20000 */ + fg = (int32_t)((uint32_t)g4 + 1); + printf("%08lx\n", bits(fg)); /* -120: c2f00000 */ + /* right */ + fg = (int32_t)((uint32_t)g6); + printf("%08lx %08lx %08lx\n", bits(fg), bits((int32_t)g4), bits((uint32_t)g4)); /* c2f20000 c2f20000 4f800000 */ + return 0; +} diff --git a/tests/known-bugs/bug051.c b/tests/known-bugs/bug051.c new file mode 100644 index 000000000..77f2bf4cd --- /dev/null +++ b/tests/known-bugs/bug051.c @@ -0,0 +1,30 @@ +/* BUG-051: a function-like macro parameter whose name is the suffix of a + * numeric constant in the replacement list is substituted inside that + * constant. `#define T(f) g = 9.0f; f(1)` expands to `g = 9.0a1; a1(1)` + * and `#define U(u) n = 10u; u` to `n = 10n = 2;; …`. The suffix letters + * are lexed as a separate identifier instead of being part of the + * pp-number, so `f`, `u`, `l`, `L`, `U`, `F` and their combinations are + * all affected. A suffix that is not a parameter (`1e5` with parameter + * `e`, `0x10` with parameter `x`) is fine. */ +#include + +static float g; +static unsigned n; +static long m; + +static void a1(int x) { g += (float)x; } + +#define T(f) g = 9.0f; f(1) +#define U(u) n = 10u; u +#define L(L) m = 10L; L +#define UL(ul) m = (long)40ul; ul + +int main(void) +{ + T(a1); + U(n += 2;) + L(m += 3;) + UL(m += 4;) + printf("%d %u %ld\n", (int)g, n, m); + return 0; +} diff --git a/tests/known-bugs/bug052.c b/tests/known-bugs/bug052.c new file mode 100644 index 000000000..01f09f657 --- /dev/null +++ b/tests/known-bugs/bug052.c @@ -0,0 +1,46 @@ +/* BUG-052: in a loop, `a[1] = (0 ? K : x) - 1.0f;` with `x` a 32-bit + * parameter that is assigned (and not read) after the loop is accumulated + * into the parameter: each iteration subtracts 1 again, so three + * iterations store x - 3. The constant condition is folded and the + * parameter is then treated as the expression's own temporary. In a `for` + * loop the low word of the float is also overwritten by the counter + * (`c0560002` for -0.35f - 3). A `float` or `long` parameter is wrong; an + * `int` one, a local copy of the parameter, `(x + 0.0f) - 1.0f` without + * the `?:`, an element that varies with the counter (`a[i]`), a plain + * global target, and reading `x` after the loop are all right. */ +#include +#include + +static float fa[4]; +static long la[4]; +static int ia[4]; +static float g; + +static void pf(const char *n, float f) +{ + union { float f; uint32_t u; } u; + u.f = f; + printf("%s %08lx\n", n, (unsigned long)u.u); +} + +static void f1(float x) { int i; for (i = 0; i < 3; ++i) fa[1] = (0 ? 2.0f : x) - 1.0f; x = -1.0f; } +static void f2(float x) { int i; for (i = 0; i < 3; ++i) fa[1] = (1 ? x : 2.0f) - 1.0f; x = -1.0f; } +static void f3(float x) { int i; i = 0; while (i < 3) { fa[1] = (0 ? 2.0f : x) - 1.0f; i++; } x = g; } +static void f4(long x) { int i; for (i = 0; i < 3; ++i) la[1] = (0 ? 2L : x) - 1L; x = -1L; } +static void f5(int x) { int i; for (i = 0; i < 3; ++i) ia[1] = (0 ? 2 : x) - 1; x = -1; } +static void ok1(float x) { int i; for (i = 0; i < 3; ++i) fa[1] = (x + 0.0f) - 1.0f; x = -1.0f; } +static void ok2(float x) { float y = x; int i; for (i = 0; i < 3; ++i) fa[1] = (0 ? 2.0f : y) - 1.0f; y = -1.0f; } +static void ok3(float x) { int i; for (i = 0; i < 3; ++i) fa[1] = (0 ? 2.0f : x) - 1.0f; fa[2] = x; } + +int main(void) +{ + f1(-0.35f); pf("f1", fa[1]); + f2(-0.35f); pf("f2", fa[1]); + f3(-0.35f); pf("f3", fa[1]); + f4(5L); printf("f4 %ld\n", la[1]); + f5(5); printf("f5 %d\n", ia[1]); + ok1(-0.35f); pf("ok1", fa[1]); + ok2(-0.35f); pf("ok2", fa[1]); + ok3(-0.35f); pf("ok3", fa[1]); pf("ok3", fa[2]); + return 0; +} diff --git a/tests/known-bugs/bug053.c b/tests/known-bugs/bug053.c new file mode 100644 index 000000000..77d272a8f --- /dev/null +++ b/tests/known-bugs/bug053.c @@ -0,0 +1,96 @@ +/* BUG-053: with dccpeep, a 32-bit parameter that is assigned after an empty + * counted loop and then read through a pointer that holds its address returns + * the old parameter value. dccpeep's pass_elim_dead_ix_stores remembers that + * the parameter's address escaped, but it treats every label that is not + * `L:` as a new function and forgets the escape; the loop's signed + * compare is rewritten by fold_signed_cmp_via_bytes with an `LS:` label, + * so the stores of `x0 = 1.0f` to (ix+4..7) are deleted as dead at `ret`. + * dcc without dccpeep (dcc-peep=false) is correct. */ +#include +#include + +static float g = 0.0f; + +/* the loop in a nested block before the assignment is what matters */ +static float with_block_loop(float x0) +{ + float *p = &x0; + if ((int32_t)12) { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + x0 = 1.0f; + return *p; +} + +static float with_float_test(float x0) +{ + float *p = &x0; + if (g == 0.0f) { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + x0 = 1.0f; + return *p; +} + +static float with_plain_block(float x0) +{ + float *p = &x0; + { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + x0 = 1.0f; + return *p; +} + +/* no loop: right */ +static float no_loop(float x0) +{ + float *p = &x0; + x0 = 1.0f; + return *p; +} + +/* read the parameter itself: right */ +static float read_param(float x0) +{ + float *p = &x0; + { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + x0 = 1.0f; + return x0 + (*p - *p); +} + +/* store through the pointer and read the parameter: right */ +static float store_through(float x0) +{ + float *p = &x0; + { + int16_t i; + for (i = 0; i < 2; ++i) {} + } + *p = 1.0f; + return x0; +} + +static void show(const char *name, float v) +{ + union { float f; uint32_t u; } c; + c.f = v; + printf("%s %08lx\n", name, (unsigned long)c.u); +} + +int main(void) +{ + show("block_loop ", with_block_loop(0.3f)); /* want 3f800000 */ + show("float_test ", with_float_test(0.3f)); /* want 3f800000 */ + show("plain_block", with_plain_block(0.3f)); /* want 3f800000 */ + show("no_loop ", no_loop(0.3f)); + show("read_param ", read_param(0.3f)); + show("store_thru ", store_through(0.3f)); + return 0; +} From 382712d2904eeeab2465af4bf0e59b49cc40a355 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Sat, 14 Nov 2026 02:34:50 +1100 Subject: [PATCH 12/14] Record BUG-015 and BUG-052 float facets from seeds 4294 and 4750 Widen the float generator's steering so a 32-bit parameter arm of a ?: and a ?: over nested-block floats no longer regenerate known bugs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 15 +++++++++++++++ scripts/clang_oracle_float_gen.py | 26 ++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/compiler-bugs.md b/compiler-bugs.md index 72f1ba4a6..1b001edbd 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -418,6 +418,12 @@ signed char l2 = -66; negated low word `0x0a3d` of `0x3f970a3d`), and `fg = (b = 1.0f) - 1;` stores 1.0f. `fg = (b = 1.18f);` and a function-scope `b` are right. Found by fuzzing (float generator, seeds 1490 and 1861). +- A `?:` whose arms are both nested-block floats behaves like the variable: + `{ float b5 = 1.0f; g = 7 + (fg ? b5 : b5); }` stores 1 to an `int32_t` + `g` (want 8), as does `fg ? b5 : b6` with a second block float `b6`, and + storing to a float global. Right when either arm is a global + (`fg ? b5 : fg`), a constant (`fg ? b5 : 3.0f`) or `b5 + 0.0f`. Found by + fuzzing (float generator, seed 4750). ## BUG-016 @@ -1646,6 +1652,15 @@ static void f4(long x) { int i; for (i = 0; i < 3; ++i) la[1] = (0 ? 2L : x) - 1 only assigning it. - With and without `dcc-peep`. - Found by fuzzing (float generator, seed 2247). +- The test need not be constant: `for (...) l -= (c ? x : 1.0f); x = 1.0f; + return l;` with `float c = 2.0f` returns 0 (want `46b78578`), as does + `l = l - (c ? x : 1.0f)`, a `while` loop, `x = g;` after the loop, and + the `long` form `l -= (ic ? x : 16777216L)` (returns 0, want 23493). + `out[0] = (c ? x : 1.0f) - 1.0f;` in the loop is wrong as in the + constant case. Right, exactly as above: no assignment to `x` after the + loop, `x + 0.0f` as the arm, `x` in the else arm (`c ? 1.0f : x`), no + `?:`, reading `x` after the loop, a local copy, or no loop. Found by + fuzzing (float generator, seed 4294). ## BUG-053 diff --git a/scripts/clang_oracle_float_gen.py b/scripts/clang_oracle_float_gen.py index a7596f199..cf850c477 100644 --- a/scripts/clang_oracle_float_gen.py +++ b/scripts/clang_oracle_float_gen.py @@ -215,10 +215,26 @@ def is_blockint(e): def is_blockfloat(e): + """A nested-block float variable, or a `?:` whose arms are all such + variables (`fg ? b5 : b6` behaves like `b5`, seed 4750).""" + if e.op == 'cond': + return all(is_blockfloat(a) for a in e.k[1:]) d = block_object(e) return d is not None and d.t is F32 +def is_paramcond(e): + """A `?:` with a constant test (seed 2247) or a bare 32-bit parameter + as an arm (seed 4294): in a loop, with the parameter assigned afterwards, + the expression accumulates into the parameter (BUG-052).""" + if e.op != 'cond': + return False + if base.is_constant(e.k[0]): + return True + return any(a.op == 'var' and a.a.storage == 'param' and is_wide(a.a.t) + for a in e.k[1:]) + + def is_ptrparam(e): """The address of a parameter: with dccpeep, a later assignment to the parameter can be dropped as dead (BUG-053, seed 3036).""" @@ -311,7 +327,7 @@ def fincdec(e): found.add('floopcallarg') if is_ptrparam(e): found.add('fptrparam') - if e.op == 'cond' and base.is_constant(e.k[0]): + if is_paramcond(e): found.add('fconstcond') if is_paramcmp(e): found.add('fparamcmp') @@ -817,7 +833,13 @@ def fexpr(self, c, depth): test = self.cond(c, depth - 1) if not self.on('fconstcond') and base.is_constant(test): return self.fexpr(c, depth - 1) # BUG-052: a folded `?:` - return self.mk('cond', [test, self.fexpr(c, depth - 1), self.fexpr(c, depth - 1)]) + e = self.mk('cond', [test, self.fexpr(c, depth - 1), self.fexpr(c, depth - 1)]) + if not self.on('fconstcond') and is_paramcond(e): + # BUG-052: a parameter arm is passed through `+ 0.0f` + e.k[1:] = [self.mk('bin', [a, E('const', a=0.0, t=F32)], '+') + if a.op == 'var' and a.a.storage == 'param' else a + for a in e.k[1:]] + return e if kind == 'call': return self.call(c, depth, F32) if kind == 'mix': From 892025817866c73ad8c3f633f12d47e75691c1d2 Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Sat, 14 Nov 2026 02:57:58 +1100 Subject: [PATCH 13/14] Document differential testing against clang and add bug-issue-body.py Add the appendix page describing the clang-oracle fuzzing method, the tools in scripts/ and the campaign and bug-logging workflow, with the clang-oracle-testing.svg diagram. Move the issue-body generator into scripts/ with titles and issue links taken from compiler-bugs.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../en/appendix/06-differential-testing.md | 175 ++++++++++++++ docs/docs/mkdocs.yml | 1 + scripts/README.md | 15 ++ scripts/bug-issue-body.py | 213 ++++++++++++++++++ 4 files changed, 404 insertions(+) create mode 100644 docs/docs/en/appendix/06-differential-testing.md create mode 100755 scripts/bug-issue-body.py diff --git a/docs/docs/en/appendix/06-differential-testing.md b/docs/docs/en/appendix/06-differential-testing.md new file mode 100644 index 000000000..7268b2db0 --- /dev/null +++ b/docs/docs/en/appendix/06-differential-testing.md @@ -0,0 +1,175 @@ +# Appendix: differential testing against clang + +The regression suite in `tests/` checks that programs people have written +still behave. It cannot find a bug in a shape nobody has written yet. This +appendix describes the second line of testing: a *differential fuzzer* that +generates C programs, lets the host C compiler say what each one should +print, and reports where the DCC C Compiler disagrees. + +The host compiler is the oracle. clang is a mature, heavily tested compiler, +so when it and dcc print different things for a program that is defined C, +the fault is almost always dcc's. The method is the same one Csmith and +similar tools use for mainstream compilers, adapted to a target whose `int` is +16 bits wide and whose only floating type is `float`. + +[![How dcc is tested against clang: generate, prove defined, build on both, compare, reduce, confirm, park, log](../images/clang-oracle-testing.svg)](../images/clang-oracle-testing.svg) + +## How it works + +1. **Generate a program.** One seed produces one self-contained C program: + helper and test functions over fixed-width integers, arrays, structs, + bit-fields and pointers, with loops, `switch`, nested-block locals, + `goto`, calls with mixed-width arguments, bounded recursion and calls + through function pointers. The float generator adds `float` objects of + every storage class, arithmetic, comparison, `?:`, `++`/`--`, compound + assignment and conversions to and from every integer width. The program + never spells `int` or `long`; the renderer inserts the casts and masks that + make every expression mean the same with a 16-bit and a 32-bit `int`. +2. **Prove it is defined C.** What a cast cannot fix — signed overflow at 16 + bits, division by zero, out-of-range shifts, an unsequenced modification — + is caught on the host. A second rendering routes each such operation + through a checking helper and is built with ASan and UBSan; operations + that trip are repaired and the program is checked again. It is used only + when the checked build reports nothing, clang finds no unsequenced + modification or C23-only syntax, and the `-O0`, `-O2`, sanitized and + checked builds all print the same. Otherwise it is discarded as `INVALID`. +3. **Build on both sides.** clang's output is the expected result. dcc builds + the same source through `dcc → dccpeep → m80c → l80c` and runs it under + `ntvcm`, in each production configuration: with and without `dccpeep`, + with and without the stack check. `--configs all` adds one configuration + per MIR candidate emitter, forced with `DCC_MIR_SELECT_CANDIDATE`, so the + emitters the cost policy does not pick today are exercised as well. +4. **Compare.** Same output in every configuration: the program passes and + its files are deleted. Otherwise the seed is reported as `COMPILE` (valid + C rejected), `VERIFY` (a MIR verifier diagnostic), `MISMATCH`, `RUN-HANG` + or `BUILD-HANG`. A program that only overflows the target stack, does not + fit in 64K, or reports `out of ram` has met a limit of the target, not a + defect, and is counted as `limit-only`. +5. **Reduce.** A failing seed is shrunk automatically to the few lines that + still fail the same way. Every candidate edit must still pass the host + checks of step 2, and the reducer refuses edits that would turn the + failure into one already logged. +6. **Confirm by hand.** One-statement variants of the reduced program are + run on both compilers to find exactly which shape triggers the defect and + which neighbouring shapes are right. A new shape of a filed bug becomes a + comment on that issue rather than a new one; a fault in the fuzzer itself + is fixed, not filed. +7. **Park a reproducer.** `tests/known-bugs/bugNNN.c` holds the failing + cases with passing controls. Its baseline is clang's output, proven under + ASan and UBSan. These programs are kept outside `tests/*.c`, so the + regression suite stays green while the bug is open. +8. **Log and report.** The bug gets an entry and a summary-table row in + `compiler-bugs.md`, a GitHub issue, and a steering rule in the generator + so later runs look for something new instead of finding the same bug + again. + +The outputs are deliberately plain: one line per call with the returned +value, or a checksum of every global after the call, printed as hex. Floats +are printed as their IEEE bits with `-0` folded to `0`, so a difference is a +difference in the arithmetic, not in formatting, and a one-ulp error is +visible. + +## Tools + +All of these live in `scripts/`, run on the host with Python 3.9 or later, +and need clang, `ntvcm` and a built toolchain in the repository root. + +| Tool | Role | +| --- | --- | +| `clang-oracle-fuzz.py` | The driver: generation, host proof, dcc build and run in each configuration, comparison, reduction | +| `clang_oracle_gen.py` | The integer program model, generator, renderers and undefined-behaviour repair; `KNOWN_BUGS` maps generator features to logged bugs | +| `clang_oracle_float_gen.py` | The float generator, built on the same model; selected with `--gen float` | +| `run-known-bugs.py` | Checks every parked reproducer: proves its baseline on the host, then reports `fails` or `FIXED` for dcc with and without `dccpeep` | +| `bug-issue-body.py` | Writes a GitHub issue body for an entry in `compiler-bugs.md` from the reproducer, its baseline and dcc's current behaviour; `--create` opens the issue | +| `typed-expr-fuzz.py` | The older single-expression fuzzer, checked against a Python model rather than clang | + +The full option list is in `scripts/README.md` and `--help`. + +## Running a campaign + +Fuzz a range of seeds in the production configurations, leaving out every +form that hits a bug already logged: + +```sh +python3 scripts/clang-oracle-fuzz.py --seeds 1-300 --configs production --off known +python3 scripts/clang-oracle-fuzz.py --gen float --seeds 1-300 --configs production --off known +``` + +A 300-seed batch takes about ten minutes on a laptop with `--jobs 6`. The +last line is the tally, for example `300 programs: FAIL 1, INVALID 29, +ok 270`; an `INVALID` rate of ten to fifteen percent is normal and means the +host proof rejected programs that were not defined C. + +Reduce a failure under one configuration, with a short timeout so a hang +reduces quickly: + +```sh +python3 scripts/clang-oracle-fuzz.py --reduce 4750 --config nopeep --off known --timeout 3 +``` + +The result is `build/clang-oracle-fuzz/4750-reduce-nopeep/reduced.c` with +`reduced.expected` and `reduced.actual` beside it. Keep the reduced program, +not the seed: a seed names a program only together with the generator version +and the `--tests`, `--size`, `--depth` and `--off` options. + +Force every MIR candidate emitter as well. This is roughly ten times the +work, so use smaller batches, and remember that a failure only in a forced +configuration is latent rather than shipped behaviour: + +```sh +python3 scripts/clang-oracle-fuzz.py --seeds 1-150 --configs all --off known --jobs 3 +python3 scripts/clang-oracle-fuzz.py --list-configs +``` + +A reasonable stop rule for a campaign is two consecutive clean batches after +the last change to the generator. Changing the generator re-randomises every +seed, so the count restarts. + +## Logging a bug + +1. Confirm the reduced program by hand with one-statement variants, and check + `compiler-bugs.md` for an entry with the same root shape. A new shape of + an existing bug is a bullet in that entry and a comment on its issue. +2. Write `tests/known-bugs/bugNNN.c` with the failing cases and passing + controls, using `` types only, and let the host write its + baseline: + + ```sh + python3 scripts/run-known-bugs.py --update bugNNN + python3 scripts/run-known-bugs.py bugNNN + ``` + +3. Add a `## BUG-NNN` entry and a summary-table row to `compiler-bugs.md`, + and update the open count. +4. Generate and open the issue, then put its number in the table row: + + ```sh + python3 scripts/bug-issue-body.py bugNNN --create + ``` + +5. Steer the generator round the shape: add a feature to `FEATURES` and + `KNOWN_BUGS` in the generator, a detector in `known_shapes()`, and the + rewrite that avoids it. Confirm that no program over a few thousand seeds + still contains a known shape with `--off known`, then rerun the campaign. + +## When a bug is fixed + +`run-known-bugs.py` reports the reproducer as `FIXED`. Promote it to a +regular test — move it to `tests/.c` with its baseline, add its row to +`tests/perf_baselines.csv`, close the entry in `compiler-bugs.md`, and remove +the bug's feature from `KNOWN_BUGS` so the fuzzer generates that form again. +`tests/known-bugs/README.md` has the exact steps. + +## What this does and does not find + +The fuzzer finds wrong code, rejected valid C, verifier diagnostics, +compile-time crashes and hangs in the shapes the generator can produce. +Because every program is proven defined on the host, every report is a real +defect, not a porting difference, and the reproducer is small enough to +read. + +It does not cover the standard library, I/O, the preprocessor beyond what the +generated programs use, or behaviour that depends on CP/M. It cannot judge +performance. And it only explores the language surface the generators know +about, so extending a generator — a new statement form, a new lvalue kind, a +new conversion — is how the search is widened. diff --git a/docs/docs/mkdocs.yml b/docs/docs/mkdocs.yml index a03355343..e9a20f54b 100644 --- a/docs/docs/mkdocs.yml +++ b/docs/docs/mkdocs.yml @@ -55,6 +55,7 @@ nav: - Utilities: en/appendix/03-utilities.md - Banked memory support: en/appendix/04-banked-memory.md - Building project docs: en/appendix/05-building-project-docs.md + - Differential testing against clang: en/appendix/06-differential-testing.md plugins: - search diff --git a/scripts/README.md b/scripts/README.md index a63a69619..d467e9966 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -160,6 +160,21 @@ python3 scripts/run-known-bugs.py bug003 --dcc path/to/another/dcc python3 scripts/run-known-bugs.py --update bug022 # write a new baseline from clang ``` +## `bug-issue-body.py` + +Writes the GitHub issue for an entry in `compiler-bugs.md`: the entry's text, +the reproducer from `tests/known-bugs/`, the build commands, the host +compiler's output (checked against the baseline) and what dcc does now, with +and without `dccpeep`. The title comes from the entry's summary-table row and +cross-references become issue links through the table's Issue column, so the +row must exist first. Bodies go to `build/bug-issues/bugNNN.md`; `--create` +opens the issue with `gh`. + +```sh +python3 scripts/bug-issue-body.py bug054 +python3 scripts/bug-issue-body.py bug054 --create +``` + ## `mir-migration-census.py` Measures generated MIR selection across `tests/*.c` and compares two snapshots diff --git a/scripts/bug-issue-body.py b/scripts/bug-issue-body.py new file mode 100755 index 000000000..cefb6bac4 --- /dev/null +++ b/scripts/bug-issue-body.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Write a GitHub issue body for an open entry in compiler-bugs.md. + +The body is the entry's text, the parked reproducer from tests/known-bugs/, +the commands to build it, the host compiler's output (checked against the +baseline) and what dcc actually does, captured by running it now with and +without dccpeep. The title is the entry's row in the summary table, and +references to other entries become issue links through the table's Issue +column. + + bug-issue-body.py bug054 write build/bug-issues/bug054.md + bug-issue-body.py bug054 --create also open the issue with gh + bug-issue-body.py every entry without an issue number + +An entry must have a summary row before its issue can be written, and the +compiler must be the upstream one: local commits may add tests and scripts, +not change src/ or DCCRTL.MAC. +""" +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +OUT = REPO / 'build' / 'bug-issues' +KNOWN = REPO / 'tests' / 'known-bugs' +HOST = ['clang', '-std=gnu99', '-fsigned-char'] + + +def upstream_commit(): + """The upstream commit the compiler is built from.""" + commit = subprocess.run(['git', 'merge-base', 'HEAD', 'origin/main'], cwd=REPO, + capture_output=True, text=True).stdout.strip()[:8] + changed = subprocess.run(['git', 'diff', '--name-only', commit, 'HEAD', '--', + 'src', 'DCCRTL.MAC'], + cwd=REPO, capture_output=True, text=True).stdout.strip() + if changed: + sys.exit('local commits change the compiler: issues must not cite upstream %s' + % commit) + return commit + + +def table(): + """bugNNN -> (issue number or '', kind, defect) from the summary table.""" + out = {} + text = (REPO / 'compiler-bugs.md').read_text() + for m in re.finditer(r'^\| \[BUG-(\d+)\]\S* \| (#?\d*|—|-)? ?\| ([^|]*) \| (.*) \|$', + text, re.M): + issue = (m.group(2) or '').strip('# ') + out['bug' + m.group(1)] = (issue if issue.isdigit() else '', + m.group(3).strip(), + m.group(4).strip().replace(r'\|', '|')) + return out + + +def sections(): + text = (REPO / 'compiler-bugs.md').read_text() + out = {} + for m in re.finditer(r'^## (BUG-(\d+))\n(.*?)(?=^## |^---$)', text, re.M | re.S): + out['bug' + m.group(2)] = m.group(3).strip() + return out + + +def run(cmd, cwd, timeout): + try: + r = subprocess.run(cmd, cwd=cwd, capture_output=True, timeout=timeout, + stdin=subprocess.DEVNULL) + except subprocess.TimeoutExpired as exc: + partial = (exc.stdout or b'').decode('latin-1').replace('\r', '') + return None, partial[:600] + return r.returncode, (r.stdout + r.stderr).decode('latin-1').replace('\r', '') + + +def dcc(name, src, peep): + """('compile', text) | ('hang', text) | ('output', text)""" + build = OUT / 'work' / ('%s-%s' % (name, 'peep' if peep else 'nopeep')) + shutil.rmtree(build, ignore_errors=True) + code, log = run([str(REPO / 'dccmake'), 'dcc-input=%s' % src, 'dcc-output=%s' % name.upper(), + 'dcc-build-dir=%s' % build, 'dcc-peep=%s' % ('true' if peep else 'false')], + REPO, 120) + if code != 0: + lines = [ln for ln in log.splitlines() if not ln.startswith('+') + and not ln.startswith('command failed') and ln.strip()] + return 'compile', '\n'.join(lines).replace(str(src), name + '.c') + diag = [ln for ln in log.splitlines() if ln.startswith('; MIR ')] + code, out = run(['ntvcm', name.upper() + '.COM'], build, 60) + if code is None: + return 'hang', out + return 'output', ('\n'.join(diag) + '\n' if diag else '') + out + + +def title(name, rows): + issue, kind, defect = rows[name] + if kind.startswith('wrong result'): + prefix = 'Wrong code' + elif kind.startswith('hang'): + prefix = 'Hang' + elif kind.startswith('compile'): + prefix = 'Valid C rejected' + else: + prefix = kind[0].upper() + kind[1:] + if 'dccpeep' in kind: + prefix += ' (dccpeep)' + return '%s: %s' % (prefix, defect) + + +def body(name, entry, rows, commit): + src = OUT / 'work' / (name + '.c') + src.parent.mkdir(parents=True, exist_ok=True) + source = (KNOWN / (name + '.c')).read_text() + src.write_text(source) + # the local log's id means nothing upstream + shown = re.sub(r'^/\* BUG-\d+: (.)', lambda m: '/* ' + m.group(1).upper(), source) + host_exe = OUT / 'work' / (name + '.host') + subprocess.run(HOST + [str(src), '-o', str(host_exe)], check=True, capture_output=True) + _, expected = run([str(host_exe)], OUT, 20) + baseline = (KNOWN / 'baselines' / (name + '.txt')).read_text() + if expected != baseline: + sys.exit('%s: the host output differs from its baseline' % name) + nopeep = dcc(name, src, False) + peep = dcc(name, src, True) + for other, (issue, _, _) in rows.items(): + if issue: + ref = 'BUG-' + other[3:] + entry = entry.replace(ref, '#' + issue) + shown = shown.replace(ref, '#' + issue) + left = sorted(set(re.findall(r'BUG-\d+', entry + shown))) + if left: + sys.exit('%s refers to %s, which have no issue yet' % (name, ', '.join(left))) + upper = name.upper() + parts = [entry, '', '## Reproduce', '', + 'Compiler: `main` at `%s`, built with `pwsh ./scripts/build-dcc.ps1`. ' + 'Emulator: ntvcm.' % commit, '', + 'Save as `%s.c` in the repository root:' % name, '', '```c', shown.rstrip(), '```', '', + '```sh', + './dccmake dcc-input=%s.c dcc-output=%s dcc-build-dir=build/%s dcc-peep=false' % ( + name, upper, name), + '(cd build/%s && ntvcm %s.COM)' % (name, upper), + '', + '# reference result from the host compiler', + '%s %s.c -o %s.host && ./%s.host' % (' '.join(HOST), name, name, name), + '```', '', + '## Expected', '', + 'The host compiler\'s output. The program uses fixed-width types only, so the ' + 'result does not depend on `int` being 16 or 32 bits, and it runs clean under ' + '`-fsanitize=address,undefined`.', '', + '```', expected.rstrip(), '```', '', '## Actual', ''] + kind, text = nopeep + if kind == 'compile': + parts += ['The compile fails:', '', '```', text.rstrip(), '```'] + elif kind == 'hang': + if text.strip(): + parts += ['The program is still running after 60 seconds. It printed this first ' + '(truncated):', '', '```', text.rstrip(), '```'] + else: + parts += ['The program prints nothing and is still running after 60 seconds.'] + else: + parts += ['```', text.rstrip(), '```'] + parts.append('') + if peep == nopeep: + parts.append('The result is the same with `dcc-peep=true`.') + elif peep[0] == 'output' and nopeep[0] == 'output': + parts += ['With `dcc-peep=true` the output differs again:', '', '```', peep[1].rstrip(), '```'] + else: + parts += ['With `dcc-peep=true`: %s.' % ('the compile fails the same way' if peep[0] == 'compile' + else 'the program hangs' if peep[0] == 'hang' + else 'see below')] + parts += ['', '## Notes', '', + '- Found by differential testing against clang with generated programs ' + '(`scripts/clang-oracle-fuzz.py`), then narrowed by hand. The reproducer is ' + '`tests/known-bugs/%s.c`; `python3 scripts/run-known-bugs.py %s` checks it.' + % (name, name)] + return '\n'.join(parts) + '\n' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('names', nargs='*', help='entries, e.g. bug054; default: all without an issue') + parser.add_argument('--create', action='store_true', help='open each issue with gh') + parser.add_argument('--repo', default='davidly/dcc') + args = parser.parse_args() + rows, entries = table(), sections() + names = args.names or sorted(n for n, (issue, _, _) in rows.items() if not issue) + if not names: + sys.exit('every entry in the table has an issue') + for name in names: + if name not in rows: + sys.exit('%s has no row in the summary table of compiler-bugs.md' % name) + if name not in entries: + sys.exit('%s has no ## BUG- section in compiler-bugs.md' % name) + commit = upstream_commit() + OUT.mkdir(parents=True, exist_ok=True) + for name in names: + text = body(name, entries[name], rows, commit) + path = OUT / (name + '.md') + path.write_text(text) + heading = title(name, rows) + (OUT / (name + '.title')).write_text(heading + '\n') + print('%s: %d bytes\n %s' % (name, len(text), heading)) + if args.create: + r = subprocess.run(['gh', 'issue', 'create', '--repo', args.repo, '--label', 'bug', + '--title', heading, '--body-file', str(path)], + capture_output=True, text=True) + print(' ' + (r.stdout.strip() or r.stderr.strip())) + + +if __name__ == '__main__': + main() From 9f7eeda8f5b037b734c025e514c87f640c5336ae Mon Sep 17 00:00:00 2001 From: Dave Glover Date: Sat, 14 Nov 2026 04:08:43 +1100 Subject: [PATCH 14/14] Record BUG-020 and BUG-031 facets from forced-candidate seeds 20063 and 20149 Forced-MIR-candidate runs (21 nopeep configs, seeds 20001-20300, int and float) found no candidate-only failures. The two production-visible failures are new facets of existing bugs; steer the generators around them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler-bugs.md | 14 ++++++++++++++ scripts/clang_oracle_float_gen.py | 12 +++++++----- scripts/clang_oracle_gen.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/compiler-bugs.md b/compiler-bugs.md index 1b001edbd..b1dfe0687 100644 --- a/compiler-bugs.md +++ b/compiler-bugs.md @@ -588,6 +588,14 @@ static int32_t f(int32_t x) x)` (parameter on the right), `x >= l`, both operands parameters, `l` a local assigned instead of returned, and the same with `int` or `long` operands. Found by fuzzing (float generator, seed 2017). +- The right operand of the float arithmetic can be an integer constant: + after `if (n) { while (n5 > 0) n5 = 0; } else { }`, `if (7.0f != (x0 + + 28))` with a float parameter `x0` is fatal, as are `x0 - 28`, `x0 * 28`, + `x0 / 28`, `x0 + (float)28`, `==`, `<`, an integer or float global on the + left, the value assigned, and an `int` function. Fine: `x0 + 28.0f`, a + float global in place of the parameter, the operands swapped (`(x0 + 28) + != 7.0f`), `7.0f != x0`, no loop in the `then` body, or the comparison + before the `if`. Found by fuzzing (float generator, seed 20063). ## BUG-021 @@ -1013,6 +1021,12 @@ dcc emits, for the store: `{ int32_t b = 0; b = y; a2[1] = b; }` (found by fuzzing, seed 5089). A different type on the right, such as `uint32_t b = 0; b = y;` with an `int32_t y`, is correct. +- A constant initialiser is also wrong when the destination address is + computed: `{ uint32_t b = 0x30201; a2[i] = b; }` with a variable index + stores 0 (want `0201`), as do `*p = b` (`p` a global or local pointer) + and `ps->m = b`. Right with a constant index, `s.m = b`, `(uint16_t)b`, + `b + 1`, an initialiser that is a global or an expression, the local at + function scope, or the block an `if` body. Found by fuzzing (seed 20149). - Not a regression: the v3.6.0 compiler (`20290235`) does the same. - One more case of a nested-block local losing its declared width; see BUG-015 and BUG-025. diff --git a/scripts/clang_oracle_float_gen.py b/scripts/clang_oracle_float_gen.py index cf850c477..7fc0c8888 100644 --- a/scripts/clang_oracle_float_gen.py +++ b/scripts/clang_oracle_float_gen.py @@ -185,16 +185,18 @@ def is_wide(t): def is_paramcmp(e): """e is the BUG-020 facet shape: `v REL (param OP x)` with a 32-bit - binop on the right, a parameter as its left operand and a non-constant - as its right operand. An `if … else` with a loop, or a braced early - return (seed 2017), before it makes the compile fail; any function - counts.""" + binop on the right, a parameter as its left operand and anything but a + plain float constant as its right operand (an integer constant or a + cast of one counts: seed 20063). An `if … else` with a loop, or a + braced early return (seed 2017), before it makes the compile fail; any + function counts.""" if e.op != 'bin' or e.a not in CMP: return False rhs = e.k[1] return rhs.op == 'bin' and rhs.a in ('+', '-', '*', '/') and is_wide(ty(rhs)) \ and rhs.k[0].op == 'var' and rhs.k[0].a.storage == 'param' \ - and not is_anyconst(rhs.k[1]) + and not (rhs.k[1].op == 'const' and rhs.k[1].t is F32) \ + and not (is_anyconst(rhs.k[1]) and ty(rhs) is not F32) def block_object(e): diff --git a/scripts/clang_oracle_gen.py b/scripts/clang_oracle_gen.py index b84d8496a..a74024f6e 100644 --- a/scripts/clang_oracle_gen.py +++ b/scripts/clang_oracle_gen.py @@ -489,6 +489,16 @@ def wide_block_copy(d): and isinstance(d.init, E) and d.init.op == 'var' and d.init.a.storage != 'global' +def wide_block_store(lv, rhs): + """A 32-bit nested-block local stored as it stands to a 16-bit element, + pointee or member reached through a pointer: the BUG-031 shape when the + address is computed (seed 20149 stores 0 for a constant-initialised + local through a variable index).""" + return lv.op != 'var' and is_int(ty(lv)) and ty(lv).bits == 16 \ + and rhs.op == 'var' and rhs.a.storage == 'block' and is_int(rhs.a.t) \ + and rhs.a.t.bits == 32 + + def const_div_zero(e): """e is a constant divided by a constant zero, which dcc rejects even where it is never evaluated: the BUG-033 shape.""" @@ -591,6 +601,8 @@ def expr(e, root): found.add('loopcopy') if lv.a.storage != 'global' and lt.bits == 16 and cross_type(rhs, lt): found.add('crosstypeasg') + if wide_block_store(lv, rhs): + found.add('blockwidecopy') elif is_int(lt): local = lv.op == 'var' and lv.a.storage != 'global' if lv.op != 'var' and lt.bits < 32 and ty(rhs).bits == 32: @@ -1876,6 +1888,8 @@ def st_assign(self, c, depth, b): rhs = self.gen_expr(c, depth) if lv.op == 'var': rhs = self.stored(rhs, lv.a.t, lv.a.storage) + elif not self.on('blockwidecopy') and wide_block_store(lv, rhs): + rhs = self.mk('cast', [rhs], t=ty(lv)) b.stmts.append(S('expr', [self.mk('asg', [lv, rhs], '=')])) def compound(self, lv, rhs, op): @@ -1883,6 +1897,8 @@ def compound(self, lv, rhs, op): reject or miscompile (see KNOWN_BUGS); the operand is converted so the statement stays out of it and fuzzing can reach other code.""" lt = ty(lv) + if op == '=' and not self.on('blockwidecopy') and wide_block_store(lv, rhs): + rhs = self.mk('cast', [rhs], t=lt) if op != '=' and is_int(lt): if not self.on('selfand') and lv.op == 'var' and rhs.op == 'var' and rhs.a is lv.a: rhs = self.small(1, 7)