Compare commits

..
Author SHA1 Message Date
J. Nick Koston f3676e27d4 Merge remote-tracking branch 'origin/parse-float-prefix' into HEAD 2026-11-14 08:39:53 -06:00
J. Nick Koston 365bea82ad Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:35:53 -06:00
J. Nick Koston 15291f06a5 Merge commit '090156a32e851ea237bf16e18e967fdbab20cdfe' into parse-float-components 2026-11-13 23:45:59 -06:00
J. Nick Koston 090156a32e [core] Say str_to_float does not set errno 2026-11-13 23:45:56 -06:00
J. Nick Koston db07dac851 Merge commit '543151ab1cffd9ddce66ab310d647ba907247abf' into parse-float-components 2026-11-13 23:05:30 -06:00
J. Nick Koston 543151ab1c [core] Keep the str_to_float comment within 120 columns 2026-11-13 23:05:27 -06:00
J. Nick Koston 3f8705ecbb Merge commit '3b053f1a0c5abe194d1930a0c02a8c52ac2e29b1' into parse-float-components 2026-11-13 23:05:18 -06:00
J. Nick Koston 3b053f1a0c [core] Point str_to_float only at float results of strtod and atof 2026-11-13 23:05:15 -06:00
J. Nick Koston d719b9220e Merge commit '7bed89eafc5b7bbe70131a7793a92030187d656d' into parse-float-components 2026-11-13 22:37:58 -06:00
J. Nick Koston 7bed89eafc [core] Document what parse_float accepts and rejects 2026-11-13 22:37:55 -06:00
J. Nick Koston 381d6fe7cd Merge commit '04fe5cdac96bec4161ff85b29f9a7eb4eb30f64b' into parse-float-components 2026-11-13 22:13:14 -06:00
J. Nick Koston 04fe5cdac9 [core] Stop hex digits at ':' to '?' instead of reading them as 10 to 15
picolibc 1.8.10 (and its main) read them as hex digits, so "0x1:" was 26 and
parse_number<float> accepted it; newlib and the C standard stop there.
2026-11-13 22:13:11 -06:00
J. Nick Koston 72555af9b6 Merge commit 'b35a450f5f9ea4bb90da86da842b5583d3f6e6a3' into parse-float-components 2026-11-13 19:43:09 -06:00
J. Nick Koston b35a450f5f [core] Say stof uses str_to_float in the StringRef helpers comment 2026-11-13 19:43:06 -06:00
J. Nick Koston 16205445bb Merge commit 'bf6d3ca470e598bffbb1573caf1118125b48b2d7' into parse-float-components 2026-11-13 19:40:00 -06:00
J. Nick Koston bf6d3ca470 [core] Note the one non-tie near a midpoint 2026-11-13 19:39:57 -06:00
J. Nick Koston 6cd7dd98f3 Merge commit '16f660b0090391b511dd06ad5c20ba9e02cefcc0' into parse-float-components 2026-11-13 19:37:24 -06:00
J. Nick Koston 16f660b009 [core] Terser parser comments and docstrings 2026-11-13 19:37:21 -06:00
J. Nick Koston ce3a282a41 Merge commit '1e127efe8e6c5f7e613c6661ba2d08ca6f1164c3' into parse-float-components 2026-11-13 19:35:10 -06:00
J. Nick Koston 1e127efe8e [core] Rename the pow10 template to pow10_exact so it cannot hide libm's pow10 2026-11-13 19:35:07 -06:00
J. Nick Koston e657d18e45 Merge commit 'cdd994455141a13f0ecb7d752f98b49072b658a3' into parse-float-components 2026-11-13 19:34:32 -06:00
J. Nick Koston cdd9944551 [core] Fewer lines in parse_float.cpp; object code identical on host, ESP8266 and RP2040 2026-11-13 19:34:29 -06:00
J. Nick Koston dacf19954f Merge commit '9e64b21eb46a2ecae9cebb3a6efacb1e42ae08c3' into parse-float-components 2026-11-13 19:21:23 -06:00
J. Nick Koston 9e64b21eb4 [core] Share the plain-decimal limit between parse_float and str_to_float; no multiply for the fast-path factor 2026-11-13 19:21:16 -06:00
J. Nick Koston ba424d1f26 Merge commit '6f7b62602e6' into parse-float-components 2026-11-13 17:46:10 -06:00
J. Nick Koston 6f7b62602e [core] Give picolibc's strtof result, with integer arithmetic only
str_to_float and parse_number<float> now return what picolibc 1.8.10's
strtof (ESP-IDF 6) returns, bits and end pointer, on every platform: a
decimal is rounded to 9 significant digits (ties to even) and those are
converted to the nearest float; hex is rounded to the nearest float.

- the 9 digits convert with one float operation when both operands are
  exact floats, otherwise with one 64-bit product against a 12-entry
  table of 10^(8k); all 85 billion (m10, e10) were checked against
  picolibc's engine, and the ties the product cannot decide alone are
  computed exactly
- no double arithmetic and no big-integer path: time linear in the
  input length, and less code
- picolibc's syntax: "x" after zero digits starts hex, "inf" and
  "infinity", "nan(" with letters, digits and '_', exponent digits
  capped at 224
- tests use picolibc's values for long input and syntax, the host
  strtof for up to 9 significant digits, and name the RP2040 and
  RP2350 libc results the bench found (flushed denormals, NaN to inf)
2026-11-13 17:40:06 -06:00
J. Nick Koston 29020748e2 [dfrobot_sen0609][hydreon_rgxx][smt100] Use str_to_float 2026-11-13 17:00:01 -06:00
J. Nick Koston 8917409887 Merge commit 'dd08b19bd51' into parse-float-components 2026-11-13 16:59:51 -06:00
J. Nick Koston dd08b19bd5 Round to the nearest float; rename parse_float_prefix to str_to_float
The parser now gives the correctly rounded float (ties to even) for decimal
and hex input on every platform, instead of newlib's decimal-to-double-to-
float double rounding. Syntax and end pointer stay newlib's; the hex exponent
saturates instead of wrapping at 32 bits.

- one integer rounding helper builds the float bits for the double guess,
  hex input and double_to_float_bits
- the exact comparison is against the float midpoint itself
- the early return checks the guess's distance to a float midpoint
  directly, without two extra conversions
- named exponent clamps folded into the EXACT_WORDS static_assert
- tests use the host's correctly rounded strtof as the reference, with
  named tests for the inputs where newlib and picolibc differ
- the unit test is host only (ESP32 clang-tidy), reproducible RNG seeds
  are marked, and int32_t replaces long
2026-11-13 16:45:41 -06:00
J. Nick Koston 7b8afeb4df [dfrobot_sen0609][hydreon_rgxx][smt100] Sort the parse_float.h include 2026-11-13 16:24:26 -06:00
J. Nick Koston 2eb25bf800 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 16:24:14 -06:00
J. Nick Koston b4e65d51bb [core] Test the double fast path, the overflow midpoint and nan forms; bound exact rounds at two 2026-11-13 16:24:12 -06:00
J. Nick Koston da65beb404 [core] Move pow10, is_digit and is_space_c into helpers.h; integer powers of ten on the plain path 2026-11-13 16:24:12 -06:00
J. Nick Koston 491674adec [dfrobot_sen0609][hydreon_rgxx][smt100] Include parse_float.h for parse_float_prefix 2026-11-13 15:54:42 -06:00
J. Nick Koston e3a9f11525 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:54:30 -06:00
J. Nick Koston 859299b94c [core] Drop the parse_float_prefix benchmarks that repeat parse_number<float>'s 2026-11-13 15:54:29 -06:00
J. Nick Koston 080bdbd53b [core] Simplify parse_float_prefix after review and declare parse_float in parse_float.h 2026-11-13 15:54:29 -06:00
J. Nick Koston ba53757f7e [dfrobot_sen0609] Update a comment that still named strtof 2026-11-13 15:24:03 -06:00
J. Nick Koston df31b05804 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:23:56 -06:00
J. Nick Koston 49e696555e [core] Benchmark rejected text and drop the libc comparison benchmarks 2026-11-13 15:23:45 -06:00
J. Nick Koston 10a7992f5b [core] Bound parse_float_prefix's time on every platform and speed up common inputs 2026-11-13 15:23:44 -06:00
J. Nick Koston 774cb9e9e5 Merge remote-tracking branch 'origin/parse-float-prefix' into parse-float-components 2026-11-13 14:51:40 -06:00
J. Nick Koston cbf38abdb9 [core] Benchmark parse_number<float> and parse_float_prefix against strtof 2026-11-13 14:51:26 -06:00
J. Nick Koston e5bf7c74e7 [core] Take strtod's exact double fast path in parse_float_prefix 2026-11-13 14:51:26 -06:00
J. Nick Koston c197a824e3 [dfrobot_sen0609][hydreon_rgxx][smt100] Parse floats with parse_float_prefix 2026-11-13 14:34:25 -06:00
J. Nick Koston 8cfc265b19 [core] Parse floats with parse_float_prefix so libc's strtod is not linked 2026-11-13 14:34:25 -06:00
J. Nick Koston fa08c7a305 [core] Add parse_float_prefix, a strtof that does not link libc's strtod 2026-11-13 14:27:23 -06:00
23 changed files with 962 additions and 721 deletions

No files matched your search

