fix(espidf): unknown #if must not prune its #else in the include scanner
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 <Adafruit_GFX.h>' — 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.
This commit is contained in:
parent
8391cc7fa8
commit
0d2e73ee00
|
|
@ -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 <Adafruit_GFX.h>`
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -77,6 +77,37 @@ class TestPreprocessorAwareIncludeScan(unittest.TestCase):
|
|||
'#else\n#include <C.h>\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 <Adafruit_GFX.h> // 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 <Adafruit_GFX.h>\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 <A.h>\n'
|
||||
'#else\n#include <B.h>\n#endif'),
|
||||
['A.h'])
|
||||
|
||||
def test_nested_dead_block(self):
|
||||
self.assertEqual(
|
||||
self.scan('#ifdef ESP8266\n#ifdef FOO\n#include <Deep.h>\n'
|
||||
|
|
|
|||
Loading…
Reference in New Issue