From c58a26dce2a128ff6f1556d85c6d28eec708aa9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Oct 2026 19:46:29 -0500 Subject: [PATCH] [core] Add cg.shared_progmem_array for deduplicated PROGMEM tables Emits one PROGMEM array per distinct type and contents and returns the same variable to later callers. The array name is made unique against every config id and registered variable, so a generated table can never collide with a user id. --- esphome/codegen.py | 1 + esphome/cpp_generator.py | 32 +++++++++++++++++++++++++- tests/unit_tests/test_cpp_generator.py | 29 +++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 4de1d8d5c19..daf53db43fd 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -40,6 +40,7 @@ from esphome.cpp_generator import ( # noqa: F401 progmem_array, safe_exp, set_cpp_standard, + shared_progmem_array, statement, static_const_array, static_function, diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b0c3533e040..9bcea1d09d9 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,7 +19,12 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) -from esphome.helpers import cpp_string_escape, indent, indent_all_but_first_and_last +from esphome.helpers import ( + cpp_string_escape, + ensure_unique_string, + indent, + indent_all_but_first_and_last, +) from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase @@ -463,6 +468,31 @@ def progmem_array(id_, rhs) -> "MockObj": return obj +def shared_progmem_array( + name: str, type_: "MockObjClass", rhs: SafeExpType +) -> "MockObj": + """Emit a PROGMEM array once per distinct type and contents; later calls reuse it. + + The name is made unique against every config id and registered variable. Call it at + setup() top level: the array is a static local that later reuses must be able to see. + """ + from esphome.config import iter_ids + from esphome.config_validation import RESERVED_IDS + + arrays: dict[str, MockObj] = CORE.data.setdefault("shared_progmem_array", {}) + rhs = safe_exp(rhs) + key = f"{type_} {rhs}" + if (array := arrays.get(key)) is None: + used = {str(i) for i, _ in iter_ids(CORE.config)} + used |= {str(i) for i in CORE.variables} + used |= set(RESERVED_IDS) | CORE.loaded_integrations + unique = ensure_unique_string(name, used) + array = arrays[key] = progmem_array( + ID(unique, is_declaration=True, type=type_), rhs + ) + return array + + def static_const_array(id_, rhs) -> "MockObj": rhs = safe_exp(rhs) obj = MockObj(id_, ".") diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index fa6255b8c9e..f9852026877 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -4,6 +4,7 @@ import math import pytest from esphome import cpp_generator as cg, cpp_types as ct +from esphome.core import CORE, ID class TestExpressions: @@ -808,3 +809,31 @@ async def test_templatable__lambda_with_std_string() -> None: result = await cg.templatable(lambda_obj, [], ct.std_string) assert isinstance(result, cg.LambdaExpression) + + +class TestSharedProgmemArray: + def test_identical_contents_share_one_array(self) -> None: + CORE.config = {} + a = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3]) + b = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3]) + c = cg.shared_progmem_array("table", ct.uint8, [4]) + assert a is b + assert str(a) != str(c) + assert sum("PROGMEM" in str(st) for st in CORE.main_statements) == 2 + + def test_same_contents_different_type_are_separate(self) -> None: + CORE.config = {} + a = cg.shared_progmem_array("table", ct.uint8, [1]) + b = cg.shared_progmem_array("table", ct.uint16, [1]) + assert str(a) != str(b) + + def test_name_avoids_config_ids(self) -> None: + CORE.config = {"sensor": [{"id": ID("table", is_declaration=True)}]} + array = cg.shared_progmem_array("table", ct.uint8, [1]) + assert str(array) == "table_2" + + def test_name_avoids_registered_variables(self) -> None: + CORE.config = {} + CORE.register_variable(ID("table", is_declaration=True), cg.MockObj("table")) + array = cg.shared_progmem_array("table", ct.uint8, [1]) + assert str(array) == "table_2"