+21 -64
View File
@@ -36,7 +36,6 @@ from esphome.cpp_generator import (
LambdaExpression,
MockObj,
MockObjClass,
Statement,
TemplateArgsType,
call_lambda,
)
@@ -705,16 +704,11 @@ def apply_static_function(
statements: list[str],
share: bool = False,
suffix: str = "fn",
add_global_: Callable[[Statement], object] | None = None,
) -> MockObj:
"""Emit the static function for ``id_``; ``suffix`` names a second one, ``share`` reuses an identical one.
``add_global_`` places the declaration (e.g. at a ``LatePvariable``); such a function is never shared.
"""
name = _apply_function_name(id_, suffix)
if add_global_ is not None:
return cg.static_function(name, return_type, params, statements, add_global_)
return cg.shared_static_function(name, return_type, params, statements, share=share)
"""Emit the static function for ``id_``; ``suffix`` names a second one, ``share`` reuses an identical one."""
return cg.shared_static_function(
_apply_function_name(id_, suffix), return_type, params, statements, share=share
)
def _apply_function_name(id_: ID, suffix: str = "fn") -> str:
@@ -1909,31 +1903,19 @@ def _emit_inline_run(
"""
if len(run) == 1 and run[0].emit is not None:
return run[0].emit()
statements = _run_statements(run)
id_ = run[0].id_.copy()
id_.type = ApplyRunAction
return cg.new_Pvariable(
id_, cg.TemplateArguments(_run_function(id_, run, args), *template_arg)
)
def _run_function(
id_: ID,
run: list[_InlineAction],
args: TemplateArgsType,
stoppable: bool = True,
add_global_: Callable[[Statement], object] | None = None,
) -> MockObj:
"""Emit the function that runs ``run``; a stoppable one takes the run count first and
returns once a stop clears it. ``add_global_`` is passed to ``cg.static_function``."""
params = _apply_params(args)
statements = _run_statements(run)
if stoppable:
params = [(cg.RawExpression("const int &"), _RUNNING_PARAM), *params]
else:
statements = [line for line in statements if line != _STOP_CHECK]
return apply_static_function(
id_, cg.void, params, statements, add_global_=add_global_
fn = apply_static_function(
id_,
cg.void,
[
(cg.RawExpression("const int &"), _RUNNING_PARAM),
*_apply_params(args),
],
statements,
)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
async def _build_action_plan(
@@ -2116,10 +2098,10 @@ def _automation_from_plan(
def _is_direct(plan: list[MockObj | list[_InlineAction]], config: ConfigType) -> bool:
"""Whether an automation is all inline actions without a manual id, so it needs no objects.
"""Whether a callback automation is all inline actions, so it needs no objects.
Without a Trigger or a manual id nothing can reach such an automation to stop it, so a
caller that creates no Trigger needs no stop checks either.
Without a Trigger or a manual id nothing can reach such an automation to stop it, so
no stop checks are needed either. Only for callers that create no Trigger.
"""
return (
not config[CONF_AUTOMATION_ID].is_manual
@@ -2132,15 +2114,13 @@ def _direct_function(
plan: list[MockObj | list[_InlineAction]],
args: TemplateArgsType,
config: ConfigType,
id_: ID | None = None,
stoppable: bool = False,
add_global_: Callable[[Statement], object] | None = None,
) -> MockObj | None:
"""The function that runs ``plan`` if it is direct, named after ``id_`` (default: its
first action); see ``_run_function``."""
if not _is_direct(plan, config):
return None
return _run_function(id_ or plan[0][0].id_, plan[0], args, stoppable, add_global_)
statements = [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
return apply_static_function(
plan[0][0].id_, cg.void, _apply_params(args), statements
)
def _when_call(
@@ -2160,29 +2140,6 @@ async def build_automation(
return await new_automation(args, config, trigger)
async def build_automation_or_run_function(
trigger: MockObj,
args: TemplateArgsType,
config: ConfigType,
add_global_: Callable[[Statement], object] | None = None,
) -> MockObj | None:
"""Build ``config`` on ``trigger`` like ``build_automation`` and return None; if every
action is inline, return the stoppable function ``esphome__<trigger id>__fn`` instead."""
plan = await _build_automation_plan(args, config)
if (
fn := _direct_function(
plan,
args,
config,
id_=trigger.base,
stoppable=True,
add_global_=add_global_,
)
) is None:
_automation_from_plan(plan, args, config, trigger)
return fn
async def build_trigger_callback(
args: TemplateArgsType,
config: ConfigType,
-1
View File
@@ -12,7 +12,6 @@ from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LatePvariable,
LineComment,
LogStringLiteral,
MockObj,
@@ -3,6 +3,7 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
#include "esphome/core/progmem.h"
#include <cmath>
@@ -327,8 +328,8 @@ void DFRobotSEN0609Component::parse_response_(const char *args) {
this->response_count_ = 0;
while (this->response_count_ < MAX_RESPONSE_VALUES) {
char *end;
const float value = strtof(args, &end);
// strtof also takes "nan" and "inf", which no setting can hold
const float value = str_to_float(args, &end);
// str_to_float also takes "nan" and "inf", which no setting can hold
if (end == args || !std::isfinite(value))
break;
this->response_[this->response_count_++] = value;
@@ -1,5 +1,6 @@
#include "hydreon_rgxx.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
#include <cstring>
namespace esphome::hydreon_rgxx {
@@ -235,7 +236,7 @@ void HydreonRGxxComponent::process_line_(StringRef line) {
}
// parse value, starting at str position n
float data = strtof(line.c_str() + n, nullptr);
float data = str_to_float(line.c_str() + n, nullptr);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);
+27 -115
View File
@@ -1,5 +1,3 @@
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from esphome import automation
@@ -14,14 +12,12 @@ from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DOMAIN = "script"
script_ns = cg.esphome_ns.namespace("script")
Script = script_ns.class_("Script")
AutomationScript = script_ns.class_(
"AutomationScript", Script, automation.Trigger.template()
)
FunctionBody = script_ns.class_("FunctionBody", Script)
FunctionScript = script_ns.class_("FunctionScript", Script)
FunctionPointerScript = script_ns.class_("FunctionPointerScript", Script)
Script = script_ns.class_("Script", automation.Trigger.template())
ScriptWaitAction = script_ns.class_("ScriptWaitAction", automation.Action, cg.Component)
SingleScript = script_ns.class_("SingleScript", Script)
RestartScript = script_ns.class_("RestartScript", Script)
QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component)
ParallelScript = script_ns.class_("ParallelScript", Script)
CONF_SINGLE = "single"
CONF_QUEUED = "queued"
@@ -29,38 +25,12 @@ CONF_PARALLEL = "parallel"
CONF_MAX_RUNS = "max_runs"
CONF_SCRIPT_STOP = "script.stop"
def _mode(
name: str, *parents: cg.MockObjClass
) -> tuple[cg.MockObjClass, cg.MockObjClass]:
"""The class of a mode with an Automation, and its template on the class that runs."""
return (
script_ns.class_(f"{name}Script", AutomationScript, *parents),
script_ns.class_(f"{name}Mode", Script, *parents),
)
MODES = {
CONF_SINGLE: _mode("Single"),
CONF_RESTART: _mode("Restart"),
CONF_QUEUED: _mode("Queueing", cg.Component),
CONF_PARALLEL: _mode("Parallel"),
SCRIPT_MODES = {
CONF_SINGLE: SingleScript,
CONF_RESTART: RestartScript,
CONF_QUEUED: QueueingScript,
CONF_PARALLEL: ParallelScript,
}
SCRIPT_MODES = {mode: classes[0] for mode, classes in MODES.items()}
@dataclass
class ScriptData:
# By script id: the class script.wait is templated on, or the script.wait actions to
# create once the script's class is known
waits: dict[str, cg.MockObj | list[Callable[[cg.MockObj], None]]] = field(
default_factory=dict
)
def _data() -> ScriptData:
return CORE.data.setdefault(DOMAIN, ScriptData())
PARAMETER_TYPE_TRANSLATIONS = {
"string": "std::string",
@@ -133,20 +103,6 @@ def validate_parameter_name(value):
raise cv.Invalid(f"Script's parameter name cannot be {CONF_ID}")
def validate_parameter_names(value: Any) -> Any:
"""Reject reserved parameter names here: as dict keys of a schema they would only be
reported as "extra keys not allowed"."""
if isinstance(value, dict):
for name in value:
# Generated C++ code names its own variables like this
if str(name).startswith("esphome__"):
raise cv.Invalid(
"Names starting with 'esphome__' are reserved for generated code",
path=[name],
)
return value
ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]<>")
@@ -214,9 +170,10 @@ CONFIG_SCHEMA = cv.All(
*SCRIPT_MODES, lower=True
),
cv.Optional(CONF_MAX_RUNS): cv.int_range(min=0, max=100),
cv.Optional(CONF_PARAMETERS, default={}): cv.All(
validate_parameter_names,
cv.Schema({validate_parameter_name: validate_parameter_type}),
cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
{
validate_parameter_name: validate_parameter_type,
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(single=True),
},
@@ -226,56 +183,28 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Register all variables first, so that scripts can use other scripts. Each is created
# once its actions are built, which decides its class, in the place it was declared.
waits = _data().waits
scripts = []
# Register all variables first, so that scripts can use other scripts
triggers = []
for conf in config:
template, func_args = parameters_to_template(conf[CONF_PARAMETERS])
id_ = conf[CONF_ID].copy()
id_.type = id_.type.template(template)
late = cg.LatePvariable(id_)
waits[id_.id] = []
script = late.var
trigger = cg.new_Pvariable(conf[CONF_ID], template)
# Add a human-readable name to the script
cg.add(script.set_name(cg.LogStringLiteral(id_.id)))
cg.add(trigger.set_name(cg.LogStringLiteral(conf[CONF_ID].id)))
if CONF_MAX_RUNS in conf:
cg.add(script.set_max_runs(conf[CONF_MAX_RUNS]))
cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS]))
if queued := conf[CONF_MODE] == CONF_QUEUED:
await cg.register_component(script, conf)
if conf[CONF_MODE] == CONF_QUEUED:
await cg.register_component(trigger, conf)
scripts.append((late, template, func_args, conf, queued))
triggers.append((trigger, func_args, conf))
if any(CONF_ON_STOP in conf for conf in config):
cg.add_define("USE_SCRIPT_ON_STOP")
for late, template, func_args, conf, queued in scripts:
# The class or its constructor names the function, so it is declared in the script's place
fn = await automation.build_automation_or_run_function(
late.var, func_args, conf, late.global_slot.add
)
script_class = wait_class = late.id.type
ctor_args = []
if fn is not None:
wait_class = MODES[conf[CONF_MODE]][1].template(
FunctionBody.template(template), *template
)
if queued:
# One class for all of them: a per-script class would also copy the
# Component vtable, which costs more flash than the pointer's RAM
script_class = FunctionPointerScript.template(wait_class, *template)
ctor_args = [fn]
else:
script_class = FunctionScript.template(wait_class, fn, *template)
late.define(script_class, *ctor_args)
for create_wait in waits[late.id.id]:
create_wait(wait_class)
waits[late.id.id] = wait_class
for trigger, func_args, conf in triggers:
await automation.build_automation(trigger, func_args, conf)
if (stop_conf := conf.get(CONF_ON_STOP)) is not None:
await automation.build_automation(
late.var.get_stop_trigger(), [], stop_conf
)
await automation.build_automation(trigger.get_stop_trigger(), [], stop_conf)
@automation.register_action(
@@ -362,25 +291,8 @@ automation.register_apply_action(
)
async def script_wait_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
script_waits = _data().waits[full_id.id]
pending = isinstance(script_waits, list)
# Created where it is built, once the class of the script it waits for is known
id_ = action_id.copy()
id_.type = id_.type.template(
full_id.type if pending else script_waits, *template_arg
)
late = cg.LatePvariable(id_)
def create(script_class: cg.MockObj) -> None:
late.define(
action_id.type, cg.TemplateArguments(script_class, *template_arg), paren
)
if pending:
script_waits.append(create)
else:
create(script_waits)
var = late.var
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
await cg.register_component(var, {})
return var
+34 -99
View File
@@ -24,7 +24,7 @@ class ScriptLogger {
};
/// The abstract base class for all script types.
template<typename... Ts> class Script : public ScriptLogger {
template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts...> {
public:
/** Execute a new instance of this script.
*
@@ -32,83 +32,44 @@ template<typename... Ts> class Script : public ScriptLogger {
*/
virtual void execute(Ts...) = 0;
/// Check if any instance of this script is currently running.
virtual bool is_running() = 0;
/// Stop all instances of this script. Fires the on_stop trigger once if any instance was running.
virtual void stop() = 0;
virtual bool is_running() { return this->is_action_running(); }
#ifdef USE_SCRIPT_ON_STOP
/// Stop all instances of this script. Fires the on_stop trigger once if any instance was running.
virtual void stop() {
if (this->is_action_running())
this->stop_running_();
}
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
#else
/// Stop all instances of this script.
virtual void stop() { this->stop_action(); }
#endif
// Internal function to give scripts readable names.
void set_name(const LogString *name) { name_ = name; }
protected:
const LogString *name_{nullptr};
#ifdef USE_SCRIPT_ON_STOP
Trigger<> stop_trigger_;
#endif
};
/// Runs the script's actions through an Automation, for actions that can wait (delay, wait_until, ...).
template<typename... Ts> class AutomationScript : public Script<Ts...>, public Trigger<Ts...> {
protected:
bool body_running_() { return this->is_action_running(); }
int body_num_running_() { return this->automation_parent_->num_running(); }
inline void run_body_(const Ts &...x) ESPHOME_ALWAYS_INLINE { this->trigger(x...); }
void stop_body_() { this->stop_action(); }
};
/** Runs the script's actions as one generated function, when they are all inline.
*
* Like ApplyRunAction, the function gets the run count by reference and returns early once a
* stop clears it. The mode code built on this is shared; FunctionScript or FunctionPointerScript
* supplies the function.
*/
template<typename... Ts> class FunctionBody : public Script<Ts...> {
protected:
bool body_running_() { return this->num_running_ > 0; }
int body_num_running_() { return this->num_running_; }
inline void run_body_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
this->num_running_++;
this->run_function(this->num_running_, x...);
if (this->num_running_ > 0)
this->num_running_--;
}
void stop_body_() { this->num_running_ = 0; }
virtual void run_function(const int &running, const Ts &...x) = 0;
int num_running_{0};
};
/// What all script modes share, on top of the class that runs the actions.
template<class Body, typename... Ts> class ScriptBase : public Body {
public:
bool is_running() override { return this->body_running_(); }
#ifdef USE_SCRIPT_ON_STOP
void stop() override {
if (this->body_running_())
this->stop_running_();
}
#else
void stop() override { this->stop_body_(); }
#endif
protected:
// Publish this script's name as the current source while the actions run (RAII, so nesting
// composes), so deferred work is attributed to it in blocking warnings.
// Run the action chain with this script's name published as the current source (RAII save/restore,
// so nesting composes), so deferred work inside the script is attributed to it in blocking
// warnings. Force-inlined to fold into the always-inlined trigger chain (no extra stack frame).
inline void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
ScopedSourceGuard source_guard{this->name_};
this->run_body_(x...);
this->trigger(x...);
}
// Stop the running actions early and fire the on_stop trigger.
void stop_running_() {
this->stop_body_();
this->stop_action();
#ifdef USE_SCRIPT_ON_STOP
this->stop_trigger_.trigger();
#endif
}
const LogString *name_{nullptr};
#ifdef USE_SCRIPT_ON_STOP
Trigger<> stop_trigger_;
#endif
};
/** A script type for which only a single instance at a time is allowed.
@@ -116,10 +77,10 @@ template<class Body, typename... Ts> class ScriptBase : public Body {
* If a new instance is executed while the previous one hasn't finished yet,
* a warning is printed and the new instance is discarded.
*/
template<class Body, typename... Ts> class SingleMode : public ScriptBase<Body, Ts...> {
template<typename... Ts> class SingleScript : public Script<Ts...> {
public:
void execute(Ts... x) override {
if (this->body_running_()) {
if (this->is_action_running()) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"),
LOG_STR_ARG(this->name_));
return;
@@ -134,16 +95,16 @@ template<class Body, typename... Ts> class SingleMode : public ScriptBase<Body,
* If a new instance is started but another one is already running, the existing
* script is stopped and the new instance starts from the beginning.
*/
template<class Body, typename... Ts> class RestartMode : public ScriptBase<Body, Ts...> {
template<typename... Ts> class RestartScript : public Script<Ts...> {
public:
void execute(Ts... x) override {
if (this->body_running_()) {
if (this->is_action_running()) {
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_));
this->stop_running_();
#ifdef USE_SCRIPT_ON_STOP
// on_stop may have executed this script again; only the newest run may continue
if (this->body_running_())
this->stop_body_();
if (this->is_action_running())
this->stop_action();
#endif
}
@@ -165,10 +126,10 @@ template<class Body, typename... Ts> class RestartMode : public ScriptBase<Body,
* - Subsequent executes while running are queued starting at position 0
* - Maximum total instances = max_runs_ (includes 1 running + (max_runs_ - 1) queued)
*/
template<class Body, typename... Ts> class QueueingMode : public ScriptBase<Body, Ts...>, public Component {
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
public:
void execute(Ts... x) override {
if (this->body_running_() || this->num_queued_ > 0) {
if (this->is_action_running() || this->num_queued_ > 0) {
// num_queued_ is the number of *queued* instances (waiting, not including currently running)
// max_runs_ is the maximum *total* instances (running + queued)
// So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
@@ -203,7 +164,7 @@ template<class Body, typename... Ts> class QueueingMode : public ScriptBase<Body
void stop() override {
#ifdef USE_SCRIPT_ON_STOP
// Queued runs are unfinished work too, even between two runs when nothing is running
const bool stopping = this->body_running_() || this->num_queued_ != 0;
const bool stopping = this->is_action_running() || this->num_queued_ != 0;
#endif
// Clear all queued items to free memory immediately
// Resetting the array automatically destroys all unique_ptrs and their contents
@@ -214,12 +175,12 @@ template<class Body, typename... Ts> class QueueingMode : public ScriptBase<Body
if (stopping)
this->stop_running_();
#else
ScriptBase<Body, Ts...>::stop();
Script<Ts...>::stop();
#endif
}
void loop() override {
if (this->num_queued_ != 0 && !this->body_running_()) {
if (this->num_queued_ != 0 && !this->is_action_running()) {
// Dequeue: decrement count, move tuple out (frees slot), advance read position
this->num_queued_--;
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
@@ -266,10 +227,10 @@ template<class Body, typename... Ts> class QueueingMode : public ScriptBase<Body
* If a new instance is started while previous ones haven't finished yet,
* the new one is executed in parallel to the other instances.
*/
template<class Body, typename... Ts> class ParallelMode : public ScriptBase<Body, Ts...> {
template<typename... Ts> class ParallelScript : public Script<Ts...> {
public:
void execute(Ts... x) override {
if (this->max_runs_ != 0 && this->body_num_running_() >= this->max_runs_) {
if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"),
LOG_STR_ARG(this->name_));
return;
@@ -282,32 +243,6 @@ template<class Body, typename... Ts> class ParallelMode : public ScriptBase<Body
int max_runs_ = 0;
};
template<typename... Ts> using SingleScript = SingleMode<AutomationScript<Ts...>, Ts...>;
template<typename... Ts> using RestartScript = RestartMode<AutomationScript<Ts...>, Ts...>;
template<typename... Ts> using QueueingScript = QueueingMode<AutomationScript<Ts...>, Ts...>;
template<typename... Ts> using ParallelScript = ParallelMode<AutomationScript<Ts...>, Ts...>;
/// A script of mode `Mode` (built on FunctionBody) whose actions are the generated function Fn.
template<class Mode, auto Fn, typename... Ts> class FunctionScript final : public Mode {
protected:
void run_function(const int &running, const Ts &...x) override { Fn(running, x...); }
};
/** Like FunctionScript, but one class for all scripts of `Mode`, holding the function.
*
* For queued scripts, whose per-script class would copy the Component vtable too.
*/
template<class Mode, typename... Ts> class FunctionPointerScript final : public Mode {
public:
using fn_t = void (*)(const int &, const Ts &...);
explicit FunctionPointerScript(fn_t fn) : fn_(fn) {}
protected:
void run_function(const int &running, const Ts &...x) override { this->fn_(running, x...); }
const fn_t fn_;
};
/** Wait for a script to finish before continuing.
*
* Uses queue-based storage to safely handle concurrent executions.
+5 -4
View File
@@ -1,5 +1,6 @@
#include "smt100.h"
#include "esphome/core/log.h"
#include "esphome/core/parse_float.h"
namespace esphome::smt100 {
@@ -24,19 +25,19 @@ void SMT100Component::loop() {
token = strtok(nullptr, ",");
if (!token)
return;
float permittivity = (float) strtod(token, nullptr);
float permittivity = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float moisture = (float) strtod(token, nullptr);
float moisture = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float temperature = (float) strtod(token, nullptr);
float temperature = str_to_float(token, nullptr);
token = strtok(nullptr, ",");
if (!token)
return;
float voltage = (float) strtod(token, nullptr);
float voltage = str_to_float(token, nullptr);
PUBLISH_STATE(this->counts_sensor_, counts);
-51
View File
@@ -337,57 +337,6 @@ size_t truncate_name_to(char *buffer, size_t buffer_size, const char *name, size
// Parsing & formatting
// A plain decimal converts exactly with one division: the digits accumulate
// into an integer that stays below 2^24 and the fraction's scale is a power of
// ten up to 1e9 (2^9 * 5^9), so both convert to float exactly and IEEE division
// rounds the quotient correctly, the same result strtof produces.
static constexpr uint32_t PARSE_FLOAT_MANTISSA_LIMIT = 1000000; // one more digit stays below 2^24
static constexpr uint32_t PARSE_FLOAT_MAX_FRACTION_DIGITS = 9;
optional<float> parse_float(const char *str) {
const char *p = str;
const bool negative = *p == '-';
if (negative || *p == '+')
p++;
const char *digits = p;
const char *dot = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<unsigned char>(*p) - '0';
if (d > 9) {
if (*p == '.' && dot == nullptr) {
dot = p;
continue;
}
break;
}
if (mantissa >= PARSE_FLOAT_MANTISSA_LIMIT)
break;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction_digits = dot != nullptr ? p - dot - 1 : 0;
const bool any_digit = p - digits > (dot != nullptr ? 1 : 0);
if (*p == '\0' && any_digit && fraction_digits <= PARSE_FLOAT_MAX_FRACTION_DIGITS) {
// The mantissa is exact in a float, so the sign goes on before the one rounding step
float value = static_cast<float>(mantissa);
if (negative)
value = -value;
if (fraction_digits != 0) {
uint32_t scale = 10;
for (uint32_t i = 1; i < fraction_digits; i++)
scale *= 10;
value /= static_cast<float>(scale);
}
return value;
}
// Exponents, hex, inf, nan, leading whitespace and longer inputs take the libc path.
char *end = nullptr;
const float value = ::strtof(str, &end);
if (end == str || *end != '\0' || value == HUGE_VALF)
return {};
return value;
}
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
size_t chars = std::min(length, 2 * count);
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
+21 -2
View File
@@ -25,6 +25,7 @@
#include <strings.h>
#include "esphome/core/optional.h"
#include "esphome/core/parse_float.h"
#include "esphome/core/time_conversion.h"
#ifdef USE_ESP8266
@@ -1033,6 +1034,26 @@ inline float pow10_int(int8_t exp) {
return result;
}
/// 10^n, exact while it fits in T.
template<typename T> constexpr T pow10_exact(uint32_t n) {
T result = 1;
T base = 10;
for (;;) {
if ((n & 1) != 0)
result *= base;
n >>= 1;
if (n == 0)
return result;
base *= base;
}
}
/// Whether c is '0' to '9'.
constexpr bool is_digit(char c) { return static_cast<uint8_t>(c - '0') <= 9; }
/// Whether c is C-locale white space, without isspace's ctype table.
constexpr bool is_space_c(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
/// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out).
template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max_out) {
return (value - min) * (max_out - min_out) / (max - min) + min_out;
@@ -1573,8 +1594,6 @@ template<typename T, enable_if_t<(std::is_integral<T>::value && std::is_signed<T
optional<T> parse_number(const std::string &str) {
return parse_number<T>(str.c_str());
}
/// Parse a decimal floating-point number; plain decimals skip the libc parser.
optional<float> parse_float(const char *str);
/// Parse a decimal floating-point number from a null-terminated string.
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0> optional<T> parse_number(const char *str) {
return parse_float(str);
+372
View File
@@ -0,0 +1,372 @@
// picolibc 1.8.10's strtof, bit for bit, without libc's strtod: decimals round to 9 digits, then to the nearest float.
// Float bits are built from integers, so chips that flush denormals give the same result.
#include "esphome/core/parse_float.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <bit>
namespace esphome {
namespace {
constexpr uint32_t FLOAT_SIGN_BIT = 0x80000000, FLOAT_INF_BITS = 0x7F800000;
constexpr uint32_t FLOAT_NAN_BITS = 0x7FC00000; // nanf(""), which strtof returns for every NaN
constexpr uint32_t DECIMAL_DIGITS = 9, HEX_DIGITS = 8; // digits kept before the rest only round
constexpr int32_t EXPONENT_CAP = 224; // picolibc stops adding exponent digits once the exponent reaches this
// 0.<digits> * 10^exp10 is zero at or below MIN_EXP10 and infinity at or above MAX_EXP10, as in picolibc
constexpr int32_t MIN_EXP10 = -46, MAX_EXP10 = 40;
// Past this, one more digit could pass 2^24
constexpr uint32_t PLAIN_MANTISSA_LIMIT = 1677720;
// Bits of the float nearest to (m + sticky fraction) * 2^e, ties to even; m > 0
uint32_t round_to_float_bits(uint64_t m, int32_t e, bool sticky) {
const int32_t msb = e + static_cast<int32_t>(std::bit_width(m)) - 1; // the value is in [2^msb, 2^(msb + 1))
if (msb >= 128)
return FLOAT_INF_BITS;
if (msb < -150) // below half the smallest float
return 0;
// The unit of the float's last bit: 2^(msb - 23), or 2^-149 for a denormal
const int32_t shift = std::max<int32_t>(msb - 23, -149) - e; // at most 64
uint64_t q;
if (shift <= 0) {
q = m << -shift;
} else {
q = shift < 64 ? m >> shift : 0;
const uint64_t rem = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
const uint64_t half = uint64_t{1} << (shift - 1);
if (rem > half || (rem == half && (sticky || (q & 1) != 0)))
q++;
}
if (msb < -126)
return static_cast<uint32_t>(q); // 2^23 here is the smallest normal float, as it should be
// Rounding up past the largest float gives exactly FLOAT_INF_BITS
return (static_cast<uint32_t>(msb + 127) << 23) + static_cast<uint32_t>(q - (uint64_t{1} << 23));
}
// The decimal exponents that reach decimal_to_float_bits: 1 to 9 digits and MIN_EXP10 < digits + e10 < MAX_EXP10
constexpr int32_t MIN_E10 = MIN_EXP10 + 1 - static_cast<int32_t>(DECIMAL_DIGITS), MAX_E10 = MAX_EXP10 - 2;
// Top 64 bits of 10^(8k), rounded down: 10^(8k) = entry * 2^(floor_log2_pow10(8k) - 63), exact for k = 0..3
constexpr int32_t POW10_BY_8_FIRST = MIN_E10 >> 3, POW10_BY_8_LAST = MAX_E10 >> 3;
constexpr uint64_t POW10_BY_8[] PROGMEM = {
0xFB158592BE068D2E, 0xBB127C53B17EC159, 0x8B61313BBABCE2C6, 0xCFB11EAD453994BA,
0x9ABE14CD44753B52, 0xE69594BEC44DE15B, 0xABCC77118461CEFC, 0x8000000000000000,
0xBEBC200000000000, 0x8E1BC9BF04000000, 0xD3C21BCECCEDA100, 0x9DC5ADA82B70B59D,
};
static_assert(POW10_BY_8_FIRST == -7 && POW10_BY_8_LAST == 4 &&
sizeof(POW10_BY_8) / sizeof(POW10_BY_8[0]) == POW10_BY_8_LAST - POW10_BY_8_FIRST + 1,
"POW10_BY_8 must cover MIN_E10..MAX_E10");
// floor(q * log2(10)), for |q| < 100
constexpr int32_t floor_log2_pow10(int32_t q) { return (q * 217706) >> 16; }
// The top 64 bits of a * b, rounded down
uint64_t mul_high(uint64_t a, uint64_t b) {
const uint64_t a_lo = static_cast<uint32_t>(a), a_hi = a >> 32;
const uint64_t b_lo = static_cast<uint32_t>(b), b_hi = b >> 32;
const uint64_t hi_lo = a_hi * b_lo, lo_hi = a_lo * b_hi;
const uint64_t middle = ((a_lo * b_lo) >> 32) + static_cast<uint32_t>(hi_lo) + static_cast<uint32_t>(lo_hi);
return a_hi * b_hi + (hi_lo >> 32) + (lo_hi >> 32) + (middle >> 32);
}
// Cold: a decimal within 16 units of a midpoint is an exact tie when m10 * 5^e10 is an integer
__attribute__((noinline, cold)) uint32_t tie_float_bits(uint32_t m10, int32_t e10, uint32_t bits) {
if (e10 >= 0) {
if (e10 <= 14) // m10 * 5^14 stays below 2^64
return round_to_float_bits(m10 * (pow10_exact<uint64_t>(e10) >> e10), e10, false);
} else if (e10 >= -13) {
const auto pow5 = static_cast<uint32_t>(pow10_exact<uint64_t>(-e10) >> -e10);
if (m10 % pow5 == 0)
return round_to_float_bits(m10 / pow5, e10, false);
}
return bits;
}
// The float nearest to m10 * 10^e10 (0 < m10 <= 10^9). The product is under 4 units low; checked for all 85 billion
// (m10, e10), only exact ties and one non-tie (6 units off, keeps its float) come within 34 units of a midpoint.
uint32_t decimal_to_float_bits(uint32_t m10, int32_t e10) {
// Both operands are exact floats, so the one float operation gives the nearest float
const uint32_t power = e10 < 0 ? -e10 : e10;
if (m10 < (1u << 24) && power <= 10) {
const auto value = static_cast<float>(m10);
// 10^10 overflows uint32_t
const float factor = power == 10 ? 1e10f : static_cast<float>(pow10_exact<uint32_t>(power));
return bit_cast<uint32_t>(e10 < 0 ? value / factor : value * factor);
}
const int32_t k8 = e10 >> 3; // rounds down
const uint64_t x = uint64_t{m10} * pow10_exact<uint32_t>(e10 & 7);
const auto zeros = static_cast<int32_t>(std::countl_zero(x));
uint64_t m = x << zeros;
int32_t e = -zeros;
if (k8 == 0)
return round_to_float_bits(m, e, false); // exact
m = mul_high(m, POW10_BY_8[k8 - POW10_BY_8_FIRST]);
e += floor_log2_pow10(8 * k8) + 1;
if ((m >> 63) == 0) {
m <<= 1;
e--;
}
const uint32_t bits = round_to_float_bits(m, e, false);
// The bits below the float's last one, and the midpoint among them
const int32_t shift = std::max<int32_t>(40, -149 - e);
if (shift <= 64) {
const uint64_t low = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
const uint64_t half = uint64_t{1} << (shift - 1);
if (low - half + 16 > 32) // |low - half| > 16
return bits;
}
return tie_float_bits(m10, e10, bits);
}
uint32_t no_number(const char *str, const char **end, bool *negative) {
*end = str;
*negative = false;
return 0;
}
// The rest of an "nan" or "inf" after its first letter at p; returns the end, or nullptr for no number
const char *read_inf_nan(const char *p) {
const auto lower = [](char c) { return static_cast<char>(c | 0x20); };
if (lower(*p) == 'n') {
if (lower(p[1]) != 'a' || lower(p[2]) != 'n')
return nullptr;
p += 3;
if (*p != '(')
return p;
// "nan(" must be closed after letters, digits and '_'
do {
p++;
} while (static_cast<uint8_t>(lower(*p) - 'a') < 26 || is_digit(*p) || *p == '_');
return *p == ')' ? p + 1 : nullptr;
}
if (lower(p[1]) != 'n' || lower(p[2]) != 'f')
return nullptr;
p += 3;
if (lower(*p) != 'i')
return p;
// Past "infi", only "infinity" is a number
return lower(p[1]) == 'n' && lower(p[2]) == 'i' && lower(p[3]) == 't' && lower(p[4]) == 'y' ? p + 5 : nullptr;
}
// Reads a binary exponent "p" or decimal exponent "e" at p into *exp; returns the end of the number
const char *read_exponent(const char *p, char letter, int32_t *exp) {
if ((*p | 0x20) != letter)
return p;
const char *q = p + 1;
const bool negative = *q == '-';
if (negative || *q == '+')
q++;
if (!is_digit(*q))
return p;
int32_t value = 0;
for (; is_digit(*q); q++) {
if (value < EXPONENT_CAP)
value = value * 10 + (*q - '0');
}
*exp += negative ? -value : value;
return q;
}
// Hex digits after the "x" at p, with the point and decimal exponent read so far (no non-zero digit yet)
__attribute__((noinline, cold)) uint32_t read_hex(const char *p, bool dot, int32_t exp, const char **end) {
uint32_t m = 0, kept = 0; // kept: digits in m from its first non-zero one
bool sticky = false;
for (;; p++) {
const char lower = static_cast<char>(*p | 0x20);
uint32_t digit = static_cast<uint8_t>(*p - '0');
if (static_cast<uint8_t>(lower - 'a') <= 5) {
digit = static_cast<uint8_t>(lower - 'a') + 10;
} else if (digit > 9) {
digit = 16; // picolibc takes ':' to '?' as 10 to 15; we stop there like newlib
}
if (digit < 16) {
if (kept < HEX_DIGITS) {
if (dot)
exp--;
m = m * 16 + digit;
kept += m != 0 ? 1 : 0;
} else {
if (!dot)
exp++;
sticky |= digit != 0;
}
} else if (*p == '.' && !dot) {
dot = true;
} else if (lower != 'x' || m != 0) { // picolibc takes another "x" while the value is still zero
break;
}
}
exp *= 4;
*end = read_exponent(p, 'p', &exp);
return m == 0 ? 0 : round_to_float_bits(m, exp, sticky);
}
// picolibc's conv_flt for a float from the first digit or point at p: returns the bits; *end is str for no number
__attribute__((noinline)) uint32_t read_decimal(const char *str, const char *p, const char **end) {
uint32_t m = 0, kept = 0; // the first (up to 9) significant digits, and how many from the first non-zero one
int32_t exp = 0; // the value is m * 10^exp
bool dot = false, any = false;
uint32_t overflow = 0; // digits past the kept ones: 0 none, 1 below half, 2 half, 3 above half
bool full = false; // a digit past the kept ones was read
for (;; p++) {
const uint32_t digit = static_cast<uint8_t>(*p - '0');
if (digit <= 9) {
any = true;
if (kept < DECIMAL_DIGITS) {
if (dot)
exp--;
m = m * 10 + digit;
kept += m != 0 ? 1 : 0;
} else {
if (!dot)
exp++;
if (!full) {
full = true;
overflow = digit > 5 ? 3 : digit == 5 ? 2 : digit != 0 ? 1 : 0; // the first one decides the half
} else {
overflow |= digit != 0 ? 1 : 0;
}
}
} else if (*p == '.' && !dot) {
dot = true;
} else {
break;
}
}
if ((*p | 0x20) == 'x' && any && m == 0)
return read_hex(p + 1, dot, exp, end);
if (!any) {
*end = str;
return 0;
}
*end = read_exponent(p, 'e', &exp);
if (m == 0)
return 0;
// Ties to even on the decimal digits
overflow |= m & 1;
m += (overflow + 1) >> 2;
const int32_t exp10 = static_cast<int32_t>(kept) + exp;
if (exp10 <= MIN_EXP10)
return 0;
if (exp10 >= MAX_EXP10)
return FLOAT_INF_BITS;
return decimal_to_float_bits(m, exp);
}
// picolibc's strtof from the start: returns the bits without the sign, which goes in *negative
__attribute__((noinline)) uint32_t parse_float_full(const char *str, const char **end, bool *negative) {
// No number starts with a letter but i (inf) or n (nan): "unavailable", "on" and "unknown" stop here
const char first = static_cast<char>(*str | 0x20);
if (first >= 'a' && first <= 'z' && first != 'i' && first != 'n')
return no_number(str, end, negative);
const char *p = str;
while (is_space_c(*p))
p++;
*negative = *p == '-';
if (*negative || *p == '+')
p++;
const char lower = static_cast<char>(*p | 0x20);
if (lower == 'i' || lower == 'n') {
const char *stop = read_inf_nan(p);
if (stop == nullptr)
return no_number(str, end, negative);
*end = stop;
return lower == 'i' ? FLOAT_INF_BITS : FLOAT_NAN_BITS;
}
const uint32_t bits = read_decimal(str, p, end);
if (*end == str)
*negative = false;
return bits;
}
// A plain decimal below 2^24 with up to 9 digits after the point, in one float operation; nullptr otherwise
ESPHOME_ALWAYS_INLINE inline const char *parse_plain_decimal(const char *str, uint32_t *bits, bool *negative) {
const char *p = str;
*negative = *p == '-';
if (*negative || *p == '+')
p++;
const char *digits = p;
const char *point = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<uint8_t>(*p) - '0';
if (d > 9) {
if (*p == '.' && point == nullptr) {
point = p;
continue;
}
break;
}
if (mantissa > PLAIN_MANTISSA_LIMIT)
return nullptr;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
const char c = static_cast<char>(*p | 0x20);
if (p - digits == (point != nullptr ? 1 : 0) || fraction > 9 || c == 'e' || c == 'x')
return nullptr;
float value = static_cast<float>(mantissa);
if (fraction != 0)
value /= static_cast<float>(pow10_exact<uint32_t>(fraction)); // 10^9 is an exact float
*bits = bit_cast<uint32_t>(value);
return p;
}
} // namespace
optional<float> parse_float(const char *str) {
// Own copy of the plain-decimal loop: sharing parse_plain_decimal costs 12 to 18% more instructions
const char *p = str;
const bool negative = *p == '-';
if (negative || *p == '+')
p++;
const char *digits = p;
const char *point = nullptr;
uint32_t mantissa = 0;
for (;; p++) {
const uint32_t d = static_cast<uint8_t>(*p) - '0';
if (d > 9) {
if (*p == '.' && point == nullptr) {
point = p;
continue;
}
break;
}
if (mantissa > PLAIN_MANTISSA_LIMIT)
break;
mantissa = mantissa * 10 + d;
}
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
if (*p == '\0' && p - digits > (point != nullptr ? 1 : 0) && fraction <= 9) {
float value = negative ? -static_cast<float>(mantissa) : static_cast<float>(mantissa);
if (fraction != 0)
value /= static_cast<float>(pow10_exact<uint32_t>(fraction));
return value;
}
const char *end;
bool full_negative;
const uint32_t bits = parse_float_full(str, &end, &full_negative);
// HUGE_VALF, positive overflow or "inf", is rejected like before
if (end == str || *end != '\0' || (!full_negative && bits == FLOAT_INF_BITS))
return {};
return bit_cast<float>(full_negative ? bits | FLOAT_SIGN_BIT : bits);
}
float str_to_float(const char *str, char **end) {
const char *stop;
bool negative;
uint32_t bits;
if (const char *plain_end = parse_plain_decimal(str, &bits, &negative)) {
stop = plain_end;
} else {
bits = parse_float_full(str, &stop, &negative);
}
if (end != nullptr)
*end = const_cast<char *>(stop); // strtof's signature
return bit_cast<float>(negative ? bits | FLOAT_SIGN_BIT : bits);
}
} // namespace esphome
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#include "esphome/core/optional.h"
namespace esphome {
/// The whole string as str_to_float's syntax (leading space, hex, inf, nan); trailing characters, "inf" and positive
/// overflow are rejected, while -inf, negative overflow and NaN are accepted.
optional<float> parse_float(const char *str);
/// strtof with picolibc's results on every platform, but it never sets errno (overflow gives +-HUGE_VALF, underflow 0);
/// use it instead of strtof, or strtod/atof for a float result.
float str_to_float(const char *str, char **end);
} // namespace esphome
+3 -2
View File
@@ -7,6 +7,7 @@
#include <memory>
#include <string>
#include "esphome/core/defines.h"
#include "esphome/core/parse_float.h"
#ifdef USE_JSON
#include "esphome/components/json/json_util.h"
@@ -293,7 +294,7 @@ inline bool operator!=(const std::string &lhs, const ProgmemStringRef &rhs) { re
#endif
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
// Must be in esphome namespace for ADL to find them. Uses strtol, strtod and str_to_float to avoid heap allocation.
namespace internal {
// NOLINTBEGIN(google-runtime-int)
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
@@ -334,7 +335,7 @@ inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
return internal::parse_number<long>(str, pos, base, std::strtol);
}
inline float stof(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<float>(str, pos, std::strtof);
return internal::parse_number<float>(str, pos, str_to_float);
}
inline double stod(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<double>(str, pos, std::strtod);
+14 -65
View File
@@ -803,18 +803,6 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
if type_ is not None:
id_.type = type_
_emit_pvariable(id_, rhs, CORE.add_global, CORE.add)
CORE.register_variable(id_, obj)
return obj
def _emit_pvariable(
id_: ID,
rhs: SafeExpType,
add_global_: Callable[[Expression | Statement], object],
add_: Callable[[Expression | Statement], object],
) -> None:
"""Emit the storage, declaration and construction of the pointer variable ``id_``."""
if isinstance(rhs, MockObj) and rhs.is_new_expr:
# For 'new' allocations, use placement new into static storage
# to avoid heap fragmentation on embedded devices.
@@ -837,14 +825,14 @@ def _emit_pvariable(
storage_name = _pstorage_name(actual_type, id_.id)
# Declare aligned byte array for the object storage
add_global_(
CORE.add_global(
RawStatement(
f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];"
)
)
# Pointer declaration uses id_.type to preserve the declared base-class
# pointer type for downstream callers (polymorphism through base ptr).
add_global_(
CORE.add_global(
AssignmentExpression(
f"static {pointer_type}",
"*const ",
@@ -853,48 +841,15 @@ def _emit_pvariable(
)
)
placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
add_(ExpressionStatement(placement_new))
CORE.add(ExpressionStatement(placement_new))
CORE.data.setdefault(_STATIC_POINTER_IDS, set()).add(id_.id)
else:
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
add_global_(decl)
add_(AssignmentExpression(None, None, id_, rhs))
CORE.add_global(decl)
CORE.add(AssignmentExpression(None, None, id_, rhs))
class _StatementSlot(Statement):
"""A place for statements that are added later."""
__slots__ = ("statements",)
def __init__(self) -> None:
self.statements: list[Statement] = []
def add(self, expression: Expression | Statement) -> None:
self.statements.append(statement(expression))
def __str__(self) -> str:
return "\n".join(str(item).rstrip() for item in self.statements)
class LatePvariable:
"""A pointer variable registered now, so other code can refer to ``var``, and created by
``define()`` in the place it was declared, once its class is known."""
def __init__(self, id_: ID) -> None:
self.id = id_
self.var = MockObj(id_, "->")
# Global declarations that must come before the variable can be added here
self.global_slot = _StatementSlot()
self._main_slot = _StatementSlot()
CORE.add_global(self.global_slot)
CORE.add(self._main_slot)
CORE.register_variable(id_, self.var)
def define(self, type_: "MockObjClass", *args: SafeExpType) -> None:
"""Create the variable as ``type_``, like ``new_Pvariable`` with ``args``."""
self.id.type = type_
rhs = _new_expression(self.id, args)
_emit_pvariable(self.id, rhs, self.global_slot.add, self._main_slot.add)
CORE.register_variable(id_, obj)
return obj
_STATIC_POINTER_IDS = "static_pointer_ids"
@@ -916,21 +871,16 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
with the given arguments.
:param id_: The ID used to declare the variable (also specifies the type).
:param args: The values to pass to the constructor, after optional template arguments.
:param args: The values to pass to the constructor.
:return: The new variable as a MockObj.
"""
if args and isinstance(args[0], TemplateArguments):
id_ = id_.copy()
return Pvariable(id_, _new_expression(id_, args))
def _new_expression(id_: ID, args: tuple[SafeExpType, ...]) -> "MockObj":
"""``new`` of ``id_.type``, first templated on ``args[0]`` if that is TemplateArguments."""
if args and isinstance(args[0], TemplateArguments):
id_.type = id_.type.template(args[0])
args = args[1:]
return id_.type.new(*args)
rhs = id_.type.new(*args)
return Pvariable(id_, rhs)
def static_function(
@@ -938,19 +888,18 @@ def static_function(
return_type: SafeExpType,
parameters: TemplateArgsType,
body: list[str | Statement],
add_global_: Callable[[Statement], object] | None = None,
) -> RawExpression:
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
expression naming it, for use as a template argument or a function pointer.
The declaration goes with the other globals (or to ``add_global_``, e.g. a
``LatePvariable``'s place), so a template argument can name the function at once; the body
goes after the user includes, where every id and everything those headers declare is in scope.
The declaration goes with the other globals, so a template argument can name the function
at once; the body goes after the user includes, where every id and everything those
headers declare is in scope.
"""
signature = (
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
)
(add_global_ or add_global)(RawStatement(f"{signature};"))
add_global(RawStatement(f"{signature};"))
add_global_after_includes(
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
)
@@ -0,0 +1,40 @@
#include <benchmark/benchmark.h>
#include "esphome/core/helpers.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
static const char *const PLAIN[] = {"0.5", "255", "-12.75", "21.3"};
static const char *const EXPONENT[] = {"1e3", "2.5e-2", "-6.02E5", "1.5e+1"};
static const char *const PRECISE[] = {"3.14159265358979", "0.000123456789", "123456789.125", "-0.1000000000000000055"};
// Home Assistant states that are not numbers
static const char *const REJECT[] = {"unavailable", "unknown", "on", "off"};
template<size_t N, typename F> static void run(benchmark::State &state, const char *const (&inputs)[N], F parse) {
for (auto _ : state) {
float sum = 0.0f;
for (int i = 0; i < kInnerIterations; i++)
sum += parse(inputs[i % N]);
benchmark::DoNotOptimize(sum);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
static float parse_number_float(const char *str) { return parse_number<float>(str).value_or(0.0f); }
static void ParseNumberFloat_Plain(benchmark::State &state) { run(state, PLAIN, parse_number_float); }
BENCHMARK(ParseNumberFloat_Plain);
static void ParseNumberFloat_Exponent(benchmark::State &state) { run(state, EXPONENT, parse_number_float); }
BENCHMARK(ParseNumberFloat_Exponent);
static void ParseNumberFloat_Precise(benchmark::State &state) { run(state, PRECISE, parse_number_float); }
BENCHMARK(ParseNumberFloat_Precise);
static void ParseNumberFloat_Reject(benchmark::State &state) { run(state, REJECT, parse_number_float); }
BENCHMARK(ParseNumberFloat_Reject);
} // namespace esphome::benchmarks
@@ -14,8 +14,7 @@ def test_nec_field_lambdas_get_the_script_args(
main_cpp = generate_main(component_config_path("nec_lambda_args.yaml"))
(body,) = protocol_call_bodies(main_cpp)
assert re.search(
r"static void esphome__\w+__fn\(const int & esphome__running, "
r"const std::remove_cvref_t<int> & x\) \{",
r"static void esphome__\w+__fn\(const std::remove_cvref_t<int> & x\) \{",
main_cpp,
)
assert "apply_call.data.address = [](int x) -> uint16_t {" in body
@@ -51,10 +51,9 @@ def test_code_storage_id_warns(
def test_no_action_objects(main_cpp: str) -> None:
assert "RawAction" not in main_cpp
assert "RCSwitch" not in main_cpp.replace("RCSwitchBase", "")
# The script runs its transmits from one generated function; the buttons call functions directly
assert "new(applyaction_id" not in main_cpp
assert "new(automation_id" not in main_cpp
assert "FunctionScript<" in main_cpp
# The script's two transmits merge into one action; the buttons call functions directly
assert main_cpp.count("new(applyaction_id") == 1
assert main_cpp.count("new(automation_id") == 1
def test_raw_static_code_in_flash(main_cpp: str, bodies: list[str]) -> None:
@@ -33,50 +33,24 @@ def _actions_of(main_cpp: str, automation: str) -> list[str]:
return re.findall(rf"\b{automation}->add_action\((\w+)\);", main_cpp)
def _template_tail(types: tuple[str, ...]) -> str:
return "".join(f", {type_}" for type_ in types)
def _single_action(main_cpp: str, script: str) -> str:
(action,) = _actions_of(main_cpp, _automation(main_cpp, script))
return action
def _mode_class(mode: str, *types: str) -> str:
"""The mode class shared by scripts that run a function, with the same parameters."""
return f"script::{mode}<script::FunctionBody<{', '.join(types)}>{_template_tail(types)}>"
def _run_function(main_cpp: str, script: str) -> str:
"""Return the function of the one merged action the script's Automation runs."""
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
match = re.match(r"ApplyRunAction<(\w+)[,>]", constructed)
assert match is not None, constructed
return match.group(1)
def _function_script(mode: str, script: str, *types: str) -> str:
"""The class of a script whose actions run as one function."""
return (
f"script::FunctionScript<{_mode_class(mode, *types)}, "
f"esphome__{script}__fn{_template_tail(types)}>"
)
def _assert_function_script(main_cpp: str, mode: str, script: str, *types: str) -> None:
assert (
_constructed(main_cpp, script) == f"{_function_script(mode, script, *types)}()"
)
def _wait_class(script_class: str) -> str:
return f"script::ScriptWaitAction<{script_class}>"
def _script_body(main_cpp: str, script: str) -> str:
"""The function a script runs; it has no Automation."""
assert not re.search(rf"\bAutomation<.*?>\({script}\);", main_cpp)
return _function_body(main_cpp, f"esphome__{script}__fn")
def test_inline_actions_run_as_one_function(generate_main) -> None:
"""A script of inline actions calls one function, declared before the script."""
def test_inline_actions_merge_into_one_action(generate_main) -> None:
"""A script of inline actions runs them as one merged action of an Automation."""
main_cpp = generate_main(CONFIG)
class_ = _function_script("SingleMode", "inline_single")
assert _constructed(main_cpp, "inline_single") == f"{class_}()"
declaration = main_cpp.index(
"static void esphome__inline_single__fn(const int & esphome__running);"
)
assert declaration < main_cpp.index(f"alignas({class_})")
assert f"static {class_} *const inline_single" in main_cpp
body = _script_body(main_cpp, "inline_single")
assert _constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
body = _function_body(main_cpp, _run_function(main_cpp, "inline_single"))
assert '"INLINE first"' in body
assert "counter->value() += 1;" in body
# A script can be stopped from inside, so the rest of the run checks for it
@@ -84,37 +58,31 @@ def test_inline_actions_run_as_one_function(generate_main) -> None:
def test_one_lambda_action(generate_main) -> None:
"""A single inline action is the whole function."""
"""A single inline action keeps its own action object."""
main_cpp = generate_main(CONFIG)
body = _script_body(main_cpp, "one_action")
assert '"ONE action"' in body
assert "esphome__running" not in body
constructed = _constructed(main_cpp, _single_action(main_cpp, "one_action"))
assert constructed.startswith("StatelessLambdaAction<>(")
assert '"ONE action"' in constructed
def test_modes(generate_main) -> None:
"""Each mode has its own template; max_runs is set on queued and parallel scripts."""
"""Each mode has its own class; max_runs is set on queued and parallel scripts."""
main_cpp = generate_main(CONFIG)
for mode, script in (
("RestartMode", "inline_restart"),
("ParallelMode", "inline_parallel"),
):
_assert_function_script(main_cpp, mode, script)
# One class for all queued scripts, given the function
assert _constructed(main_cpp, "inline_queued") == (
f"script::FunctionPointerScript<{_mode_class('QueueingMode')}>"
"(esphome__inline_queued__fn)"
)
assert _constructed(main_cpp, "inline_restart") == "script::RestartScript<>()"
assert _constructed(main_cpp, "inline_queued") == "script::QueueingScript<>()"
assert _constructed(main_cpp, "inline_parallel") == "script::ParallelScript<>()"
assert "inline_queued->set_max_runs(3);" in main_cpp
assert "inline_parallel->set_max_runs(2);" in main_cpp
assert re.search(r"App\.register_component_\(inline_queued, \d+\);", main_cpp)
assert '"QUEUED run"' in _script_body(main_cpp, "inline_queued")
assert '"PARALLEL run"' in _script_body(main_cpp, "inline_parallel")
for script in ("inline_queued", "inline_parallel"):
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
assert constructed.startswith("StatelessLambdaAction<>(")
def test_restart_executes_itself(generate_main) -> None:
"""A script can execute itself; on_stop keeps its own Automation."""
"""A script can execute itself; on_stop has its own Automation."""
main_cpp = generate_main(CONFIG)
body = _script_body(main_cpp, "inline_restart")
body = _function_body(main_cpp, _run_function(main_cpp, "inline_restart"))
assert "::inline_restart->execute();" in body
stop = _automation(main_cpp, r"inline_restart->get_stop_trigger\(\)")
(action,) = _actions_of(main_cpp, stop)
@@ -124,14 +92,18 @@ def test_restart_executes_itself(generate_main) -> None:
def test_parameters_by_value(generate_main) -> None:
"""Lambdas take the parameters by value, so ``x *= 2`` compiles in every form."""
main_cpp = generate_main(CONFIG)
_assert_function_script(main_cpp, "SingleMode", "with_params", "int", "std::string")
assert (
_constructed(main_cpp, "with_params")
== "script::SingleScript<int, std::string>()"
)
name = _run_function(main_cpp, "with_params")
assert re.search(
r"static void esphome__with_params__fn\(const int & esphome__running, "
rf"static void {name}\(const int & esphome__running, "
r"const std::remove_cvref_t<int> & x, "
r"const std::remove_cvref_t<std::string> & s\)",
main_cpp,
)
body = _script_body(main_cpp, "with_params")
body = _function_body(main_cpp, name)
assert body.count("[](int x, std::string s) -> void {") == 2
assert body.count("[](int x, std::string s) -> int {") == 2
assert "x *= 2;\n ESP_LOGI(" in body
@@ -144,7 +116,7 @@ def test_parameters_by_value(generate_main) -> None:
def test_script_stop_inside(generate_main) -> None:
"""``script.stop`` without an id stops the script it is in; the next action checks."""
main_cpp = generate_main(CONFIG)
body = _script_body(main_cpp, "stops_itself")
body = _function_body(main_cpp, _run_function(main_cpp, "stops_itself"))
stop, rest = body.split("::stops_itself->stop();")
assert '"STOPS start"' in stop
assert rest.lstrip("\n }").startswith("if (!esphome__running)\n return;")
@@ -154,59 +126,35 @@ def test_script_stop_inside(generate_main) -> None:
def test_calls_itself_with_parameter(generate_main) -> None:
"""A parallel script with a parameter can execute itself from an ``if``."""
main_cpp = generate_main(CONFIG)
_assert_function_script(main_cpp, "ParallelMode", "calls_itself", "int")
body = _script_body(main_cpp, "calls_itself")
assert _constructed(main_cpp, "calls_itself") == "script::ParallelScript<int>()"
body = _function_body(main_cpp, _run_function(main_cpp, "calls_itself"))
assert "return n > 0;" in body
assert "::calls_itself->execute(std::move(esphome__arg_0));" in body
def test_delay_keeps_separate_actions(generate_main) -> None:
"""A delay keeps today's class, Automation and separate actions."""
"""A delay keeps the Automation and its separate actions."""
main_cpp = generate_main(CONFIG)
assert _constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
assert "static script::SingleScript<> *const with_delay" in main_cpp
before, delay, after = _actions_of(main_cpp, _automation(main_cpp, "with_delay"))
assert _constructed(main_cpp, delay) == "DelayAction<>()"
assert '"DELAY before"' in _constructed(main_cpp, before)
assert '"DELAY after"' in _constructed(main_cpp, after)
assert "esphome__with_delay__fn" not in main_cpp
def test_manual_automation_id(generate_main) -> None:
"""An automation_id set by hand names the Automation, so a lambda can reach it."""
main_cpp = generate_main(CONFIG)
assert _constructed(main_cpp, "manual_automation") == "script::SingleScript<>()"
assert _automation(main_cpp, "manual_automation") == "manual_automation_id"
(action,) = _actions_of(main_cpp, "manual_automation_id")
assert '"MANUAL"' in _constructed(main_cpp, action)
def test_script_wait(generate_main) -> None:
"""``script.wait`` on a script that runs a function uses the class shared by its mode."""
"""``script.wait`` is a component templated on the class of the script it waits for."""
main_cpp = generate_main(CONFIG)
(action,) = _actions_of(main_cpp, _automation(main_cpp, "waits"))
class_ = _wait_class(_mode_class("SingleMode"))
assert _constructed(main_cpp, action) == f"{class_}(inline_single)"
def test_script_wait_built_first(generate_main) -> None:
"""A ``script.wait`` built before the script it waits for gets the shared class too."""
main_cpp = generate_main(CONFIG)
late, delay = _actions_of(main_cpp, _automation(main_cpp, "waits_early"))
class_ = _wait_class(_mode_class("SingleMode"))
assert _constructed(main_cpp, late) == f"{class_}(late_target)"
assert f"static {class_} *const {late}" in main_cpp
assert _constructed(main_cpp, delay) == (
f"{_wait_class('script::SingleScript<>')}(with_delay)"
)
def test_script_wait_keeps_waiting_script_parameters(generate_main) -> None:
"""Only the waited-for script's class changes, not a parameter with the same name."""
main_cpp = generate_main(CONFIG)
automation = _automation(main_cpp, "waits_with_param")
wait, _ = _actions_of(main_cpp, automation)
assert _constructed(main_cpp, wait) == (
f"script::ScriptWaitAction<{_mode_class('SingleMode')}, "
"script::SingleScript<>*>(late_target)"
assert (
_constructed(main_cpp, action)
== "script::ScriptWaitAction<script::SingleScript<>>(inline_single)"
)
@@ -11,20 +11,6 @@ globals:
type: int
script:
# Built before the scripts it waits for
- id: waits_early
then:
- script.wait: late_target
- script.wait: with_delay
# Has a parameter of the class an inline script had before it was known
- id: waits_with_param
parameters:
other: script::SingleScript<>*
then:
- script.wait: late_target
- lambda: other->execute();
# Inline actions only
- id: inline_single
then:
@@ -117,7 +103,3 @@ script:
- id: waits
then:
- script.wait: inline_single
- id: late_target
then:
- logger.log: "LATE target"
+342 -11
View File
@@ -1,34 +1,74 @@
#include <gtest/gtest.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <string>
#include "esphome/core/helpers.h"
#include "esphome/core/parse_float.h"
#ifdef USE_HOST
namespace esphome::core::testing {
static uint32_t bits(float value) {
uint32_t out;
memcpy(&out, &value, sizeof(out));
return out;
static uint32_t bits(float value) { return bit_cast<uint32_t>(value); }
// picolibc rounds a decimal to 9 significant digits and converts those to the nearest float, so up to 9
// significant digits its result is the correctly rounded float the host's strtof (glibc, macOS) gives. Longer
// input and the syntax where libcs differ are checked against values taken from picolibc 1.8.10's own strtof.
static float reference_strtof(const char *str, char **end) {
const float value = ::strtof(str, end);
if (std::isnan(value))
return std::signbit(value) ? -NAN : NAN;
return value;
}
// strtof is the reference: every accepted input must give the same bits.
// Every accepted input must give the reference's bits.
static void expect_matches_strtof(const char *str) {
char *end = nullptr;
const float expected = ::strtof(str, &end);
const float expected = reference_strtof(str, nullptr);
const auto parsed = parse_number<float>(str);
if (!parsed.has_value()) {
ADD_FAILURE() << str << " was rejected";
return;
}
const float actual = parsed.value();
EXPECT_EQ(bits(actual), bits(expected)) << str << " parsed " << actual << " expected " << expected;
EXPECT_EQ(bits(parsed.value()), bits(expected)) << str;
}
// The same bits and end as the host's strtof; for decimals of up to 9 significant digits
static void expect_str_to_float_matches(const char *str) {
char *expected_end = nullptr;
const float expected = reference_strtof(str, &expected_end);
char *end = nullptr;
const float actual = str_to_float(str, &end);
EXPECT_EQ(bits(actual), bits(expected)) << '"' << str << '"';
EXPECT_EQ(end - str, expected_end - str) << '"' << str << '"';
}
struct PicolibcCase {
const char *str;
uint32_t bits;
int32_t end;
};
// Bits and end from picolibc 1.8.10's strtof
static void expect_picolibc(const PicolibcCase &c) {
char *end = nullptr;
const float value = str_to_float(c.str, &end);
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
}
static void expect_picolibc(const std::string &str, uint32_t expected) {
char *end = nullptr;
EXPECT_EQ(bits(str_to_float(str.c_str(), &end)), expected) << str.substr(0, 40);
EXPECT_EQ(static_cast<size_t>(end - str.c_str()), str.size()) << str.substr(0, 40);
}
static float float_from(uint32_t value) { return bit_cast<float>(value); }
TEST(ParseFloat, PlainDecimalsMatchStrtof) {
for (const char *str :
{"0", "-0", "+0", "1", "-1", "100.50", "99.75", "0.1", "0.3",
@@ -46,6 +86,14 @@ TEST(ParseFloat, SweepOfTwoDecimalValuesMatchesStrtof) {
}
}
// Eight-digit mantissas below 2^24 stay on the one-division path; the next digit past the limit falls back
TEST(ParseFloat, EightDigitPlainDecimalsMatchStrtof) {
for (const char *str : {"12345678", "-12345678", "1234.5678", "0.12345678", "16777209", "1677720.9", "0.16777209",
"16777210", "16777215", "1677721.5", "99999999", "9999999.9", "16777209."}) {
expect_matches_strtof(str);
}
}
// A fraction longer than the fast path accepts must fall back, however long it is.
TEST(ParseFloat, LongFractionsMatchStrtof) {
std::string str = "0.";
@@ -68,10 +116,293 @@ TEST(ParseFloat, LibcPathInputsMatchStrtof) {
// Positive overflow and a positive literal infinity both come back as HUGE_VALF from strtof, and the parser
// rejects both; the negative forms pass through, as they did before the fast path.
TEST(ParseFloat, RejectsInvalidInput) {
for (const char *str :
{"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf", "infinity"}) {
for (const char *str : {"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf",
"infinity", "infi", "nan(12 ab)", "nan(12"}) {
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
}
}
// Above the midpoint 1 + 2^-24 by less than half a double: newlib's strtof rounds to the midpoint and then to
// 1.0f; picolibc's 9 digits, 1.00000006, give the next float.
TEST(ParseFloat, FullPathRoundsLikePicolibc) {
EXPECT_EQ(bits(parse_number<float>("1.0000000596046448").value()), 0x3F800001u);
EXPECT_EQ(bits(parse_number<float>("-1.0000000596046448").value()), 0xBF800001u);
}
// picolibc takes "nan(" with letters, digits and '_' up to a ")", and nothing at all otherwise
TEST(ParseFloat, NanForms) {
EXPECT_TRUE(std::isnan(parse_number<float>("nan(12)").value()));
EXPECT_TRUE(std::isnan(parse_number<float>("nan(xyz_1)").value()));
EXPECT_FALSE(parse_number<float>("nan(x y)").has_value());
}
// The callers' overflow check: a positive overflow is HUGE_VALF and rejected, a negative one is -inf and accepted
TEST(ParseFloat, OverflowIsHugeValf) {
EXPECT_FALSE(parse_number<float>("1e39").has_value());
EXPECT_FALSE(parse_number<float>("3.40282357e38").has_value());
EXPECT_FALSE(parse_number<float>("1e99999").has_value());
EXPECT_EQ(bits(parse_number<float>("-1e39").value()), 0xFF800000u);
EXPECT_EQ(bits(parse_number<float>("3.4028235e38").value()), 0x7F7FFFFFu);
}
// Up to 9 significant digits, with leading and trailing zeros, a point anywhere and an exponent
TEST(StrToFloat, RandomDecimalsMatchStrtof) {
std::mt19937_64 rng(1); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
for (int i = 0; i < 50000; i++) {
std::string str;
if (rng() % 3 == 0)
str += (rng() & 1) ? "-" : "+";
const int zeros = static_cast<int>(rng() % 4);
const int significant = 1 + static_cast<int>(rng() % 9);
const int trailing = static_cast<int>(rng() % 4);
std::string digits(zeros, '0');
digits += static_cast<char>('1' + rng() % 9);
for (int j = 1; j < significant; j++)
digits += static_cast<char>('0' + rng() % 10);
digits.append(trailing, '0');
const int point = static_cast<int>(rng() % (digits.size() + 2)) - 1;
for (int j = 0; j < static_cast<int>(digits.size()); j++) {
if (j == point)
str += '.';
str += digits[j];
}
if (rng() & 1) {
str += (rng() & 1) ? 'e' : 'E';
char exp[8];
snprintf(exp, sizeof(exp), "%d", static_cast<int>(rng() % 110) - 60);
str += exp;
}
expect_str_to_float_matches(str.c_str());
}
}
// Every 16411th float, printed with 6 and 9 digits
TEST(StrToFloat, FloatsMatchStrtof) {
char buf[64];
for (uint32_t u = 0; u < 0x7F800000; u += 16411) {
for (int precision : {6, 9}) {
snprintf(buf, sizeof(buf), "%.*e", precision - 1, static_cast<double>(float_from(u)));
expect_str_to_float_matches(buf);
}
}
}
TEST(StrToFloat, SyntaxMatchesStrtof) {
// Tokens whose combinations the libcs read alike: no "x", "nan(" or "infi"
static const char *const TOKENS[] = {"0", "1", "9", ".", "e", "E", "+", "-", "a", "N", " ", "\t", "y", "00", "#", ""};
for (const char *a : TOKENS) {
for (const char *b : TOKENS) {
for (const char *c : TOKENS) {
for (const char *d : TOKENS) {
const std::string str = std::string(a) + b + c + d;
expect_str_to_float_matches(str.c_str());
}
}
}
}
}
// Where newlib and glibc read the text differently from picolibc
TEST(StrToFloat, SyntaxMatchesPicolibc) {
static const PicolibcCase CASES[] = {
// "x" after any zero digits starts hex, even after a point or a second "0x"
{"000x", 0x00000000, 4},
{"0x", 0x00000000, 2},
{"-0x", 0x80000000, 3},
{"00x1", 0x3F800000, 4},
{"0x0x1", 0x3F800000, 5},
{"0.0x1", 0x3B800000, 5},
{".x1", 0x00000000, 0},
{"0x.p1", 0x00000000, 5},
{"0x.8", 0x3F000000, 4},
{"0X1.8", 0x3FC00000, 5},
{"0x1P-1", 0x3F000000, 6},
{"0x1p+", 0x3F800000, 3},
{"1x5", 0x3F800000, 1},
// "inf" stops before anything but "i"; past "infi" only "infinity" is a number
{"inf", 0x7F800000, 3},
{"-inf", 0xFF800000, 4},
{"INFINITY", 0x7F800000, 8},
{"+infi0", 0x00000000, 0},
{"infi", 0x00000000, 0},
{"infinit", 0x00000000, 0},
// "nan(" takes letters, digits and '_' up to ")", and is no number without the ")"
{"NaN", 0x7FC00000, 3},
{"nan()", 0x7FC00000, 5},
{"nan(xyz)", 0x7FC00000, 8},
{"nan(_a1)", 0x7FC00000, 8},
{"-nan(1)", 0xFFC00000, 7},
{"nan( )", 0x00000000, 0},
{"nan(12 ab)", 0x00000000, 0},
{"nan(12", 0x00000000, 0},
// An exponent needs a digit
{"1e", 0x3F800000, 1},
{"1e+", 0x3F800000, 1},
{"1e-x", 0x3F800000, 1},
{"\t 12", 0x41400000, 4},
{"- 1", 0x00000000, 0},
{"1.2.3", 0x3F99999A, 3},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// The inputs where newlib's strtof (decimal to double to float) and picolibc's were found to differ; every platform
// now gives picolibc's result, which is also the correctly rounded float for these
TEST(StrToFloat, RoundsLikePicolibcWhereNewlibDoubleRounds) {
expect_picolibc({"1.0000000596046448", 0x3F800001, 18});
expect_picolibc({"0x1.100b0d0000000c", 0x3F880587, 18});
expect_picolibc("1.000000059604644775390625" + std::string(800, '0') + "1", 0x3F800001);
}
// picolibc rounds a decimal to 9 significant digits, ties to even, before converting it: past 9 digits its float can
// be one away from the nearest, and an exact tie between two floats can round up. Values from picolibc.
TEST(StrToFloat, NineDigitRoundingLikePicolibc) {
static const PicolibcCase CASES[] = {
{"1.0856183295439e10", 0x5021C50E, 18}, // nearest 0x5021C50D
{"3.97496754550e-4", 0x39D0671D, 16}, // nearest 0x39D0671C
{"9.76178673657108930e23", 0x674EB6C1, 22}, // nearest 0x674EB6C0
{"156.6562236e-26", 0x17F269ED, 15}, // nearest 0x17F269EC
{"1.000000059604644775390625", 0x3F800001, 26}, // the tie 1 + 2^-24: nearest-even 0x3F800000
{"1.000000059", 0x3F800001, 11}, // 1.00000006 after rounding
{"7.0064923216240854e-46", 0x00000000, 22}, // 7.00649232e-46, below half the smallest float
{"21.299999999999997", 0x41AA6666, 18},
{"0.30000000000000004", 0x3E99999A, 19},
{"0.1000000000000000055511151231257827", 0x3DCCCCCD, 36},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// Nine digits converted exactly: ties to even between floats, where the 64-bit product cannot decide alone
TEST(StrToFloat, NineDigitTiesGoToEven) {
static const PicolibcCase CASES[] = {
{"16777217", 0x4B800000, 8}, {"16777219", 0x4B800002, 8}, {"16777217.0", 0x4B800000, 10},
{"8388608.5", 0x4B000000, 9}, {"8388609.5", 0x4B000002, 9}, {"3e10", 0x50DF8476, 4},
{"5e10", 0x513A43B7, 4}, {"1.00000006", 0x3F800001, 10},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// RP2350 flushes float denormals in its double to float conversion; #22677 bench. The parser builds the bits
// with integer operations, so every denormal parses to its own value.
TEST(StrToFloat, DenormalsParseWithoutFlushing) {
char buf[64];
const auto check = [&buf](uint32_t k) {
snprintf(buf, sizeof(buf), "%.8e", std::ldexp(static_cast<double>(k), -149));
EXPECT_EQ(bits(str_to_float(buf, nullptr)), k) << buf;
};
for (uint32_t k = 0; k <= 0x800000; k += 4099)
check(k);
// The largest denormal and FLT_MIN, where rounding carries into the exponent
for (uint32_t k : {1u, 2u, 0x7FFFFEu, 0x7FFFFFu, 0x800000u})
check(k);
}
// Pico W and Pico 2 W bench, #22677: the RP2040 and RP2350 libcs flush float denormals to 0 in their double to
// float conversion, and the RP2040 one turns a NaN into +infinity
TEST(StrToFloat, Rp2040LibcFlushesAndNanToInfinity) {
static const PicolibcCase CASES[] = {
{"1e-40", 0x000116C2, 5}, {"1.4e-45", 0x00000001, 7}, {"1e-38", 0x006CE3EE, 5},
{"1.1754942e-38", 0x007FFFFF, 13}, {"0x1p-149", 0x00000001, 8}, {"1.17549435e-38", 0x00800000, 14},
{"nan(12)", 0x7FC00000, 7}, {"nan", 0x7FC00000, 3},
};
for (const PicolibcCase &c : CASES)
expect_picolibc(c);
}
// RP2350 bench: libc returned 0 and the first parser stalled stepping up from a flushed guess; #22677 bench
TEST(StrToFloat, DenormalsAndFltMinMatchStrtof) {
for (const char *str : {"1.4e-45", "-1.4e-45", "1e-40", "1e-38", "1.17549435e-38", "1.1754942e-38", "1.17549421e-38",
"1.1754945e-38", "7.0064923e-46", "7.0064924e-46"}) {
expect_str_to_float_matches(str);
}
}
// Home Assistant's non-numeric states; CodSpeed HomeassistantSensor_StateResponse_Unavailable on #22677
TEST(StrToFloat, NonNumericStatesAreRejected) {
for (const char *str : {"unavailable", "unknown", "on", "off", "", "-", ".", "none", "inactive"}) {
expect_str_to_float_matches(str);
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
}
}
// The one float operation path takes up to 2^24 - 1 and 10^10; #22677 review
TEST(StrToFloat, PlainPathBoundaries) {
for (const char *top : {"16777215", "16777216", "16777217"}) {
for (const char *exp : {"e10", "e-10", "e11", "e-11", "e9", "e-9", ".5", ".25"}) {
const std::string str = std::string(top) + exp;
expect_str_to_float_matches(str.c_str());
}
}
}
// The midpoint between FLT_MAX and 2^128 rounds to infinity; review
TEST(StrToFloat, OverflowMidpoint) {
expect_picolibc({"340282356779733661637539395458142568448", 0x7F800000, 39});
expect_picolibc({"3.40282356e38", 0x7F7FFFFF, 13});
expect_picolibc({"3.40282357e38", 0x7F800000, 13});
EXPECT_EQ(bits(str_to_float("0x1.ffffffp127", nullptr)), 0x7F800000u);
}
// picolibc stops adding exponent digits at 224, so the digits of the mantissa can move it no further
TEST(StrToFloat, HugeExponents) {
for (const char *str : {"1e-99999", "1e99999", "-1e99999", "0e99999", "1e9999999999999999999",
"1e-9999999999999999999", "1e-999999999999", "0.000e99999", "1e224", "1e2240", "1e-2240"}) {
expect_str_to_float_matches(str);
}
// 10^-3000 * 10^3000 is 1, but the exponent counts only as 300
expect_picolibc("0." + std::string(2999, '0') + "1e3000", 0x00000000);
expect_picolibc("1" + std::string(300, '0') + "e-300", 0x3F800000);
}
TEST(StrToFloat, LongMantissas) {
expect_picolibc("1" + std::string(799, '0') + "e-799", 0x3F800000);
expect_picolibc("1." + std::string(3000, '0') + "1e-3001", 0x00000000);
expect_picolibc("0." + std::string(400, '0') + "34028235" + std::string(500, '7') + "e440", 0x7F800000);
expect_picolibc(std::string(300, '9') + "e-261", 0x7F800000);
expect_picolibc("3" + std::string(799, '7') + "e-761", 0x7F800000);
}
// picolibc takes ':' to '?' as hex digits 10 to 15 ("0x1:" is 26); these stop at them, as newlib does. #22677 review
TEST(StrToFloat, HexStopsAtPunctuationAfterNine) {
struct Case {
const char *str;
uint32_t bits;
int32_t end;
};
const Case cases[] = {
{"0x1:", 0x3F800000, 3}, {"0x1;", 0x3F800000, 3}, {"0x1<", 0x3F800000, 3}, {"0x1=", 0x3F800000, 3},
{"0x1>", 0x3F800000, 3}, {"0x1?", 0x3F800000, 3}, {"0x1.:", 0x3F800000, 4}, {"0x:", 0x00000000, 2},
{"0x9?", 0x41100000, 3}, {"0xA:", 0x41200000, 3},
};
for (const auto &c : cases) {
char *end = nullptr;
const float value = str_to_float(c.str, &end);
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
EXPECT_FALSE(parse_number<float>(c.str).has_value()) << c.str;
}
}
TEST(StrToFloat, HexDenormalsAndOverflow) {
struct Case {
const char *str;
uint32_t bits;
};
static const Case CASES[] = {
{"0x1p-149", 0x00000001}, {"0x1p-150", 0x00000000},
{"0x1.8p-149", 0x00000002}, {"0x1.fffffcp-127", 0x007FFFFF},
{"0x1.fffffep-127", 0x00800000}, {"-0x1p-149", 0x80000001},
{"0x1p128", 0x7F800000}, {"0x1.fffffep127", 0x7F7FFFFF},
{"0x1.fffffefp127", 0x7F7FFFFF}, {"0x1.0000000000001p-150", 0x00000001},
{"0x1.000001p0", 0x3F800000}, {"0x1.000003p0", 0x3F800002},
{"0x1p-99999999999", 0x00000000}, {"0x1p99999999999", 0x7F800000},
};
for (const Case &c : CASES)
EXPECT_EQ(bits(str_to_float(c.str, nullptr)), c.bits) << c.str;
}
} // namespace esphome::core::testing
#endif // USE_HOST
@@ -1,121 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/script/script.h"
namespace esphome::script::testing {
// Each body is the function of its script, as code generation emits it; the bodies reach
// their script and record what happened through globals.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
static void single_body(const int &running);
using TestSingle = FunctionScript<SingleMode<FunctionBody<>>, single_body>;
static TestSingle *single_script;
static int single_runs;
static bool single_running_inside;
static void single_body(const int &running) {
single_runs++;
single_running_inside = single_script->is_running();
// Only once, so a script that is not running here fails the test instead of recursing
if (single_runs == 1)
single_script->execute();
}
TEST(FunctionScriptTest, SingleIsRunningOnlyInsideItsBody) {
TestSingle script;
single_script = &script;
single_runs = 0;
script.execute();
EXPECT_EQ(single_runs, 1);
EXPECT_TRUE(single_running_inside);
EXPECT_FALSE(script.is_running());
}
static void stop_body(const int &running);
using TestStop = FunctionScript<SingleMode<FunctionBody<>>, stop_body>;
static TestStop *stop_script;
static int stop_running_after;
static void stop_body(const int &running) {
stop_script->stop();
stop_running_after = running;
}
TEST(FunctionScriptTest, StopInsideClearsTheRunCount) {
TestStop script;
stop_script = &script;
stop_running_after = -1;
script.execute();
EXPECT_EQ(stop_running_after, 0);
EXPECT_FALSE(script.is_running());
}
static void restart_body(const int &running);
using TestRestart = FunctionScript<RestartMode<FunctionBody<>>, restart_body>;
static TestRestart *restart_script;
static int restart_starts;
static int restart_ends;
static void restart_body(const int &running) {
if (restart_starts++ == 0)
restart_script->execute();
if (running)
restart_ends++;
}
TEST(FunctionScriptTest, RestartFromInsideStopsTheOuterRun) {
TestRestart script;
restart_script = &script;
restart_starts = restart_ends = 0;
script.execute();
EXPECT_EQ(restart_starts, 2);
EXPECT_EQ(restart_ends, 1);
EXPECT_FALSE(script.is_running());
}
static void parallel_body(const int &running, const int &n);
using TestParallel = FunctionScript<ParallelMode<FunctionBody<int>, int>, parallel_body, int>;
static TestParallel *parallel_script;
static int parallel_runs;
static void parallel_body(const int &running, const int &n) {
parallel_runs++;
if (n < 5)
parallel_script->execute(n + 1);
}
TEST(FunctionScriptTest, ParallelCountsNestedRuns) {
TestParallel script;
script.set_max_runs(2);
parallel_script = &script;
parallel_runs = 0;
script.execute(1);
EXPECT_EQ(parallel_runs, 2);
EXPECT_FALSE(script.is_running());
}
static void queued_body(const int &running, const int &n);
using TestQueued = FunctionPointerScript<QueueingMode<FunctionBody<int>, int>, int>;
// Through the base class, as external components keep scripts
static Script<int> *queued_script;
static int queued_last;
static int queued_runs;
static void queued_body(const int &running, const int &n) {
queued_runs++;
queued_last = n;
if (n == 1) {
queued_script->execute(2);
queued_script->execute(3);
}
}
TEST(FunctionScriptTest, QueuedRunsTheQueuedRunAfterTheBody) {
TestQueued script(queued_body);
script.set_max_runs(2);
queued_script = &script;
queued_runs = 0;
script.execute(1);
EXPECT_EQ(queued_runs, 2);
EXPECT_EQ(queued_last, 2);
EXPECT_FALSE(script.is_running());
}
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::script::testing
+15 -35
View File
@@ -9,14 +9,14 @@ from esphome import automation
from esphome.components.const import CONF_ON_STOP
from esphome.components.script import (
CONF_SCRIPT_STOP,
CONFIG_SCHEMA,
SCRIPT_MODES,
SingleScript,
default_script_stop_ids,
to_code,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS, CONF_THEN
from esphome.core import ID
from esphome.cpp_generator import MockObj
def test_bare_script_stop_defaults_to_enclosing_script() -> None:
@@ -103,78 +103,58 @@ async def test_on_stop_builds_stop_trigger_automation() -> None:
stop_conf = {CONF_THEN: []}
config = [
{
CONF_ID: ID("my_script", is_declaration=True, type=SCRIPT_MODES["single"]),
CONF_ID: ID("my_script", is_declaration=True, type=SingleScript),
CONF_MODE: "single",
CONF_PARAMETERS: {},
CONF_THEN: [],
CONF_ON_STOP: stop_conf,
},
{
CONF_ID: ID(
"other_script", is_declaration=True, type=SCRIPT_MODES["single"]
),
CONF_ID: ID("other_script", is_declaration=True, type=SingleScript),
CONF_MODE: "single",
CONF_PARAMETERS: {},
CONF_THEN: [],
},
]
with (
patch("esphome.codegen.new_Pvariable") as new_pvariable,
patch("esphome.codegen.add"),
patch("esphome.codegen.add_define") as add_define,
patch(
"esphome.automation.build_automation", new_callable=AsyncMock
) as build_automation,
patch(
"esphome.automation.build_automation_or_run_function",
new_callable=AsyncMock,
return_value=None,
) as build_or_run,
):
new_pvariable.side_effect = lambda id_, *args: MockObj(id_.id, "->")
await to_code(config)
add_define.assert_called_once_with("USE_SCRIPT_ON_STOP")
assert [(str(c.args[0]), *c.args[1:3]) for c in build_or_run.await_args_list] == [
("my_script", [], config[0]),
("other_script", [], config[1]),
]
assert [
(str(c.args[0]), *c.args[1:]) for c in build_automation.await_args_list
] == [("my_script->get_stop_trigger()", [], stop_conf)]
] == [
("my_script", [], config[0]),
("my_script->get_stop_trigger()", [], stop_conf),
("other_script", [], config[1]),
]
@pytest.mark.asyncio
async def test_no_on_stop_define_without_on_stop() -> None:
config = [
{
CONF_ID: ID("my_script", is_declaration=True, type=SCRIPT_MODES["single"]),
CONF_ID: ID("my_script", is_declaration=True, type=SingleScript),
CONF_MODE: "single",
CONF_PARAMETERS: {},
CONF_THEN: [],
}
]
with (
patch("esphome.codegen.new_Pvariable") as new_pvariable,
patch("esphome.codegen.add"),
patch("esphome.codegen.add_define") as add_define,
patch(
"esphome.automation.build_automation_or_run_function",
new_callable=AsyncMock,
return_value=None,
),
patch("esphome.automation.build_automation", new_callable=AsyncMock),
):
new_pvariable.side_effect = lambda id_, *args: MockObj(id_.id, "->")
await to_code(config)
add_define.assert_not_called()
@pytest.mark.parametrize("name", ["esphome__running", "esphome__arg_0"])
def test_reserved_parameter_names_rejected(name: str) -> None:
"""The reason reaches the user, at the parameter's path."""
config = [{CONF_ID: "my_script", CONF_PARAMETERS: {name: "int"}, CONF_THEN: []}]
with pytest.raises(cv.MultipleInvalid) as err:
CONFIG_SCHEMA(config)
(error,) = err.value.errors
assert (
error.msg == "Names starting with 'esphome__' are reserved for generated code"
)
assert error.path == [0, CONF_PARAMETERS, name]
+1 -5
View File
@@ -1595,11 +1595,7 @@ def apply_functions(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str, str
emitted: list[tuple[str, str, str]] = []
def fake_static_function(
name: str,
return_type: Any,
parameters: list[tuple[Any, str]],
body: list[str],
add_global_: Any = None,
name: str, return_type: Any, parameters: list[tuple[Any, str]], body: list[str]
) -> RawExpression:
assert str(return_type) == "void"
params = ", ".join(f"{type_} {arg}" for type_, arg in parameters)
-26
View File
@@ -1173,32 +1173,6 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
]
def test_static_function_declaration_sink() -> None:
declarations: list[cg.Statement] = []
cg.static_function(
"esphome__act__fn", ct.void, [], ["return;"], declarations.append
)
assert [str(st) for st in declarations] == ["static void esphome__act__fn();"]
assert CORE.global_statements == []
def test_late_pvariable_is_created_where_declared() -> None:
"""A late variable is registered at once and emitted in its place, like new_Pvariable."""
id_ = ID("late", is_declaration=True, type=ct.esphome_ns.class_("Late"))
late = cg.LatePvariable(id_)
assert CORE.variables[id_] is late.var
CORE.add_global(cg.RawStatement("int after;"))
cg.add(cg.RawStatement("after();"))
late.global_slot.add(cg.RawStatement("int before_late;"))
late.define(ct.esphome_ns.class_("Final"), cg.TemplateArguments(ct.int32), 5)
assert str(id_.type) == "Final<int32_t>"
globals_ = CORE.cpp_global_section
assert globals_.index("Final<int32_t> *const late") < globals_.index("int after;")
main = CORE.cpp_main_section
assert main.index("new(late) Final<int32_t>(5);") < main.index("after();")
assert cg.is_static_pointer(late.var)
def test_static_function_accepts_statements() -> None:
call = cg.MockObj("foo")(cg.MockObj("x") >> 2)
cg.static_function(