mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 18:16:03 +00:00
Merge remote-tracking branch 'origin/core-chunked-setup' into integration
This commit is contained in:
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.10
|
||||
rev: v0.15.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -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,6 +10,7 @@
|
||||
# pylint: disable=unused-import
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
ComponentMarker,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
|
||||
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
|
||||
int reset_pin_{-1};
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
spi_host_device_t interface_{SPI2_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -531,6 +531,42 @@ class Library:
|
||||
return self
|
||||
|
||||
|
||||
def _wrap_in_iifes(lines: list[str], max_statements: int) -> 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.
|
||||
|
||||
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 = 0
|
||||
|
||||
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. If depth ever
|
||||
# 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:
|
||||
flush()
|
||||
flush()
|
||||
return out
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class EsphomeCore:
|
||||
def __init__(self):
|
||||
@@ -1003,14 +1039,29 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def cpp_main_section(self):
|
||||
from esphome.cpp_generator import statement
|
||||
from esphome.cpp_generator import ComponentMarker, 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.
|
||||
prefix: list[str] = []
|
||||
components: list[list[str]] = []
|
||||
current = prefix
|
||||
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):
|
||||
current = []
|
||||
components.append(current)
|
||||
continue
|
||||
current.append(str(statement(exp)).rstrip())
|
||||
|
||||
if not components:
|
||||
return "\n".join(prefix) + "\n\n"
|
||||
|
||||
pieces = list(prefix)
|
||||
for body in components:
|
||||
pieces.extend(_wrap_in_iifes(body, max_statements=50))
|
||||
return "\n".join(pieces) + "\n\n"
|
||||
|
||||
@property
|
||||
def cpp_global_section(self):
|
||||
|
||||
@@ -434,6 +434,25 @@ class LineComment(Statement):
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
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. Safe because every statement either placement-
|
||||
news into static storage or mutates a file-scope global."""
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return f"// component-marker: {self.name}"
|
||||
|
||||
|
||||
class ProgmemAssignmentExpression(AssignmentExpression):
|
||||
__slots__ = ()
|
||||
|
||||
@@ -458,7 +477,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 +492,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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.11 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 6
|
||||
mosi_pin: 7
|
||||
miso_pin: 2
|
||||
cs_pin: 10
|
||||
interrupt_pin: 3
|
||||
reset_pin: 4
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
@@ -45,6 +45,11 @@ esphome:
|
||||
args:
|
||||
- response->status_code
|
||||
- body.c_str()
|
||||
- delay: 1s
|
||||
- logger.log:
|
||||
format: "After delay, body still: %s"
|
||||
args:
|
||||
- body.c_str()
|
||||
|
||||
http_request:
|
||||
useragent: esphome/tagreader
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from strategies import mac_addr_strings
|
||||
|
||||
from esphome import const, core
|
||||
from esphome.cpp_generator import ComponentMarker, RawStatement
|
||||
|
||||
|
||||
class TestHexInt:
|
||||
@@ -867,3 +868,170 @@ 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_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
|
||||
|
||||
|
||||
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