Merge remote-tracking branch 'origin/core-chunked-setup' into integration

This commit is contained in:
J. Nick Koston
2026-04-17 17:46:08 -05:00
11 changed files with 281 additions and 11 deletions
+1 -1
View File
@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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
+57 -6
View File
@@ -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):
+27 -2
View File
@@ -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 -1
View File
@@ -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
+168
View File
@@ -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("[]() {")