From 0d65d23c05362229e6e0bd473252b295e465cea8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 11:16:10 -1000 Subject: [PATCH 1/4] lambda everything --- esphome/components/light/automation.h | 82 ++++----------------- esphome/components/light/automation.py | 98 ++++++++++++++++++-------- 2 files changed, 81 insertions(+), 99 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 552b18d8c8..0b570ab752 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,22 +24,19 @@ template class ToggleAction : public Action { LightState *state_; }; -/** Compact light control action using per-field union storage. +/** Compact light control action — every field is a function pointer. * - * Each field stores either a constant value or a function pointer in the same - * word. A 2-bit-per-field type tag in a uint32_t tracks whether each field is - * unset, a constant, or a lambda (stateless function pointer — ESPHome codegen - * always produces empty-capture lambdas). + * Python codegen wraps constant values in stateless lambdas, so each field + * is either nullptr (unset) or a typed function pointer. No tagged union, + * no type dispatch, no memcpy — just call through the pointer. * - * Size: ~76 bytes on ESP32 (vs ~128 bytes with TemplatableValue per field). - * No per-field heap allocation. + * Size: ~72 bytes on ESP32 (vs ~128 bytes with TemplatableValue per field). */ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} // clang-format off -// Single field list — drives enum, setters, and play(). #define LIGHT_CONTROL_FIELDS(X) \ X(ColorMode, color_mode) \ X(bool, state) \ @@ -57,75 +54,22 @@ template class LightControlAction : public Action { X(uint32_t, effect) #define LIGHT_CONTROL_SETTER_(type, name) \ - void set_##name(type v) { this->set_constant_(FIELD_##name, v); } \ - template void set_##name(F f) requires std::invocable { \ - this->template set_lambda_(FIELD_##name, std::forward(f)); \ + template void set_##name(F f) { \ + static_assert(std::convertible_to, \ + "LightControlAction: only stateless lambdas are supported"); \ + this->name##_ = static_cast(f); \ } #define APPLY_LIGHT_FIELD_(type, name) \ - if (auto t = this->get_field_type_(FIELD_##name); t) \ - call.set_##name(this->get_value_(FIELD_##name, t, x...)); + if (this->name##_) call.set_##name(this->name##_(x...)); // clang-format on protected: - enum FieldIndex : uint8_t { -#define FIELD_ENUM_(type, name) FIELD_##name, - LIGHT_CONTROL_FIELDS(FIELD_ENUM_) -#undef FIELD_ENUM_ - NUM_FIELDS - }; - - /// 2-bit field type encoded in field_types_ - enum FieldType : uint8_t { TYPE_NONE = 0, TYPE_CONSTANT = 1, TYPE_LAMBDA = 2 }; - static_assert(NUM_FIELDS * 2 <= 32, "Too many fields for uint32_t field_types_"); - - /// Per-field storage: constant value or function pointer bytes in the same word. - /// All access is via memcpy to avoid type-punning and void*-to-function-pointer UB. - union FieldValue { - uint8_t raw[sizeof(void *)]; - }; - - FieldType get_field_type_(uint8_t field) const { - return static_cast((this->field_types_ >> (field * 2)) & 0x3); - } - - void set_field_type_(uint8_t field, FieldType type) { - uint32_t shift = field * 2; - this->field_types_ = (this->field_types_ & ~(0x3u << shift)) | (static_cast(type) << shift); - } - - template void set_constant_(uint8_t field, T value) { - static_assert(std::is_trivially_copyable_v); - static_assert(sizeof(T) <= sizeof(FieldValue)); - this->set_field_type_(field, TYPE_CONSTANT); - memcpy(&this->values_[field], &value, sizeof(T)); - } - - template void set_lambda_(uint8_t field, F f) { - // ESPHome codegen always produces stateless lambdas (empty capture list), - // which are convertible to function pointers. - static_assert(std::convertible_to, "LightControlAction only supports stateless lambdas"); - static_assert(sizeof(T(*)(Ts...)) <= sizeof(FieldValue)); - this->set_field_type_(field, TYPE_LAMBDA); - auto fn = static_cast(f); - memcpy(&this->values_[field], &fn, sizeof(fn)); - } - - template T get_value_(uint8_t field, FieldType type, const Ts &...x) const { - if (type == TYPE_CONSTANT) { - T value; - memcpy(&value, &this->values_[field], sizeof(T)); - return value; - } - // TYPE_LAMBDA — function pointer stored via memcpy - T (*fn)(Ts...); - memcpy(&fn, &this->values_[field], sizeof(fn)); - return fn(x...); - } +#define LIGHT_CONTROL_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; LightState *parent_; - uint32_t field_types_{0}; ///< 2 bits per field - FieldValue values_[NUM_FIELDS]{}; + LIGHT_CONTROL_FIELDS(LIGHT_CONTROL_FIELD_DECL_) +#undef LIGHT_CONTROL_FIELD_DECL_ public: LIGHT_CONTROL_FIELDS(LIGHT_CONTROL_SETTER_) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 55273003b9..f97249bfeb 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv @@ -28,7 +30,7 @@ from esphome.const import ( ) from esphome.core import CORE, Lambda from esphome.cpp_generator import LambdaExpression -from esphome.types import ConfigType +from esphome.types import ConfigType, SafeExpType from .types import ( COLOR_MODES, @@ -116,6 +118,28 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) +async def _as_lambda( + value: Any, + args: list[tuple[SafeExpType, str]], + output_type: SafeExpType, +) -> LambdaExpression: + """Return a stateless lambda expression for a templatable value. + + If value is already a lambda, process it normally. Otherwise wrap + the constant in a ``[](...) -> T { return ; }`` expression + so that LightControlAction can store every field as a plain + function pointer. + """ + if cg.is_template(value): + return await cg.process_lambda(value, args, return_type=output_type) + return LambdaExpression( + f"return {cg.safe_exp(value)};", + args, + capture="", + return_type=output_type, + ) + + def _resolve_effect_index(config: ConfigType) -> int: """Resolve a static effect name to its 1-based index at codegen time. @@ -149,46 +173,57 @@ async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_COLOR_MODE in config: - template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) - cg.add(var.set_color_mode(template_)) + cg.add( + var.set_color_mode( + await _as_lambda(config[CONF_COLOR_MODE], args, ColorMode) + ) + ) if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, bool) - cg.add(var.set_state(template_)) + cg.add(var.set_state(await _as_lambda(config[CONF_STATE], args, bool))) if CONF_TRANSITION_LENGTH in config: - template_ = await cg.templatable( - config[CONF_TRANSITION_LENGTH], args, cg.uint32 + cg.add( + var.set_transition_length( + await _as_lambda(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + ) ) - cg.add(var.set_transition_length(template_)) if CONF_FLASH_LENGTH in config: - template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) - cg.add(var.set_flash_length(template_)) + cg.add( + var.set_flash_length( + await _as_lambda(config[CONF_FLASH_LENGTH], args, cg.uint32) + ) + ) if CONF_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) - cg.add(var.set_brightness(template_)) + cg.add( + var.set_brightness(await _as_lambda(config[CONF_BRIGHTNESS], args, float)) + ) if CONF_COLOR_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) - cg.add(var.set_color_brightness(template_)) + cg.add( + var.set_color_brightness( + await _as_lambda(config[CONF_COLOR_BRIGHTNESS], args, float) + ) + ) if CONF_RED in config: - template_ = await cg.templatable(config[CONF_RED], args, float) - cg.add(var.set_red(template_)) + cg.add(var.set_red(await _as_lambda(config[CONF_RED], args, float))) if CONF_GREEN in config: - template_ = await cg.templatable(config[CONF_GREEN], args, float) - cg.add(var.set_green(template_)) + cg.add(var.set_green(await _as_lambda(config[CONF_GREEN], args, float))) if CONF_BLUE in config: - template_ = await cg.templatable(config[CONF_BLUE], args, float) - cg.add(var.set_blue(template_)) + cg.add(var.set_blue(await _as_lambda(config[CONF_BLUE], args, float))) if CONF_WHITE in config: - template_ = await cg.templatable(config[CONF_WHITE], args, float) - cg.add(var.set_white(template_)) + cg.add(var.set_white(await _as_lambda(config[CONF_WHITE], args, float))) if CONF_COLOR_TEMPERATURE in config: - template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) - cg.add(var.set_color_temperature(template_)) + cg.add( + var.set_color_temperature( + await _as_lambda(config[CONF_COLOR_TEMPERATURE], args, float) + ) + ) if CONF_COLD_WHITE in config: - template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) - cg.add(var.set_cold_white(template_)) + cg.add( + var.set_cold_white(await _as_lambda(config[CONF_COLD_WHITE], args, float)) + ) if CONF_WARM_WHITE in config: - template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) - cg.add(var.set_warm_white(template_)) + cg.add( + var.set_warm_white(await _as_lambda(config[CONF_WARM_WHITE], args, float)) + ) if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): # Lambda returns a string — wrap in a C++ lambda that resolves @@ -211,8 +246,11 @@ async def light_control_to_code(config, action_id, template_arg, args): cg.add(var.set_effect(wrapper)) else: # Static string — resolve effect name to index at codegen time - effect_index = _resolve_effect_index(config) - cg.add(var.set_effect(effect_index)) + cg.add( + var.set_effect( + await _as_lambda(_resolve_effect_index(config), args, cg.uint32) + ) + ) return var From e862b4f5fdf76654d44d9ecb9fe5025c59cb3db8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 11:23:37 -1000 Subject: [PATCH 2/4] reduce --- esphome/components/light/automation.h | 34 ++++++++++----------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 0b570ab752..7f6c48b29d 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -53,34 +53,26 @@ template class LightControlAction : public Action { X(float, warm_white) \ X(uint32_t, effect) -#define LIGHT_CONTROL_SETTER_(type, name) \ - template void set_##name(F f) { \ - static_assert(std::convertible_to, \ - "LightControlAction: only stateless lambdas are supported"); \ - this->name##_ = static_cast(f); \ - } - -#define APPLY_LIGHT_FIELD_(type, name) \ - if (this->name##_) call.set_##name(this->name##_(x...)); +#define LIGHT_FIELD_SETTER_(type, name) void set_##name(type (*f)(Ts...)) { this->name##_ = f; } +#define LIGHT_FIELD_APPLY_(type, name) if (this->name##_) call.set_##name(this->name##_(x...)); +#define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; // clang-format on - protected: -#define LIGHT_CONTROL_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; - - LightState *parent_; - LIGHT_CONTROL_FIELDS(LIGHT_CONTROL_FIELD_DECL_) -#undef LIGHT_CONTROL_FIELD_DECL_ - - public: - LIGHT_CONTROL_FIELDS(LIGHT_CONTROL_SETTER_) -#undef LIGHT_CONTROL_SETTER_ + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(APPLY_LIGHT_FIELD_) + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) call.perform(); } -#undef APPLY_LIGHT_FIELD_ + + protected: + LightState *parent_; + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) + +#undef LIGHT_FIELD_DECL_ +#undef LIGHT_FIELD_APPLY_ +#undef LIGHT_FIELD_SETTER_ #undef LIGHT_CONTROL_FIELDS }; From 1ad84035c3730789547c6ce120290a014aff8375 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 11:24:19 -1000 Subject: [PATCH 3/4] reduce --- esphome/components/light/automation.h | 35 ++++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 7f6c48b29d..1fece0bb24 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -36,27 +36,28 @@ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} -// clang-format off #define LIGHT_CONTROL_FIELDS(X) \ - X(ColorMode, color_mode) \ - X(bool, state) \ - X(uint32_t, transition_length)\ - X(uint32_t, flash_length) \ - X(float, brightness) \ - X(float, color_brightness) \ - X(float, red) \ - X(float, green) \ - X(float, blue) \ - X(float, white) \ - X(float, color_temperature) \ - X(float, cold_white) \ - X(float, warm_white) \ + X(ColorMode, color_mode) \ + X(bool, state) \ + X(uint32_t, transition_length) \ + X(uint32_t, flash_length) \ + X(float, brightness) \ + X(float, color_brightness) \ + X(float, red) \ + X(float, green) \ + X(float, blue) \ + X(float, white) \ + X(float, color_temperature) \ + X(float, cold_white) \ + X(float, warm_white) \ X(uint32_t, effect) -#define LIGHT_FIELD_SETTER_(type, name) void set_##name(type (*f)(Ts...)) { this->name##_ = f; } -#define LIGHT_FIELD_APPLY_(type, name) if (this->name##_) call.set_##name(this->name##_(x...)); +#define LIGHT_FIELD_SETTER_(type, name) \ + void set_##name(type (*f)(Ts...)) { this->name##_ = f; } +#define LIGHT_FIELD_APPLY_(type, name) \ + if (this->name##_) \ + call.set_##name(this->name##_(x...)); #define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; - // clang-format on LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) From 35a7ba75de7952026d04481de43fda87a4761df1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 11:26:06 -1000 Subject: [PATCH 4/4] reduce --- esphome/components/light/automation.h | 10 +--- esphome/components/light/automation.py | 73 ++++++++------------------ 2 files changed, 24 insertions(+), 59 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 1fece0bb24..a5c9220a23 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,14 +24,8 @@ template class ToggleAction : public Action { LightState *state_; }; -/** Compact light control action — every field is a function pointer. - * - * Python codegen wraps constant values in stateless lambdas, so each field - * is either nullptr (unset) or a typed function pointer. No tagged union, - * no type dispatch, no memcpy — just call through the pointer. - * - * Size: ~72 bytes on ESP32 (vs ~128 bytes with TemplatableValue per field). - */ +/// Compact light control action — each field is a function pointer (nullptr = unset). +/// Codegen wraps constants in stateless lambdas. 72 bytes vs 128 with TemplatableValue. template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index f97249bfeb..6906ef7cc2 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -172,58 +172,29 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_COLOR_MODE in config: - cg.add( - var.set_color_mode( - await _as_lambda(config[CONF_COLOR_MODE], args, ColorMode) + + # (config_key, setter_name, c++ type) + FIELDS = ( + (CONF_COLOR_MODE, "set_color_mode", ColorMode), + (CONF_STATE, "set_state", bool), + (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), + (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), + (CONF_BRIGHTNESS, "set_brightness", float), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", float), + (CONF_RED, "set_red", float), + (CONF_GREEN, "set_green", float), + (CONF_BLUE, "set_blue", float), + (CONF_WHITE, "set_white", float), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", float), + (CONF_COLD_WHITE, "set_cold_white", float), + (CONF_WARM_WHITE, "set_warm_white", float), + ) + for conf_key, setter, type_ in FIELDS: + if conf_key in config: + cg.add( + getattr(var, setter)(await _as_lambda(config[conf_key], args, type_)) ) - ) - if CONF_STATE in config: - cg.add(var.set_state(await _as_lambda(config[CONF_STATE], args, bool))) - if CONF_TRANSITION_LENGTH in config: - cg.add( - var.set_transition_length( - await _as_lambda(config[CONF_TRANSITION_LENGTH], args, cg.uint32) - ) - ) - if CONF_FLASH_LENGTH in config: - cg.add( - var.set_flash_length( - await _as_lambda(config[CONF_FLASH_LENGTH], args, cg.uint32) - ) - ) - if CONF_BRIGHTNESS in config: - cg.add( - var.set_brightness(await _as_lambda(config[CONF_BRIGHTNESS], args, float)) - ) - if CONF_COLOR_BRIGHTNESS in config: - cg.add( - var.set_color_brightness( - await _as_lambda(config[CONF_COLOR_BRIGHTNESS], args, float) - ) - ) - if CONF_RED in config: - cg.add(var.set_red(await _as_lambda(config[CONF_RED], args, float))) - if CONF_GREEN in config: - cg.add(var.set_green(await _as_lambda(config[CONF_GREEN], args, float))) - if CONF_BLUE in config: - cg.add(var.set_blue(await _as_lambda(config[CONF_BLUE], args, float))) - if CONF_WHITE in config: - cg.add(var.set_white(await _as_lambda(config[CONF_WHITE], args, float))) - if CONF_COLOR_TEMPERATURE in config: - cg.add( - var.set_color_temperature( - await _as_lambda(config[CONF_COLOR_TEMPERATURE], args, float) - ) - ) - if CONF_COLD_WHITE in config: - cg.add( - var.set_cold_white(await _as_lambda(config[CONF_COLD_WHITE], args, float)) - ) - if CONF_WARM_WHITE in config: - cg.add( - var.set_warm_white(await _as_lambda(config[CONF_WARM_WHITE], args, float)) - ) + if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): # Lambda returns a string — wrap in a C++ lambda that resolves