From e9ff4d3c4ec8242216001da42ffc71d31d0b182e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 11:47:35 -0600 Subject: [PATCH] handle static --- esphome/cpp_generator.py | 46 ++++++++++++++-- tests/unit_tests/test_lambda_dedup.py | 75 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2904cae796..4f91696ca1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -29,6 +29,11 @@ from esphome.yaml_util import ESPHomeDataBase _KEY_LAMBDA_DEDUP = "lambda_dedup" _KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" +# Regex patterns for static variable detection (compiled once) +_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE) +_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+") + class RawExpression(Expression): __slots__ = ("text",) @@ -716,20 +721,51 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) -def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str: +def _has_static_variables(code: str) -> bool: + """Check if code contains static variable definitions. + + Static variables in lambdas should not be deduplicated because each lambda + instance should have its own static variable state. + + Args: + code: The lambda body code to check + + Returns: + True if code contains static variable definitions + """ + # Remove C++ comments to avoid false positives + # Remove single-line comments (// ...) + code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code) + # Remove multi-line comments (/* ... */) + code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments) + + # Match: static + # But not: static_cast, static_assert, static_pointer_cast + return bool(_RE_STATIC_VARIABLE.search(code_no_comments)) + + +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None: """Get the shared function name for a lambda expression. If an identical lambda was already generated, returns the existing shared function name. Otherwise, creates a new shared function and returns its name. + Lambdas with static variables are not deduplicated to preserve their + independent state. + Args: lambda_expr: The lambda expression to deduplicate Returns: - The name of the shared function for this lambda (either existing or newly created) + The name of the shared function for this lambda (either existing or newly created), + or None if the lambda should not be deduplicated (e.g., contains static variables) """ # Create a unique key from the lambda content, parameters, and return type content = lambda_expr.content + + # Don't deduplicate lambdas with static variables - each instance needs its own state + if _has_static_variables(content): + return None param_str = str(lambda_expr.parameters) return_str = ( str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" @@ -832,13 +868,15 @@ async def process_lambda( # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). # Stateful lambdas cannot be shared as they capture different contexts. + # Lambdas with static variables are also not deduplicated to preserve independent state. if capture == "": lambda_expr = LambdaExpression( parts, parameters, capture, return_type, location ) func_name = _get_shared_lambda_name(lambda_expr) - # Return a shared function reference instead of inline lambda - return SharedFunctionLambdaExpression(func_name, parameters, return_type) + 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) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index ebc217f308..a0691e29dc 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -192,3 +192,78 @@ def test_stateful_lambdas_not_deduplicated() -> None: # We verify the lambda has a non-empty capture assert stateful_lambda.capture != "" assert stateful_lambda.capture == "=" + + +def test_static_variable_detection() -> None: + """Test detection of static variables in lambda code.""" + # Should detect static variables + assert cg._has_static_variables("static int counter = 0;") + assert cg._has_static_variables("static bool flag = false; return flag;") + assert cg._has_static_variables(" static float value = 1.0; ") + + # Should NOT detect static_cast, static_assert, etc. + assert not cg._has_static_variables("return static_cast(value);") + assert not cg._has_static_variables("static_assert(sizeof(int) == 4);") + assert not cg._has_static_variables("auto ptr = static_pointer_cast(bar);") + + # Should NOT detect in comments + assert not cg._has_static_variables("// static int x = 0;\nreturn 42;") + assert not cg._has_static_variables("/* static int y = 0; */ return 42;") + + # Should detect even with comments elsewhere + assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;") + + # Should NOT detect non-static code + assert not cg._has_static_variables("int counter = 0; return counter++;") + assert not cg._has_static_variables("return 42;") + + +def test_lambdas_with_static_not_deduplicated() -> None: + """Test that lambdas with static variables are not deduplicated.""" + # Two identical lambdas with static variables + lambda1 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should return None (not deduplicated) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is None + assert func_name2 is None + + +def test_lambdas_without_static_still_deduplicated() -> None: + """Test that lambdas without static variables are still deduplicated.""" + # Two identical lambdas WITHOUT static variables + lambda1 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should be deduplicated (same function name) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is not None + assert func_name2 is not None + assert func_name1 == func_name2