mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 10:38:38 +00:00
[core] Detect bare-local emission and disable sub-split for those groups
A component's group is wrapped in a single IIFE with no sub-splitting when its to_code emits any of: scope-brace RawStatement, direct RawExpression via cg.add (raw bare-local or field-assignment like 'tz.field = x'), or typed AssignmentExpression (cg.variable). Detection is content-aware so entity_helpers' inline-comment RawStatements and RawExpression-as-CallExpression-arg don't false-positive. Adds 4 regression tests covering each detection path and the non-triggering inverse cases.
This commit is contained in:
+73
-12
@@ -531,11 +531,47 @@ class Library:
|
||||
return self
|
||||
|
||||
|
||||
def _wrap_in_iifes(lines: list[str], max_statements: int) -> list[str]:
|
||||
def _emits_bare_local(exp: "Statement") -> bool:
|
||||
"""True if ``exp`` emits a scope brace or bare-raw construct that may
|
||||
declare a function-local whose lifetime extends past the current
|
||||
statement. Components that emit any such statement must not be
|
||||
sub-split — later references within the same ``to_code`` would land
|
||||
in a different IIFE and fail to compile."""
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
ExpressionStatement,
|
||||
RawExpression,
|
||||
RawStatement,
|
||||
)
|
||||
|
||||
# Scope braces from cg.with_local_variable() or inline scope blocks
|
||||
# (e.g. time's tz pattern). Content-aware so RawStatements emitted
|
||||
# for "call(); // comment" (entity_helpers) don't false-positive.
|
||||
if isinstance(exp, RawStatement) and str(exp).strip() in ("{", "}"):
|
||||
return True
|
||||
# cg.add(RawExpression(...)) — bare raw text, e.g.
|
||||
# `time::ParsedTimezone tz{}` or `tz.field = ...`. CORE.add wraps
|
||||
# a passed Expression in an ExpressionStatement; when the inner is
|
||||
# a RawExpression the author is emitting uninterpreted text that
|
||||
# may reference a local declared elsewhere in the same block.
|
||||
if isinstance(exp, ExpressionStatement) and isinstance(
|
||||
exp.expression, RawExpression
|
||||
):
|
||||
return True
|
||||
# cg.variable(id, rhs) — emits ``Type id = rhs;`` as a function-local.
|
||||
return (
|
||||
isinstance(exp, ExpressionStatement)
|
||||
and isinstance(exp.expression, AssignmentExpression)
|
||||
and exp.expression.type is not None
|
||||
)
|
||||
|
||||
|
||||
def _wrap_in_iifes(lines: list[str], max_statements: int | None) -> list[str]:
|
||||
"""Wrap ``lines`` in ``[]() {...}();`` IIFEs of up to ``max_statements``
|
||||
each. Never splits inside a brace-balanced block (e.g. the ``{`` / ``}``
|
||||
pair from ``cg.with_local_variable()``), so an IIFE may exceed the cap
|
||||
when a block straddles it. Comment-only chunks pass through verbatim.
|
||||
each, or in a single IIFE when ``max_statements`` is ``None``. Never
|
||||
splits inside a brace-balanced block (e.g. the ``{`` / ``}`` pair from
|
||||
``cg.with_local_variable()``), so an IIFE may exceed the cap when a
|
||||
block straddles it. Comment-only chunks pass through verbatim.
|
||||
|
||||
No ``noinline`` attribute — GCC's inliner re-folds small chunks freely,
|
||||
keeping flash small without regressing peak stack."""
|
||||
@@ -561,7 +597,7 @@ def _wrap_in_iifes(lines: list[str], max_statements: int) -> list[str]:
|
||||
# goes negative (unbalanced input) we never return to 0 and the
|
||||
# rest falls through into a single final IIFE — safe fallback.
|
||||
depth += line.count("{") - line.count("}")
|
||||
if depth == 0 and len(chunk) >= max_statements:
|
||||
if max_statements is not None and depth == 0 and len(chunk) >= max_statements:
|
||||
flush()
|
||||
flush()
|
||||
return out
|
||||
@@ -1049,32 +1085,57 @@ class EsphomeCore:
|
||||
# component's group in an IIFE, sub-splitting at 50 statements so
|
||||
# a single heavy component (e.g. a sensor platform with many
|
||||
# filter registrations) can't blow the peak chunk frame.
|
||||
# Components that contain an IIFEUnsafeStatement (e.g. safe_mode's
|
||||
# setup-scope `return`) are emitted flat so the statement affects
|
||||
# setup()'s control flow, not the lambda's.
|
||||
#
|
||||
# Two escape hatches control whether a component's group is safe
|
||||
# to sub-split:
|
||||
#
|
||||
# - IIFEUnsafeStatement (e.g. safe_mode's setup-scope `return`):
|
||||
# the whole group must stay at setup() scope so the statement
|
||||
# affects setup()'s control flow, not the lambda's. Emit flat.
|
||||
#
|
||||
# - Any statement that may declare a function-local: a bare
|
||||
# ``{`` / ``}`` RawStatement (from ``cg.with_local_variable``,
|
||||
# time's inline tz block, etc.), a direct ``RawExpression``
|
||||
# passed to ``cg.add`` (raw bare-local or field-assignment
|
||||
# emission like ``time::ParsedTimezone tz`` followed by
|
||||
# ``tz.field = ...``), or a typed ``AssignmentExpression``
|
||||
# (``cg.variable`` emitting ``Type id = rhs;``). Each signals
|
||||
# "this group's body may contain bare names whose scope is the
|
||||
# enclosing IIFE"; wrap the whole group in one IIFE with no
|
||||
# sub-split so the declaration and any later references stay
|
||||
# together.
|
||||
prefix: list[str] = []
|
||||
components: list[tuple[list[str], list[bool]]] = []
|
||||
components: list[tuple[list[str], list[bool], list[bool]]] = []
|
||||
current: list[str] = prefix
|
||||
unsafe_flag: list[bool] = [False] # unused for prefix
|
||||
no_split_flag: list[bool] = [False] # unused for prefix
|
||||
for exp in self.main_statements:
|
||||
if isinstance(exp, ComponentMarker):
|
||||
current = []
|
||||
unsafe_flag = [False]
|
||||
components.append((current, unsafe_flag))
|
||||
no_split_flag = [False]
|
||||
components.append((current, unsafe_flag, no_split_flag))
|
||||
continue
|
||||
if isinstance(exp, IIFEUnsafeStatement):
|
||||
unsafe_flag[0] = True
|
||||
if _emits_bare_local(exp):
|
||||
no_split_flag[0] = True
|
||||
current.append(str(statement(exp)).rstrip())
|
||||
|
||||
if not components:
|
||||
return "\n".join(prefix) + "\n\n"
|
||||
|
||||
pieces: list[str] = list(prefix)
|
||||
for body, group_unsafe in components:
|
||||
for body, group_unsafe, group_no_split in components:
|
||||
if group_unsafe[0]:
|
||||
pieces.extend(body)
|
||||
else:
|
||||
pieces.extend(_wrap_in_iifes(body, max_statements=50))
|
||||
# A group containing raw scope braces or bare-local
|
||||
# declarations must stay in one IIFE so the declarations
|
||||
# remain visible to subsequent uses. ``max_statements=None``
|
||||
# disables sub-splitting for the group.
|
||||
cap = None if group_no_split[0] else 50
|
||||
pieces.extend(_wrap_in_iifes(body, max_statements=cap))
|
||||
return "\n".join(pieces) + "\n\n"
|
||||
|
||||
@property
|
||||
|
||||
@@ -534,10 +534,15 @@ def literal(name: str) -> "MockObj":
|
||||
|
||||
|
||||
def variable(
|
||||
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
|
||||
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register: bool = True
|
||||
) -> "MockObj":
|
||||
"""Declare a new variable, not pointer type, in the code generation.
|
||||
|
||||
Emits a function-local declaration ``Type id = rhs;`` inside setup().
|
||||
``cpp_main_section`` detects typed ``AssignmentExpression`` and
|
||||
disables sub-chunking for the component's group, so later references
|
||||
to the local within the same ``to_code`` stay visible.
|
||||
|
||||
:param id_: The ID used to declare the variable.
|
||||
:param rhs: The expression to place on the right hand side of the assignment.
|
||||
:param type_: Manually define a type for the variable, only use this when it's not possible
|
||||
|
||||
@@ -8,8 +8,11 @@ from strategies import mac_addr_strings
|
||||
|
||||
from esphome import const, core
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
ComponentMarker,
|
||||
ExpressionStatement,
|
||||
IIFEUnsafeStatement,
|
||||
MockObj,
|
||||
RawExpression,
|
||||
RawStatement,
|
||||
)
|
||||
@@ -1016,6 +1019,83 @@ def test_cpp_main_section_component_marker_wraps_in_iife() -> None:
|
||||
assert "component-marker" not in out
|
||||
|
||||
|
||||
def test_cpp_main_section_scope_brace_raw_disables_sub_split() -> None:
|
||||
# A group containing scope-brace RawStatements (e.g. `{` / `}` from
|
||||
# with_local_variable) must stay in one IIFE regardless of size so
|
||||
# the scope bounds and any locals between them stay together.
|
||||
target = core.EsphomeCore()
|
||||
stmts: list = [ComponentMarker("wifi"), RawStatement("{")]
|
||||
stmts.extend(RawStatement(f"s{i}();") for i in range(100))
|
||||
stmts.append(RawStatement("}"))
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 1
|
||||
assert out.count("}();") == 1
|
||||
|
||||
|
||||
def test_cpp_main_section_inline_comment_raw_still_sub_splits() -> None:
|
||||
# entity_helpers emits `call(); // flags` as RawStatement for inline
|
||||
# comments. Those shouldn't flag the group as scope-using — the
|
||||
# content-aware check only triggers on bare `{` / `}`.
|
||||
target = core.EsphomeCore()
|
||||
stmts: list = [ComponentMarker("sensor")]
|
||||
stmts.extend(RawStatement(f"s{i}(); // flags") for i in range(120))
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
# 120 statements / 50-cap = 3 sub-chunks expected.
|
||||
assert out.count("[]() {") == 3
|
||||
|
||||
|
||||
def test_cpp_main_section_raw_expression_disables_sub_split() -> None:
|
||||
# cg.add(RawExpression(...)) — e.g. `time::ParsedTimezone tz` followed
|
||||
# by `tz.field = ...` — is raw bare text that may reference a local
|
||||
# declared elsewhere in the same group. Keep the group in one IIFE.
|
||||
target = core.EsphomeCore()
|
||||
stmts: list = [
|
||||
ComponentMarker("time"),
|
||||
ExpressionStatement(RawExpression("time::ParsedTimezone tz{}")),
|
||||
]
|
||||
stmts.extend(
|
||||
ExpressionStatement(RawExpression(f"tz.field_{i} = {i}")) for i in range(100)
|
||||
)
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 1
|
||||
|
||||
|
||||
def test_cpp_main_section_raw_expression_as_call_arg_still_sub_splits() -> None:
|
||||
# RawExpression passed as an argument to a method call (e.g.
|
||||
# `var.set_program(RawExpression("&foo"))`) produces
|
||||
# `ExpressionStatement(CallExpression(..., RawExpression))` — the
|
||||
# outer expression is a CallExpression, not a RawExpression, so
|
||||
# the group is still sub-splittable.
|
||||
target = core.EsphomeCore()
|
||||
stmts: list = [ComponentMarker("rp2040_pio_led_strip")]
|
||||
# Emit >50 plain statements with one being an ExpressionStatement
|
||||
# wrapping a non-RawExpression inner (RawStatement for simplicity
|
||||
# here — the real codegen wraps a CallExpression).
|
||||
stmts.extend(RawStatement(f"s{i}();") for i in range(120))
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 3
|
||||
|
||||
|
||||
def test_cpp_main_section_typed_assignment_disables_sub_split() -> None:
|
||||
# cg.variable(id, rhs) emits `Type id = rhs;` via
|
||||
# ExpressionStatement(AssignmentExpression(type=..., ...)). That's a
|
||||
# function-local whose name must stay visible across all uses in
|
||||
# the component — no sub-split.
|
||||
target = core.EsphomeCore()
|
||||
typed_assign = ExpressionStatement(
|
||||
AssignmentExpression(MockObj("int"), "", MockObj("x"), MockObj("42"))
|
||||
)
|
||||
stmts: list = [ComponentMarker("custom"), typed_assign]
|
||||
stmts.extend(RawStatement(f"use_x_{i}();") for i in range(100))
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 1
|
||||
|
||||
|
||||
def test_cpp_main_section_iife_unsafe_statement_emits_component_flat() -> None:
|
||||
# A component that emits IIFEUnsafeStatement (e.g. safe_mode with
|
||||
# `if (...) return;`) must be emitted flat — a `return` inside an
|
||||
|
||||
Reference in New Issue
Block a user