From 0d2e73ee00bf74e3f22463c2a777e8fd8ce6929a Mon Sep 17 00:00:00 2001 From: David Montero Date: Sat, 15 Aug 2026 04:13:59 +0200 Subject: [PATCH] fix(espidf): unknown #if must not prune its #else in the include scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dead-branch pruner marked any assumed-live branch as 'taken', so the #else of an UNKNOWN condition was skipped. GxEPD2 guards its GFX base class with '#if ENABLE_GxEPD2_GFX (default 0) ... #else #include ' — the pruned #else silently dropped the Adafruit_GFX dependency and every GxEPD2 e-paper example failed with 'fatal error: Adafruit_GFX.h: No such file'. Found by the nightly gallery compile smoke. 'taken' is now set only by PROVABLY-true conditions (all identifiers in the known-true set), keeping the 2026-08-03 ESPAsyncWebServer behaviour: provably-dead branches still prune, provably-true branches still kill their #else. --- backend/app/services/espidf_compiler.py | 36 +++++++++++++++++-- .../unit/test_espidf_include_scanner.py | 31 ++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index 73bc9b33..e9abd7f5 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -1623,6 +1623,24 @@ class ESPIDFCompiler: # Dead only when we recognise EVERY identifier as false-on-ESP32. return not all(i in self._PP_FALSE for i in idents) + def _pp_branch_is_provably_true(self, expr: str) -> bool: + """True only when EVERY identifier is a macro known-true on ESP32 — + the only case where the #elif/#else siblings are provably dead. + + This must stay distinct from _pp_branch_is_live: an UNKNOWN condition + is treated as live for scanning, but it must NOT mark the branch + chain as taken. GxEPD2 guards its GFX base class as + `#if ENABLE_GxEPD2_GFX ... #elif ... #else #include ` + with ENABLE_GxEPD2_GFX defaulting to 0 — treating the unknown #if as + taken pruned the #else, silently dropped the Adafruit_GFX dependency + and broke every GxEPD2 e-paper example (found by the gallery compile + smoke, 2026-08-15).""" + idents = [ + i for i in self._PP_IDENT_RE.findall(expr) + if i not in ('defined', 'ifdef', 'ifndef') + ] + return bool(idents) and all(i in self._PP_TRUE for i in idents) + def _detect_external_includes( self, code: str, own_files: set[str] | None = None ) -> list[str]: @@ -1663,21 +1681,33 @@ class ESPIDFCompiler: if line.startswith('#'): dtv = line[1:].lstrip() if dtv.startswith(('ifdef', 'ifndef', 'if ', 'if(')): + # `taken` gates whether LATER #elif/#else arms are dead. + # It must be set only by PROVABLY-true conditions: an + # unknown #if is scanned as live, but its #else must stay + # live too — the real compiler may well take it (the + # GxEPD2 ENABLE_GxEPD2_GFX default-0 pattern). if dtv.startswith('ifdef'): live = self._pp_branch_is_live(dtv[5:]) + taken = self._pp_branch_is_provably_true(dtv[5:]) elif dtv.startswith('ifndef'): - # #ifndef X is live unless X is known-defined here. + # #ifndef X is live unless X is known-defined here, + # and provably TRUE when X is known-undefined. idents = self._PP_IDENT_RE.findall(dtv[6:]) live = not any(i in self._PP_TRUE for i in idents) + taken = bool(idents) and all(i in self._PP_FALSE for i in idents) else: live = self._pp_branch_is_live(dtv[2:]) - stack.append((live, live)) + taken = self._pp_branch_is_provably_true(dtv[2:]) + stack.append((live, taken)) continue if dtv.startswith('elif'): if stack: _cur, taken = stack[-1] live = (not taken) and self._pp_branch_is_live(dtv[4:]) - stack[-1] = (live, taken or live) + stack[-1] = ( + live, + taken or self._pp_branch_is_provably_true(dtv[4:]), + ) continue if dtv.startswith('else'): if stack: diff --git a/test/backend/unit/test_espidf_include_scanner.py b/test/backend/unit/test_espidf_include_scanner.py index 8cc50786..4489dc0e 100644 --- a/test/backend/unit/test_espidf_include_scanner.py +++ b/test/backend/unit/test_espidf_include_scanner.py @@ -77,6 +77,37 @@ class TestPreprocessorAwareIncludeScan(unittest.TestCase): '#else\n#include \n#endif'), ['B.h']) + def test_else_of_an_unknown_branch_is_live(self): + """The GxEPD2 shape (found by the gallery compile smoke, 2026-08-15): + + #if ENABLE_GxEPD2_GFX // defaults to 0 — unknown to us + #include "GxEPD2_GFX.h" + #elif defined(_GFX_H_) + #define GxEPD2_GFX_BASE_CLASS GFX + #else + #include // what the compiler really takes + #endif + + An UNKNOWN #if must not mark the chain as taken: pruning this #else + dropped Adafruit_GFX from user_libs_all and broke every GxEPD2 + e-paper example with 'fatal error: Adafruit_GFX.h: No such file'.""" + got = self.scan( + '#if ENABLE_GxEPD2_GFX\n#include "GxEPD2_GFX.h"\n' + '#elif defined(_GFX_H_)\n#define GxEPD2_GFX_BASE_CLASS GFX\n' + '#else\n#include \n#endif') + self.assertIn('Adafruit_GFX.h', got) + # The unknown arm itself also stays live — over-reporting is the + # accepted cost, hiding a real dependency is not. + self.assertIn('GxEPD2_GFX.h', got) + + def test_else_of_a_provably_true_branch_stays_dead(self): + """The counterweight: when the #if IS provably true on ESP32, its + #else really is dead and must stay pruned.""" + self.assertEqual( + self.scan('#if defined(ESP32)\n#include \n' + '#else\n#include \n#endif'), + ['A.h']) + def test_nested_dead_block(self): self.assertEqual( self.scan('#ifdef ESP8266\n#ifdef FOO\n#include \n'