[core] Address Copilot review: robust brace depth, accurate docstrings

- Count { and } characters per line instead of matching whole-line
  tokens. Current codegen only emits scope braces as standalone lines
  (from cg.with_local_variable()), but the defensive change is robust
  against future codegen emitting inline control flow like
  `if (cond) {` or `} else {` on one line.
- Add a regression test covering those inline-brace patterns.
- Fix stale docstrings on ComponentMarker and cpp_main_section that
  still claimed "stack frame released on return" and described the
  IIFEs as "noinline". The IIFEs have no noinline attribute and rely
  on scope-based lifetime shortening rather than guaranteed frames.
This commit is contained in:
J. Nick Koston
2026-04-17 15:04:07 -05:00
parent 178f23a7aa
commit f82401a504
3 changed files with 63 additions and 13 deletions
+23 -12
View File
@@ -565,11 +565,18 @@ def _wrap_in_iifes(lines: list[str], max_statements: int) -> list[str]:
for line in lines:
chunk.append(line)
stripped = line.strip()
if stripped == "{":
depth += 1
elif stripped == "}":
depth -= 1
# Track brace depth by counting ``{`` and ``}`` characters per line
# rather than matching whole-line tokens. Today the only codegen
# that emits scope braces as separate statements is
# ``cg.with_local_variable()`` (standalone ``{`` / ``}`` lines),
# but counting is robust against future codegen that emits inline
# control-flow like ``if (cond) {`` or ``} else {`` on a single
# line. Multi-line statements (inline lambdas) carry balanced
# braces within one list entry so they contribute no net depth.
# Braces inside string literals would throw the count off, but
# esphome's generated main.cpp does not currently emit strings
# that contain unbalanced braces in main_statements.
depth += line.count("{") - line.count("}")
if depth == 0 and len(chunk) >= max_statements:
flush()
flush()
@@ -1069,13 +1076,17 @@ class EsphomeCore:
if not components:
return "\n".join(prefix) + "\n\n"
# Each component's block is wrapped in IIFE lambdas so its stack
# frame is released on return, bounding peak stack during setup().
# Large blocks are sub-split to cap single heavy components (e.g.
# sensor platforms with many filter registrations). "begin X" and
# "end X" marker comments bracket the IIFE so the generated
# main.cpp is easy to scan by component; a comment-only component
# gets a single "begin X" marker (no IIFE, no end marker).
# Each component's block is wrapped in an IIFE lambda that
# introduces a nested scope, shortening the lifetimes of
# temporaries so GCC can bound peak setup-time stack usage.
# The IIFE has no noinline attribute, so the compiler is free
# to inline the block when that produces smaller code without
# regressing peak stack. Large blocks are sub-split to cap
# single heavy components (e.g. sensor platforms with many
# filter registrations). "begin X" and "end X" marker comments
# bracket the IIFE so the generated main.cpp is easy to scan by
# component; a comment-only component gets a single "begin X"
# marker (no IIFE, no end marker).
pieces = list(prefix)
for name, body in components:
wrapped = _wrap_in_iifes(body, max_statements=50)
+6 -1
View File
@@ -437,7 +437,12 @@ class LineComment(Statement):
class ComponentMarker(Statement):
"""Sentinel marker recorded in main_statements when a component's
to_code begins emitting code. ``cpp_main_section`` consumes these
to bracket each component's IIFE with begin/end comment markers."""
to bracket each component's generated block with begin/end comment
markers and to wrap it in an IIFE scope. The IIFE introduces a
nested scope so GCC can shorten temporary lifetimes and help reduce
peak setup-time stack usage; the lambda has no ``noinline``
attribute, so the compiler may still inline the block when that
produces smaller code without regressing peak stack."""
__slots__ = ("name",)
+34
View File
@@ -934,6 +934,40 @@ def test_wrap_in_iifes_unbalanced_braces_fall_through() -> None:
assert [line for line in result if line in lines] == lines
def test_wrap_in_iifes_never_splits_inline_brace_lines() -> None:
# Defensive: if codegen ever emits control flow with braces on the
# same line (if/else/for), the depth tracker should keep the whole
# scoped block together even with aggressive max_statements.
lines = [
"before();",
"if (cond) {",
"then_branch();",
"} else {",
"for (;;) {",
"loop_body();",
"}",
"}",
"after();",
]
assert core._wrap_in_iifes(lines, max_statements=1) == [
"[]() {",
"before();",
"}();",
"[]() {",
"if (cond) {",
"then_branch();",
"} else {",
"for (;;) {",
"loop_body();",
"}",
"}",
"}();",
"[]() {",
"after();",
"}();",
]
def test_wrap_in_iifes_skips_comment_only_chunks() -> None:
# Components that emit only a ComponentMarker + config dump (no C++
# statements) should not be wrapped in an empty IIFE.