From cc1b547ad2e0e4c006a78994d3e976a8477a9cb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 22:27:23 -0600 Subject: [PATCH 1/4] der dupe lam --- esphome/__main__.py | 5 ++ esphome/cpp_generator.py | 138 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f..b714bd4a65 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -497,6 +497,11 @@ def generate_cpp_contents(config: ConfigType) -> None: CORE.flush_tasks() + # Flush deferred lambda deduplication declarations after all variables are declared + from esphome import cpp_generator as cg + + cg.flush_lambda_dedup_declarations() + def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5b..046db6ddca 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -24,6 +24,10 @@ from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase +# Keys for lambda deduplication storage in CORE.data +_KEY_LAMBDA_DEDUP = "lambda_dedup" +_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" + class RawExpression(Expression): __slots__ = ("text",) @@ -188,7 +192,7 @@ class LambdaExpression(Expression): def __init__( self, parts, parameters, capture: str = "=", return_type=None, source=None - ): + ) -> None: self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -197,16 +201,21 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def __str__(self): + def _format_body(self) -> str: + """Format the lambda body with source directive and content.""" + body = "" + if self.source is not None: + body += f"{self.source.as_line_directive}\n" + body += self.content + return body + + def __str__(self) -> str: # Stateless lambdas (empty capture) implicitly convert to function pointers # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += " {\n" - if self.source is not None: - cpp += f"{self.source.as_line_directive}\n" - cpp += f"{self.content}\n}}" + cpp += f" {{\n{self._format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -214,6 +223,37 @@ class LambdaExpression(Expression): return "".join(str(part) for part in self.parts) +class SharedFunctionLambdaExpression(LambdaExpression): + """A lambda expression that references a shared deduplicated function. + + This class wraps a function pointer but maintains the LambdaExpression + interface so calling code works unchanged. + """ + + __slots__ = ("_func_name",) + + def __init__( + self, + func_name: str, + parameters: TemplateArgsType, + return_type: SafeExpType | None = None, + ) -> None: + # Initialize parent with empty parts since we're just a function reference + super().__init__( + [], parameters, capture="", return_type=return_type, source=None + ) + self._func_name = func_name + + def __str__(self) -> str: + # Just return the function name - it's already a function pointer + return self._func_name + + @property + def content(self) -> str: + # No content, just a function reference + return "" + + # pylint: disable=abstract-method class Literal(Expression, metaclass=abc.ABCMeta): __slots__ = () @@ -583,6 +623,24 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) +def flush_lambda_dedup_declarations(): + """Flush all deferred lambda deduplication declarations to global scope. + + This must be called after all component code generation is complete + to ensure all referenced variables are declared before the shared + lambda functions that use them. + """ + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + return + + declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] + for func_declaration in declarations: + add_global(RawStatement(func_declaration)) + + # Clear the list so we don't add them again + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + + def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. @@ -656,6 +714,62 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) +def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: + """Try to deduplicate a lambda expression. + + If an identical lambda was already generated, returns the name of the + shared function. Otherwise, creates a new shared function and stores it. + + Args: + lambda_expr: The lambda expression to potentially deduplicate + + Returns: + The name of the shared function if this lambda should be deduplicated, + None if this is the first occurrence (caller should use original lambda) + """ + # Create a unique key from the lambda content, parameters, and return type + content = lambda_expr.content + param_str = str(lambda_expr.parameters) + return_str = ( + str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" + ) + + # Use tuple of (content, params, return_type) as key + lambda_key = (content, param_str, return_str) + + # Initialize deduplication storage in CORE.data if not exists + if _KEY_LAMBDA_DEDUP not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP] = {} + + lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] + + # Check if we've seen this lambda before + if lambda_key in lambda_cache: + # Return name of existing shared function + return lambda_cache[lambda_key] + + # First occurrence - create a shared function + # Use the cache size as the function number + func_name = f"shared_lambda_{len(lambda_cache)}" + + # Build the function declaration using lambda's body formatting + func_declaration = ( + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr._format_body()}\n}}" + ) + + # Store the declaration to be added later (after all variable declarations) + # We can't add it immediately because it might reference variables not yet declared + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS].append(func_declaration) + + # Store in cache + lambda_cache[lambda_key] = func_name + + # Return the function name (this is the first occurrence, but we still generate shared function) + return func_name + + async def process_lambda( value: Lambda, parameters: TemplateArgsType, @@ -713,6 +827,18 @@ async def process_lambda( location.line += value.content_offset else: location = None + + # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). + # Stateful lambdas cannot be shared as they capture different contexts. + if capture == "": + lambda_expr = LambdaExpression( + parts, parameters, capture, return_type, location + ) + func_name = _try_deduplicate_lambda(lambda_expr) + if func_name is not None: + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) + return LambdaExpression(parts, parameters, capture, return_type, location) From 6ade327cde8f12bd4b19bc60c8ebf954f2ae2aca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:05:27 -0600 Subject: [PATCH 2/4] update tests --- tests/component_tests/text/test_text.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee7..5349a5d683 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,13 +58,21 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ # Given + from esphome.core import CORE # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + # Get both global and main sections to find the shared lambda definition + full_cpp = CORE.cpp_global_section + main_cpp + # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp - assert 'return std::string{"Hello"};' in main_cpp + # Lambda is deduplicated into a shared function (reference in main section) + assert "it_4->set_template(shared_lambda_" in main_cpp + # Lambda body should be in the code somewhere + assert 'return std::string{"Hello"};' in full_cpp + # Verify the shared lambda function is defined (in global section) + assert "esphome::optional shared_lambda_" in full_cpp From 11de9486984e57d5964be1c2249e7879a7bbc8ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:12:36 -0600 Subject: [PATCH 3/4] proper codegen --- esphome/__main__.py | 5 ----- esphome/cpp_generator.py | 9 +++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b714bd4a65..b0c081a34f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -497,11 +497,6 @@ def generate_cpp_contents(config: ConfigType) -> None: CORE.flush_tasks() - # Flush deferred lambda deduplication declarations after all variables are declared - from esphome import cpp_generator as cg - - cg.flush_lambda_dedup_declarations() - def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 046db6ddca..4f64b29f80 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,6 +19,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict @@ -623,10 +624,11 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) -def flush_lambda_dedup_declarations(): +@coroutine_with_priority(CoroPriority.FINAL) +async def flush_lambda_dedup_declarations() -> None: """Flush all deferred lambda deduplication declarations to global scope. - This must be called after all component code generation is complete + This is a coroutine that runs with FINAL priority (after all components) to ensure all referenced variables are declared before the shared lambda functions that use them. """ @@ -740,6 +742,9 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Initialize deduplication storage in CORE.data if not exists if _KEY_LAMBDA_DEDUP not in CORE.data: CORE.data[_KEY_LAMBDA_DEDUP] = {} + # Register the flush job to run after all components (FINAL priority) + # This ensures all variables are declared before shared lambda functions + CORE.add_job(flush_lambda_dedup_declarations) lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] From b7c105125e0da72288df948a8bda5306c170cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:13:43 -0600 Subject: [PATCH 4/4] proper codegen --- esphome/cpp_generator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4f64b29f80..5a8685dd0a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -764,9 +764,7 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Store the declaration to be added later (after all variable declarations) # We can't add it immediately because it might reference variables not yet declared - if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: - CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] - CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS].append(func_declaration) + CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration) # Store in cache lambda_cache[lambda_key] = func_name