From 0d65d23c05362229e6e0bd473252b295e465cea8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 11:16:10 -1000 Subject: [PATCH] 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 552b18d8c88..0b570ab7526 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 55273003b95..f97249bfebf 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