From ddf86b4e77820e1077225dfb6cec00b631e64309 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:31:55 -0700 Subject: [PATCH 1/6] wip --- esphome/core/automation.h | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 0512752d50..fe2daa4f80 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -31,7 +31,16 @@ template class TemplatableValue { new (&this->value_) T(std::move(value)); } - template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + // For stateless lambdas (convertible to function pointer): use function pointer + template::value && std::is_convertible::value, int> = 0> + TemplatableValue(F f) : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas (not convertible to function pointer): use std::function + template::value && !std::is_convertible::value, int> = 0> + TemplatableValue(F f) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } @@ -41,6 +50,8 @@ template class TemplatableValue { new (&this->value_) T(other.value_); } else if (type_ == LAMBDA) { this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } @@ -51,6 +62,8 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -78,13 +91,17 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { delete this->f_; } + // STATELESS_LAMBDA needs no cleanup (function pointer, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { + if (this->type_ == STATELESS_LAMBDA) { + return this->stateless_f_(x...); // Direct function pointer call + } if (this->type_ == LAMBDA) { - return (*this->f_)(x...); + return (*this->f_)(x...); // std::function call } // return value also when none return this->type_ == VALUE ? this->value_ : T{}; @@ -109,11 +126,13 @@ template class TemplatableValue { NONE, VALUE, LAMBDA, + STATELESS_LAMBDA, } type_; union { T value_; std::function *f_; + T (*stateless_f_)(X...); }; }; From 077bd624f08712355b6998b8269ed153272e88e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:32:59 -0700 Subject: [PATCH 2/6] remove --- esphome/automation.py | 45 ++-------------- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/binary_sensor/filter.h | 15 ------ esphome/components/logger/__init__.py | 10 ++-- esphome/components/sensor/__init__.py | 3 +- esphome/components/sensor/filter.h | 15 ------ esphome/components/text_sensor/__init__.py | 3 +- esphome/components/text_sensor/filter.h | 15 ------ esphome/core/base_automation.h | 25 --------- tests/component_tests/text/test_text.py | 4 +- tests/unit_tests/test_cpp_generator.py | 55 -------------------- 11 files changed, 11 insertions(+), 182 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index cfe0af1b59..99def9f273 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -16,12 +16,7 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, ) from esphome.core import ID -from esphome.cpp_generator import ( - LambdaExpression, - MockObj, - MockObjClass, - TemplateArgsType, -) +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -92,7 +87,6 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) -StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) -StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) -def new_lambda_pvariable( - id_obj: ID, - lambda_expr: LambdaExpression, - stateless_class: MockObjClass, - template_arg: cg.TemplateArguments | None = None, -) -> MockObj: - """Create Pvariable for lambda, using stateless class if applicable. - - Combines ID selection and Pvariable creation in one call. For stateless - lambdas (empty capture), uses function pointer instead of std::function. - - Args: - id_obj: The ID object (action_id, condition_id, or filter_id) - lambda_expr: The lambda expression object - stateless_class: The stateless class to use for stateless lambdas - template_arg: Optional template arguments (for actions/conditions) - - Returns: - The created Pvariable - """ - # For stateless lambdas, use function pointer instead of std::function - if lambda_expr.capture == "": - id_obj = id_obj.copy() - id_obj.type = stateless_class - - if template_arg is not None: - return cg.new_Pvariable(id_obj, template_arg, lambda_expr) - return cg.new_Pvariable(id_obj, lambda_expr) - - def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -277,9 +240,7 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return new_lambda_pvariable( - condition_id, lambda_, StatelessLambdaCondition, template_arg - ) + return cg.new_Pvariable(condition_id, template_arg, lambda_) @register_condition( @@ -445,7 +406,7 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) + return cg.new_Pvariable(action_id, template_arg, lambda_) @register_action( diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8892b57e6e..26e784a0b8 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -300,7 +299,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) @register_filter( diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2d473c3b64..a7eb080feb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -111,21 +111,6 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} - - optional new_value(bool value) override { return this->f_(value); } - - protected: - optional (*f_)(bool); -}; - class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 22bf3d2f4c..1d02073d27 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction, StatelessLambdaAction +from esphome.automation import LambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -430,9 +430,7 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return automation.new_lambda_pvariable( - action_id, lambda_, StatelessLambdaAction, template_arg - ) + return cg.new_Pvariable(action_id, template_arg, lambda_) @automation.register_action( @@ -457,9 +455,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return automation.new_lambda_pvariable( - action_id, lambda_, StatelessLambdaAction, template_arg - ) + return cg.new_Pvariable(action_id, template_arg, lambda_) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 41ac3516b9..93283e4d47 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -261,7 +261,6 @@ ExponentialMovingAverageFilter = sensor_ns.class_( ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) @@ -574,7 +573,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) DELTA_SCHEMA = cv.Schema( diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efe..ecd55308d1 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -296,21 +296,6 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} - - optional new_value(float value) override { return this->lambda_filter_(value); } - - protected: - optional (*lambda_filter_)(float); -}; - /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index adc8a76fcd..7a9e947abd 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -57,7 +57,6 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -71,7 +70,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 85acac5c8d..c77c221235 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -62,21 +62,6 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} - - optional new_value(std::string value) override { return this->lambda_filter_(value); } - - protected: - optional (*lambda_filter_)(std::string); -}; - /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1c60dd1c7a..af8cde971b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -79,18 +79,6 @@ template class LambdaCondition : public Condition { std::function f_; }; -/// Optimized lambda condition for stateless lambdas (no capture). -/// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). -template class StatelessLambdaCondition : public Condition { - public: - explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} - bool check(Ts... x) override { return this->f_(x...); } - - protected: - bool (*f_)(Ts...); -}; - template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} @@ -202,19 +190,6 @@ template class LambdaAction : public Action { std::function f_; }; -/// Optimized lambda action for stateless lambdas (no capture). -/// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). -template class StatelessLambdaAction : public Action { - public: - explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} - - void play(Ts... x) override { this->f_(x...); } - - protected: - void (*f_)(Ts...); -}; - template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee7..75f1c4b88b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ 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 """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 2c9f760c8e..95633ca0c6 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,61 +173,6 @@ class TestLambdaExpression: "}" ) - def test_str__stateless_no_return(self): - """Test stateless lambda (empty capture) generates correctly""" - target = cg.LambdaExpression( - ('ESP_LOGD("main", "Test message");',), - (), # No parameters - "", # Empty capture (stateless) - ) - - actual = str(target) - - assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') - - def test_str__stateless_with_return(self): - """Test stateless lambda with return type generates correctly""" - target = cg.LambdaExpression( - ("return global_value > 0;",), - (), # No parameters - "", # Empty capture (stateless) - bool, # Return type - ) - - actual = str(target) - - assert actual == ("[]() -> bool {\n return global_value > 0;\n}") - - def test_str__stateless_with_params(self): - """Test stateless lambda with parameters generates correctly""" - target = cg.LambdaExpression( - ("return foo + bar;",), - ((int, "foo"), (float, "bar")), - "", # Empty capture (stateless) - float, - ) - - actual = str(target) - - assert actual == ( - "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" - ) - - def test_str__with_capture(self): - """Test lambda with capture generates correctly""" - target = cg.LambdaExpression( - ("return captured_var + x;",), - ((int, "x"),), - "captured_var", # Has capture (not stateless) - int, - ) - - actual = str(target) - - assert actual == ( - "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" - ) - class TestLiterals: @pytest.mark.parametrize( From 0bbe32683054ed933e7b222c6e38ae59f819fa81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:51:42 -0700 Subject: [PATCH 3/6] preen --- esphome/core/automation.h | 71 +++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index fe2daa4f80..eb8c9316ba 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -46,24 +46,36 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - if (type_ == VALUE) { - new (&this->value_) T(other.value_); - } else if (type_ == LAMBDA) { - this->f_ = new std::function(*other.f_); - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; + switch (type_) { + case VALUE: + new (&this->value_) T(other.value_); + break; + case LAMBDA: + this->f_ = new std::function(*other.f_); + break; + case STATELESS_LAMBDA: + this->stateless_f_ = other.stateless_f_; + break; + case NONE: + break; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - if (type_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); - } else if (type_ == LAMBDA) { - this->f_ = other.f_; - other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; + switch (type_) { + case VALUE: + new (&this->value_) T(std::move(other.value_)); + break; + case LAMBDA: + this->f_ = other.f_; + other.f_ = nullptr; + break; + case STATELESS_LAMBDA: + this->stateless_f_ = other.stateless_f_; + break; + case NONE: + break; } other.type_ = NONE; } @@ -86,25 +98,34 @@ template class TemplatableValue { } ~TemplatableValue() { - if (type_ == VALUE) { - this->value_.~T(); - } else if (type_ == LAMBDA) { - delete this->f_; + switch (type_) { + case VALUE: + this->value_.~T(); + break; + case LAMBDA: + delete this->f_; + break; + case STATELESS_LAMBDA: + case NONE: + // No cleanup needed (function pointer or empty, not heap-allocated) + break; } - // STATELESS_LAMBDA needs no cleanup (function pointer, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { - if (this->type_ == STATELESS_LAMBDA) { - return this->stateless_f_(x...); // Direct function pointer call + switch (this->type_) { + case STATELESS_LAMBDA: + return this->stateless_f_(x...); // Direct function pointer call + case LAMBDA: + return (*this->f_)(x...); // std::function call + case VALUE: + return this->value_; + case NONE: + default: + return T{}; } - if (this->type_ == LAMBDA) { - return (*this->f_)(x...); // std::function call - } - // return value also when none - return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { From b68d030f5a9a2648f63deae1d2f53f4951e82b35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:59:12 -0700 Subject: [PATCH 4/6] update tests --- tests/component_tests/text/test_text.py | 4 +- tests/unit_tests/test_cpp_generator.py | 55 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..99ddd78ee7 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ 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 + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From 48b45ba4391ed377ce83c31ce8e314283e011c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:01:54 -0700 Subject: [PATCH 5/6] we have c++20 --- esphome/core/automation.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index eb8c9316ba..82dce303d2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -27,20 +27,21 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + template + requires(!std::invocable) TemplatableValue(F value) : type_(VALUE) { new (&this->value_) T(std::move(value)); } // For stateless lambdas (convertible to function pointer): use function pointer - template::value && std::is_convertible::value, int> = 0> - TemplatableValue(F f) : type_(STATELESS_LAMBDA) { + template + requires std::invocable && std::convertible_to TemplatableValue(F f) + : type_(STATELESS_LAMBDA) { this->stateless_f_ = f; // Implicit conversion to function pointer } // For stateful lambdas (not convertible to function pointer): use std::function - template::value && !std::is_convertible::value, int> = 0> - TemplatableValue(F f) : type_(LAMBDA) { + template + requires std::invocable &&(!std::convertible_to) TemplatableValue(F f) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } From 1652ea8b97d939e7ce5ab3cda335adc74780f2ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:14:01 -0700 Subject: [PATCH 6/6] overkill --- esphome/core/automation.h | 54 +++++++++++++-------------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 82dce303d2..d357c6f58e 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -47,36 +47,24 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - switch (type_) { - case VALUE: - new (&this->value_) T(other.value_); - break; - case LAMBDA: - this->f_ = new std::function(*other.f_); - break; - case STATELESS_LAMBDA: - this->stateless_f_ = other.stateless_f_; - break; - case NONE: - break; + if (type_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - switch (type_) { - case VALUE: - new (&this->value_) T(std::move(other.value_)); - break; - case LAMBDA: - this->f_ = other.f_; - other.f_ = nullptr; - break; - case STATELESS_LAMBDA: - this->stateless_f_ = other.stateless_f_; - break; - case NONE: - break; + if (type_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -99,18 +87,12 @@ template class TemplatableValue { } ~TemplatableValue() { - switch (type_) { - case VALUE: - this->value_.~T(); - break; - case LAMBDA: - delete this->f_; - break; - case STATELESS_LAMBDA: - case NONE: - // No cleanup needed (function pointer or empty, not heap-allocated) - break; + if (type_ == VALUE) { + this->value_.~T(); + } else if (type_ == LAMBDA) { + delete this->f_; } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) } bool has_value() { return this->type_ != NONE; }