mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 18:06:34 +00:00
Compare commits
18 Commits
multi-inte
...
core-chunk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea417966d | ||
|
|
6446f309c1 | ||
|
|
093c34d4a4 | ||
|
|
3ab935bebb | ||
|
|
bb0067f517 | ||
|
|
550f6e7c72 | ||
|
|
5f2582efcd | ||
|
|
e26ce59797 | ||
|
|
9fa6d224c2 | ||
|
|
91b238aa97 | ||
|
|
00f08ba6ed | ||
|
|
f82401a504 | ||
|
|
178f23a7aa | ||
|
|
864d31aa65 | ||
|
|
936694af2c | ||
|
|
6a7c9af870 | ||
|
|
29dcf9fc51 | ||
|
|
6b67224286 |
@@ -569,6 +569,7 @@ def wrap_to_code(name, comp):
|
||||
|
||||
@functools.wraps(comp.to_code)
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.ComponentMarker(name))
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
if comp.config_schema is not None:
|
||||
conf_str = yaml_util.dump(conf)
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
# pylint: disable=unused-import
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
ComponentMarker,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
IIFEUnsafeStatement,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -87,7 +87,10 @@ async def to_code(config):
|
||||
config[CONF_REBOOT_TIMEOUT],
|
||||
config[CONF_BOOT_IS_GOOD_AFTER],
|
||||
)
|
||||
cg.add(RawExpression(f"if ({condition}) return"))
|
||||
# Wrap in IIFEUnsafeStatement so cpp_main_section emits this
|
||||
# component's block flat rather than inside an IIFE lambda —
|
||||
# the `return` must exit setup() itself, not just the lambda.
|
||||
cg.add(cg.IIFEUnsafeStatement(RawExpression(f"if ({condition}) return")))
|
||||
|
||||
CORE.data[CONF_SAFE_MODE] = {}
|
||||
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
@@ -531,6 +532,126 @@ class Library:
|
||||
return self
|
||||
|
||||
|
||||
# Cap on the number of statements in a single IIFE chunk when a
|
||||
# component's to_code body is sub-split. Picks a frame-size sweet spot
|
||||
# on esp32-s3 — large enough that most components fit in one chunk and
|
||||
# small enough that heavy sensor platforms (many filter registrations)
|
||||
# don't produce a chunk with a very large spill frame.
|
||||
IIFE_MAX_STATEMENTS = 50
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ComponentGroup:
|
||||
"""A contiguous run of statements emitted by one component's to_code."""
|
||||
|
||||
lines: list[str] = field(default_factory=list)
|
||||
# True when the group contains a statement that must affect setup()'s
|
||||
# own control flow (e.g. safe_mode's `return`). Emit the group flat,
|
||||
# bypassing IIFE wrapping entirely.
|
||||
unsafe: bool = False
|
||||
# True when the group contains a statement that may declare a
|
||||
# function-local whose lifetime extends past the current statement
|
||||
# (scope-brace RawStatement, direct RawExpression, typed
|
||||
# AssignmentExpression). Wrap the group in a single IIFE without
|
||||
# sub-splitting so the declaration and any later references stay
|
||||
# in the same lambda.
|
||||
no_split: bool = False
|
||||
|
||||
|
||||
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.
|
||||
|
||||
The detection is intentionally safety-biased: false negatives cause
|
||||
silent broken C++, false positives just keep a component in one
|
||||
slightly larger IIFE. Any ``cg.add(RawExpression(...))`` disables
|
||||
sub-splitting for its group regardless of whether the raw text
|
||||
actually references a local, because the chunker can't introspect
|
||||
arbitrary raw text."""
|
||||
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. A
|
||||
# RawExpression passed as a CallExpression argument does NOT land
|
||||
# here (its ExpressionStatement's .expression is the CallExpression),
|
||||
# so value-pass patterns like `var.set_program(RawExpression("&foo"))`
|
||||
# continue to sub-split normally.
|
||||
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, 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."""
|
||||
out: list[str] = []
|
||||
chunk: list[str] = []
|
||||
depth: int = 0
|
||||
# Once depth goes negative we stop trusting the brace count and
|
||||
# keep everything remaining in one final IIFE. A later ``{`` could
|
||||
# arithmetically bring depth back to 0, but by that point the brace
|
||||
# tracking is already unreliable — re-enabling mid-stream splits
|
||||
# could land between a declaration and its use.
|
||||
poisoned: bool = False
|
||||
|
||||
def flush() -> None:
|
||||
if not chunk:
|
||||
return
|
||||
if all(line.lstrip().startswith("//") for line in chunk):
|
||||
out.extend(chunk)
|
||||
else:
|
||||
out.append("[]() {")
|
||||
out.extend(chunk)
|
||||
out.append("}();")
|
||||
chunk.clear()
|
||||
|
||||
for line in lines:
|
||||
chunk.append(line)
|
||||
# Count { and } per line so inline control flow (e.g. `if (cond) {`)
|
||||
# and balanced inline lambdas are tracked correctly.
|
||||
depth += line.count("{") - line.count("}")
|
||||
if depth < 0:
|
||||
poisoned = True
|
||||
if (
|
||||
not poisoned
|
||||
and max_statements is not None
|
||||
and depth == 0
|
||||
and len(chunk) >= max_statements
|
||||
):
|
||||
flush()
|
||||
flush()
|
||||
return out
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class EsphomeCore:
|
||||
def __init__(self):
|
||||
@@ -1002,15 +1123,64 @@ class EsphomeCore:
|
||||
self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1
|
||||
|
||||
@property
|
||||
def cpp_main_section(self):
|
||||
from esphome.cpp_generator import statement
|
||||
def cpp_main_section(self) -> str:
|
||||
from esphome.cpp_generator import (
|
||||
ComponentMarker,
|
||||
IIFEUnsafeStatement,
|
||||
statement,
|
||||
)
|
||||
|
||||
main_code = []
|
||||
# Split main_statements at ComponentMarker sentinels and wrap each
|
||||
# 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.
|
||||
#
|
||||
# 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[_ComponentGroup] = []
|
||||
current: list[str] = prefix
|
||||
group: _ComponentGroup | None = None
|
||||
for exp in self.main_statements:
|
||||
text = str(statement(exp))
|
||||
text = text.rstrip()
|
||||
main_code.append(text)
|
||||
return "\n".join(main_code) + "\n\n"
|
||||
if isinstance(exp, ComponentMarker):
|
||||
group = _ComponentGroup()
|
||||
components.append(group)
|
||||
current = group.lines
|
||||
continue
|
||||
if group is not None:
|
||||
if isinstance(exp, IIFEUnsafeStatement):
|
||||
group.unsafe = True
|
||||
if _emits_bare_local(exp):
|
||||
group.no_split = True
|
||||
current.append(str(statement(exp)).rstrip())
|
||||
|
||||
if not components:
|
||||
return "\n".join(prefix) + "\n\n"
|
||||
|
||||
pieces: list[str] = list(prefix)
|
||||
for g in components:
|
||||
if g.unsafe:
|
||||
pieces.extend(g.lines)
|
||||
else:
|
||||
cap = None if g.no_split else IIFE_MAX_STATEMENTS
|
||||
pieces.extend(_wrap_in_iifes(g.lines, max_statements=cap))
|
||||
return "\n".join(pieces) + "\n\n"
|
||||
|
||||
@property
|
||||
def cpp_global_section(self):
|
||||
|
||||
@@ -434,6 +434,48 @@ class LineComment(Statement):
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
class IIFEUnsafeStatement(Statement):
|
||||
"""Statement that must not be placed inside an IIFE lambda when
|
||||
``cpp_main_section`` chunks ``setup()``. Causes the containing
|
||||
component's block to be emitted flat (no IIFE), so constructs that
|
||||
rely on exiting ``setup()`` directly — e.g. safe_mode's
|
||||
``if (should_enter_safe_mode(...)) return;`` — still work.
|
||||
|
||||
Accepts either a ``Statement`` or a bare ``Expression``; bare
|
||||
expressions are wrapped so they terminate with a semicolon."""
|
||||
|
||||
__slots__ = ("inner",)
|
||||
|
||||
def __init__(self, inner: Expression | Statement) -> None:
|
||||
self.inner = inner
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(statement(self.inner))
|
||||
|
||||
|
||||
class ComponentMarker(Statement):
|
||||
"""Chunking-boundary sentinel. ``cpp_main_section`` wraps the
|
||||
statements between two markers in an IIFE to shorten temporary
|
||||
lifetimes and bound peak setup-time stack. Emits no C++ output.
|
||||
|
||||
Grouping is best-effort: ``flush_tasks`` can interleave coroutines
|
||||
on ``await``, so a component's later statements may land in another
|
||||
component's chunk. This is safe for the dominant codegen patterns
|
||||
(placement-new into static storage, assignment to a file-scope
|
||||
global); patterns that depend on function-local state within the
|
||||
IIFE scope (cg.variable, with_local_variable, raw bare locals)
|
||||
are kept together by the bare-local detection in cpp_main_section
|
||||
so they aren't split across sibling lambdas."""
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"// component-marker: {self.name}"
|
||||
|
||||
|
||||
class ProgmemAssignmentExpression(AssignmentExpression):
|
||||
__slots__ = ()
|
||||
|
||||
@@ -458,7 +500,13 @@ def progmem_array(id_, rhs) -> "MockObj":
|
||||
rhs = safe_exp(rhs)
|
||||
obj = MockObj(id_, ".")
|
||||
assignment = ProgmemAssignmentExpression(id_.type, id_, rhs)
|
||||
CORE.add(assignment)
|
||||
# Emit at file scope, not inside setup(). setup() is split into
|
||||
# per-component IIFE lambdas; a function-local static declared in one
|
||||
# lambda is not visible to statements in sibling lambdas that
|
||||
# reference the same shared table (e.g. two lights sharing a gamma
|
||||
# lookup). File-scope static constexpr is semantically identical for
|
||||
# read-only lookup tables.
|
||||
CORE.add_global(assignment)
|
||||
CORE.register_variable(id_, obj)
|
||||
return obj
|
||||
|
||||
@@ -467,7 +515,7 @@ def static_const_array(id_, rhs) -> "MockObj":
|
||||
rhs = safe_exp(rhs)
|
||||
obj = MockObj(id_, ".")
|
||||
assignment = StaticConstAssignmentExpression(id_.type, id_, rhs)
|
||||
CORE.add(assignment)
|
||||
CORE.add_global(assignment)
|
||||
CORE.register_variable(id_, obj)
|
||||
return obj
|
||||
|
||||
@@ -490,10 +538,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
|
||||
|
||||
@@ -7,6 +7,16 @@ import pytest
|
||||
from strategies import mac_addr_strings
|
||||
|
||||
from esphome import const, core
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
CallExpression,
|
||||
ComponentMarker,
|
||||
ExpressionStatement,
|
||||
IIFEUnsafeStatement,
|
||||
MockObj,
|
||||
RawExpression,
|
||||
RawStatement,
|
||||
)
|
||||
|
||||
|
||||
class TestHexInt:
|
||||
@@ -867,3 +877,314 @@ class TestEsphomeCore:
|
||||
mock_enable.assert_called_once_with("Wire")
|
||||
|
||||
assert "Wire" in target.platformio_libraries
|
||||
|
||||
|
||||
def test_wrap_in_iifes_empty_input() -> None:
|
||||
assert core._wrap_in_iifes([], max_statements=10) == []
|
||||
|
||||
|
||||
def test_wrap_in_iifes_fewer_lines_than_limit() -> None:
|
||||
lines = ["a();", "b();", "c();"]
|
||||
assert core._wrap_in_iifes(lines, max_statements=10) == [
|
||||
"[]() {",
|
||||
"a();",
|
||||
"b();",
|
||||
"c();",
|
||||
"}();",
|
||||
]
|
||||
|
||||
|
||||
def test_wrap_in_iifes_splits_at_max_statements() -> None:
|
||||
lines = [f"s{i}();" for i in range(5)]
|
||||
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_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_iifes(lines, max_statements=2) == [
|
||||
"[]() {",
|
||||
"a();",
|
||||
"{",
|
||||
"inner();",
|
||||
"}",
|
||||
"}();",
|
||||
"[]() {",
|
||||
"b();",
|
||||
"}();",
|
||||
]
|
||||
|
||||
|
||||
def test_wrap_in_iifes_nested_braces() -> None:
|
||||
lines = ["{", "{", "deep();", "}", "}", "after();"]
|
||||
assert core._wrap_in_iifes(lines, max_statements=1) == [
|
||||
"[]() {",
|
||||
"{",
|
||||
"{",
|
||||
"deep();",
|
||||
"}",
|
||||
"}",
|
||||
"}();",
|
||||
"[]() {",
|
||||
"after();",
|
||||
"}();",
|
||||
]
|
||||
|
||||
|
||||
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_iifes(lines, max_statements=1)
|
||||
assert result[0] == "[]() {"
|
||||
assert result[-1] == "}();"
|
||||
assert [line for line in result if line in lines] == lines
|
||||
|
||||
|
||||
def test_wrap_in_iifes_negative_depth_stays_poisoned() -> None:
|
||||
# Once depth goes negative the brace tracker is unreliable; a later
|
||||
# arithmetic return to depth 0 must not re-enable splitting. Here
|
||||
# "}" drives depth to -1 immediately, then "{" later brings depth
|
||||
# back to 0 arithmetically. With max=1 every statement would flush
|
||||
# if splitting were still enabled — assert everything emits as one
|
||||
# IIFE because the poisoned flag stayed set.
|
||||
lines = ["}", "a();", "{", "b();", "}", "c();"]
|
||||
result = core._wrap_in_iifes(lines, max_statements=1)
|
||||
assert sum(1 for line in result if line == "[]() {") == 1
|
||||
assert result[0] == "[]() {"
|
||||
assert result[-1] == "}();"
|
||||
|
||||
|
||||
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:
|
||||
# A chunk with no C++ statements (only comments, e.g. a component's
|
||||
# config dump) should be emitted verbatim without a no-op IIFE.
|
||||
lines = ["// sha256:", "// {}"]
|
||||
assert core._wrap_in_iifes(lines, max_statements=50) == lines
|
||||
|
||||
|
||||
def test_wrap_in_iifes_ignores_iife_pattern_in_comment() -> None:
|
||||
# A comment whose text mentions "[]()" (e.g. a YAML dump of a
|
||||
# lambda) must not fool the comment-only detector into wrapping.
|
||||
lines = [
|
||||
"// on_value:",
|
||||
"// - !lambda |-",
|
||||
"// return []() { return 5; };",
|
||||
]
|
||||
assert core._wrap_in_iifes(lines, max_statements=50) == lines
|
||||
|
||||
|
||||
def test_cpp_main_section_no_components_emits_flat() -> None:
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [RawStatement("a();"), RawStatement("b();")]
|
||||
out = target.cpp_main_section
|
||||
assert "[]() {" not in out
|
||||
assert "a();" in out
|
||||
assert "b();" in out
|
||||
|
||||
|
||||
def test_cpp_main_section_component_marker_wraps_in_iife() -> None:
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [
|
||||
ComponentMarker("logger"),
|
||||
RawStatement("new_logger();"),
|
||||
ComponentMarker("wifi"),
|
||||
RawStatement("new_wifi();"),
|
||||
]
|
||||
out = target.cpp_main_section
|
||||
# One IIFE per component that emits C++ statements.
|
||||
assert out.count("[]() {") == 2
|
||||
assert out.count("}();") == 2
|
||||
# ComponentMarker produces no output of its own.
|
||||
assert "component-marker" not in out
|
||||
|
||||
|
||||
_CHUNK_COUNT = 3 # number of sub-chunks to force when testing splitting
|
||||
# Statement count that produces exactly _CHUNK_COUNT IIFEs when sub-split
|
||||
# at IIFE_MAX_STATEMENTS (full chunks at the cap, no partial).
|
||||
_STATEMENTS_OVER_CAP = core.IIFE_MAX_STATEMENTS * _CHUNK_COUNT
|
||||
|
||||
|
||||
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(_STATEMENTS_OVER_CAP))
|
||||
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(_STATEMENTS_OVER_CAP)
|
||||
)
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == _CHUNK_COUNT
|
||||
|
||||
|
||||
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(_STATEMENTS_OVER_CAP)
|
||||
)
|
||||
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")]
|
||||
stmts.extend(
|
||||
ExpressionStatement(
|
||||
CallExpression(MockObj(f"var_{i}.set_program"), RawExpression("&foo"))
|
||||
)
|
||||
for i in range(_STATEMENTS_OVER_CAP)
|
||||
)
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == _CHUNK_COUNT
|
||||
|
||||
|
||||
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(_STATEMENTS_OVER_CAP))
|
||||
target.main_statements = stmts
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 1
|
||||
|
||||
|
||||
def test_cpp_main_section_iife_unsafe_wins_over_no_split() -> None:
|
||||
# A group that triggers BOTH flags (IIFEUnsafeStatement present AND
|
||||
# a bare-local emission) must still be emitted flat — the unsafe
|
||||
# flag wins because a `return` inside any IIFE, even a single big
|
||||
# one, only exits the lambda.
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [
|
||||
ComponentMarker("safe_mode_with_local"),
|
||||
RawStatement("{"), # would trigger no_split
|
||||
RawStatement("new_foo();"),
|
||||
RawStatement("}"),
|
||||
IIFEUnsafeStatement(RawExpression("if (cond) return")),
|
||||
]
|
||||
out = target.cpp_main_section
|
||||
assert "[]() {" not in out
|
||||
assert "if (cond) return;" in out
|
||||
|
||||
|
||||
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
|
||||
# IIFE would only exit the lambda, not setup().
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [
|
||||
ComponentMarker("logger"),
|
||||
RawStatement("new_logger();"),
|
||||
ComponentMarker("safe_mode"),
|
||||
RawStatement("new_safe_mode();"),
|
||||
IIFEUnsafeStatement(RawExpression("if (entering) return")),
|
||||
ComponentMarker("sensor"),
|
||||
RawStatement("new_sensor();"),
|
||||
]
|
||||
out = target.cpp_main_section
|
||||
# logger and sensor wrapped; safe_mode flat.
|
||||
assert out.count("[]() {") == 2
|
||||
# safe_mode's statements appear at top level, not indented in a lambda.
|
||||
assert "new_safe_mode();" in out
|
||||
assert "if (entering) return;" in out
|
||||
# The IIFEUnsafeStatement wrapper picks up the trailing semicolon
|
||||
# via statement() when inner is a bare Expression.
|
||||
assert "if (entering) return\n" not in out
|
||||
|
||||
|
||||
def test_cpp_main_section_comment_only_component_omits_iife() -> None:
|
||||
# A component that emits only a ComponentMarker (no statements) adds
|
||||
# nothing to the generated output. A neighboring component with
|
||||
# actual code still gets its own IIFE.
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [
|
||||
ComponentMarker("sha256"),
|
||||
ComponentMarker("wifi"),
|
||||
RawStatement("new_wifi();"),
|
||||
]
|
||||
out = target.cpp_main_section
|
||||
assert out.count("[]() {") == 1
|
||||
assert "new_wifi();" in out
|
||||
|
||||
|
||||
def test_cpp_main_section_prefix_statements_stay_outside_iife() -> None:
|
||||
target = core.EsphomeCore()
|
||||
target.main_statements = [
|
||||
RawStatement("prefix();"),
|
||||
ComponentMarker("c"),
|
||||
RawStatement("body();"),
|
||||
]
|
||||
out = target.cpp_main_section
|
||||
assert out.index("prefix();") < out.index("[]() {")
|
||||
|
||||
Reference in New Issue
Block a user