diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 8c44964cacb..e88add3828d 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -531,18 +531,19 @@ class Library: return self -def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]: - """Wrap ``lines`` in one or more ``[]() __attribute__((noinline)) {...}();`` IIFEs. +def _wrap_in_iifes(lines: list[str], max_statements: int) -> list[str]: + """Wrap ``lines`` in one or more ``[]() {...}();`` IIFEs. Splits into IIFEs of up to ``max_statements`` entries each. Never splits inside a brace-balanced block (e.g. the ``{`` / ``}`` pair that ``cg.with_local_variable()`` emits around a scoped local), so an IIFE may exceed ``max_statements`` when a block straddles the boundary. - GCC note: ``__attribute__((noinline))`` between the lambda parameter - list and body binds to ``operator()``. The C++ standard-attribute - spelling ``[[gnu::noinline]]`` in that position binds to the return - type instead and produces ``-Wattributes`` warnings. + The IIFEs intentionally have no ``noinline`` attribute: GCC's ``-Os`` + inliner makes good decisions about which chunks to keep as functions + and which to re-inline, and forcing all chunks to stay as functions + costs flash without measurably improving peak stack on configs where + the scope structure is itself sufficient to bound live-range lifetimes. """ out: list[str] = [] chunk: list[str] = [] @@ -551,7 +552,7 @@ def _wrap_in_noinline_iifes(lines: list[str], max_statements: int) -> list[str]: def flush() -> None: if not chunk: return - out.append("[]() __attribute__((noinline)) {") + out.append("[]() {") out.extend(chunk) out.append("}();") chunk.clear() @@ -1065,7 +1066,7 @@ class EsphomeCore: # (e.g. sensor platforms with many filter registrations). pieces = list(prefix) for block in components: - pieces.extend(_wrap_in_noinline_iifes(block, max_statements=50)) + pieces.extend(_wrap_in_iifes(block, max_statements=50)) return "\n".join(pieces) + "\n\n" @property diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1e4f129896d..02a804e6a1d 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -870,14 +870,14 @@ class TestEsphomeCore: assert "Wire" in target.platformio_libraries -def test_wrap_in_noinline_iifes_empty_input() -> None: - assert not core._wrap_in_noinline_iifes([], max_statements=10) +def test_wrap_in_iifes_empty_input() -> None: + assert not core._wrap_in_iifes([], max_statements=10) -def test_wrap_in_noinline_iifes_fewer_lines_than_limit() -> None: +def test_wrap_in_iifes_fewer_lines_than_limit() -> None: lines = ["a();", "b();", "c();"] - assert core._wrap_in_noinline_iifes(lines, max_statements=10) == [ - "[]() __attribute__((noinline)) {", + assert core._wrap_in_iifes(lines, max_statements=10) == [ + "[]() {", "a();", "b();", "c();", @@ -885,51 +885,51 @@ def test_wrap_in_noinline_iifes_fewer_lines_than_limit() -> None: ] -def test_wrap_in_noinline_iifes_splits_at_max_statements() -> None: +def test_wrap_in_iifes_splits_at_max_statements() -> None: lines = [f"s{i}();" for i in range(5)] - result = core._wrap_in_noinline_iifes(lines, max_statements=2) + result = core._wrap_in_iifes(lines, max_statements=2) # With max=2 and 5 lines: chunks of 2, 2, 1 → 3 IIFEs. assert sum(1 for line in result if line.startswith("[]()")) == 3 -def test_wrap_in_noinline_iifes_never_splits_inside_braces() -> None: +def test_wrap_in_iifes_never_splits_inside_braces() -> None: # max=2 would naively split after "{" but brace guard keeps block whole. lines = ["a();", "{", "inner();", "}", "b();"] - assert core._wrap_in_noinline_iifes(lines, max_statements=2) == [ - "[]() __attribute__((noinline)) {", + assert core._wrap_in_iifes(lines, max_statements=2) == [ + "[]() {", "a();", "{", "inner();", "}", "}();", - "[]() __attribute__((noinline)) {", + "[]() {", "b();", "}();", ] -def test_wrap_in_noinline_iifes_nested_braces() -> None: +def test_wrap_in_iifes_nested_braces() -> None: lines = ["{", "{", "deep();", "}", "}", "after();"] - assert core._wrap_in_noinline_iifes(lines, max_statements=1) == [ - "[]() __attribute__((noinline)) {", + assert core._wrap_in_iifes(lines, max_statements=1) == [ + "[]() {", "{", "{", "deep();", "}", "}", "}();", - "[]() __attribute__((noinline)) {", + "[]() {", "after();", "}();", ] -def test_wrap_in_noinline_iifes_unbalanced_braces_fall_through() -> None: +def test_wrap_in_iifes_unbalanced_braces_fall_through() -> None: # Pathological input where "}" appears before "{": don't crash; emit # a single IIFE with all lines rather than splitting mid-flight. lines = ["a();", "}", "b();"] - result = core._wrap_in_noinline_iifes(lines, max_statements=1) - assert result[0] == "[]() __attribute__((noinline)) {" + result = core._wrap_in_iifes(lines, max_statements=1) + assert result[0] == "[]() {" assert result[-1] == "}();" assert [line for line in result if line in lines] == lines @@ -952,7 +952,7 @@ def test_cpp_main_section_component_marker_wraps_in_iife() -> None: RawStatement("new_wifi();"), ] out = target.cpp_main_section - assert out.count("[]() __attribute__((noinline)) {") == 2 + assert out.count("[]() {") == 2 assert out.count("}();") == 2 assert "// === logger ===" in out assert "// === wifi ===" in out @@ -966,4 +966,4 @@ def test_cpp_main_section_prefix_statements_stay_outside_iife() -> None: RawStatement("body();"), ] out = target.cpp_main_section - assert out.index("prefix();") < out.index("[]() __attribute__((noinline)) {") + assert out.index("prefix();") < out.index("[]() {")