Replace evalexpr (#3860)

This commit is contained in:
Dániel Buga 2025-07-25 08:37:49 +02:00 committed by GitHub
parent 07214ef80d
commit 87febcbb4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 105 additions and 328 deletions

View File

@ -22,7 +22,7 @@ document-features = "0.2.11"
# used by the `build` and `tui` feature
serde = { version = "1.0.197", default-features = false, features = ["derive"], optional = true }
serde_yaml = { version = "0.9", optional = true }
evalexpr = { version = "12.0.2", optional = true }
somni-expr = { version = "0.1.0", optional = true }
esp-metadata = { version = "0.8.0", path = "../esp-metadata", features = ["clap"], optional = true }
esp-metadata-generated = { version = "0.1.0", path = "../esp-metadata-generated", features = ["build-script"], optional = true }
@ -42,7 +42,7 @@ pretty_assertions = "1.4.1"
[features]
## Enable the generation and parsing of a config
build = ["dep:serde", "dep:serde_yaml", "dep:evalexpr", "dep:esp-metadata-generated"]
build = ["dep:serde", "dep:serde_yaml", "dep:somni-expr", "dep:esp-metadata-generated"]
## The TUI
tui = [

View File

@ -92,16 +92,16 @@ checks:
- 'ESP_BOOTLOADER_ESP_IDF_CONFIG_PARTITION_TABLE_OFFSET >= 32768'
```
`if` and `active` are [evalexpr](https://crates.io/crates/evalexpr) expressions returning a boolean.
`if` and `active` are [somni-expr](https://crates.io/crates/somni-expr) expressions evaluating to a boolean.
The expression supports these custom functions:
|Function|Description|
|---|---|
|feature(String)|`true` if the given chip feature is present|
|cargo_feature(String)|`true` if the given Cargo feature is active|
|ignore_feature_gates()|Usually `false` but tooling will set this to `true` to hint that the expression is evaluated by e.g. a TUI|
|ignore_feature_gates|Usually `false` but tooling will set this to `true` to hint that the expression is evaluated by e.g. a TUI|
`ignore_feature_gates()` is useful to enable otherwise disabled functionality - e.g. to offer all possible options regardless of any active / non-active features.
`ignore_feature_gates` is useful to enable otherwise disabled functionality - e.g. to offer all possible options regardless of any active / non-active features.
The `chip` variable is populated with the name of the targeted chip (if the crate is using chip specific features).

View File

@ -1,163 +0,0 @@
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct I128NumericTypes;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct I128(pub i128);
impl FromStr for I128 {
type Err = core::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(I128(s.parse()?))
}
}
impl core::fmt::Display for I128 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
use core::str::FromStr;
use evalexpr::{EvalexprError, EvalexprResult};
impl<NumericTypes: evalexpr::EvalexprNumericTypes<Int = Self>> evalexpr::EvalexprInt<NumericTypes>
for I128
{
const MIN: Self = I128(i128::MIN);
const MAX: Self = I128(i128::MAX);
fn from_usize(int: usize) -> EvalexprResult<Self, NumericTypes> {
Ok(I128(int.try_into().map_err(|_| {
EvalexprError::IntFromUsize { usize_int: int }
})?))
}
fn into_usize(&self) -> EvalexprResult<usize, NumericTypes> {
if self.0 >= 0 {
(self.0 as u64)
.try_into()
.map_err(|_| EvalexprError::IntIntoUsize { int: *self })
} else {
Err(EvalexprError::IntIntoUsize { int: *self })
}
}
fn from_hex_str(literal: &str) -> Result<Self, ()> {
Ok(I128(i128::from_str_radix(literal, 16).map_err(|_| ())?))
}
fn checked_add(&self, rhs: &Self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_add(rhs.0);
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::AdditionError {
augend: evalexpr::Value::<NumericTypes>::from_int(*self),
addend: evalexpr::Value::<NumericTypes>::from_int(*rhs),
})
}
}
fn checked_sub(&self, rhs: &Self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_sub(rhs.0);
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::SubtractionError {
minuend: evalexpr::Value::<NumericTypes>::from_int(*self),
subtrahend: evalexpr::Value::<NumericTypes>::from_int(*rhs),
})
}
}
fn checked_neg(&self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_neg();
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::NegationError {
argument: evalexpr::Value::<NumericTypes>::from_int(*self),
})
}
}
fn checked_mul(&self, rhs: &Self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_mul(rhs.0);
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::MultiplicationError {
multiplicand: evalexpr::Value::<NumericTypes>::from_int(*self),
multiplier: evalexpr::Value::<NumericTypes>::from_int(*rhs),
})
}
}
fn checked_div(&self, rhs: &Self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_div(rhs.0);
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::DivisionError {
dividend: evalexpr::Value::<NumericTypes>::from_int(*self),
divisor: evalexpr::Value::<NumericTypes>::from_int(*rhs),
})
}
}
fn checked_rem(&self, rhs: &Self) -> EvalexprResult<Self, NumericTypes> {
let result = (self.0).checked_rem(rhs.0);
if let Some(result) = result {
Ok(I128(result))
} else {
Err(EvalexprError::ModulationError {
dividend: evalexpr::Value::<NumericTypes>::from_int(*self),
divisor: evalexpr::Value::<NumericTypes>::from_int(*rhs),
})
}
}
fn abs(&self) -> EvalexprResult<Self, NumericTypes> {
Ok(I128(self.0.abs()))
}
fn bitand(&self, rhs: &Self) -> Self {
I128(std::ops::BitAnd::bitand(self.0, rhs.0))
}
fn bitor(&self, rhs: &Self) -> Self {
I128(std::ops::BitOr::bitor(self.0, rhs.0))
}
fn bitxor(&self, rhs: &Self) -> Self {
I128(std::ops::BitXor::bitxor(self.0, rhs.0))
}
fn bitnot(&self) -> Self {
I128(std::ops::Not::not(self.0))
}
fn bit_shift_left(&self, rhs: &Self) -> Self {
I128(std::ops::Shl::shl(self.0, rhs.0))
}
fn bit_shift_right(&self, rhs: &Self) -> Self {
I128(std::ops::Shr::shr(self.0, rhs.0))
}
}
impl evalexpr::EvalexprNumericTypes for I128NumericTypes {
type Int = I128;
type Float = f64;
fn int_as_float(int: &Self::Int) -> Self::Float {
int.0 as Self::Float
}
fn float_as_int(float: &Self::Float) -> Self::Int {
I128(float.trunc() as i128)
}
}

View File

@ -1,22 +1,17 @@
use core::fmt::Display;
use std::{collections::HashMap, env, fmt, fs, io::Write, path::PathBuf};
use evalexpr::{ContextWithMutableFunctions, ContextWithMutableVariables};
use serde::{Deserialize, Serialize};
use somni_expr::TypeSet128;
use crate::generate::{
evalexpr_extensions::{I128, I128NumericTypes},
validator::Validator,
value::Value,
};
use crate::generate::{validator::Validator, value::Value};
pub(crate) mod evalexpr_extensions;
mod markdown;
pub(crate) mod validator;
pub(crate) mod value;
/// Configuration errors.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub enum Error {
/// Parse errors.
Parse(String),
@ -42,6 +37,12 @@ impl Error {
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@ -51,6 +52,20 @@ impl fmt::Display for Error {
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
None
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn core::error::Error> {
self.source()
}
}
/// The root node of a configuration.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
@ -156,23 +171,18 @@ pub fn generate_config_from_yaml_definition(
/// Check the given actual values by applying checking the given checks
pub fn do_checks(checks: Option<&Vec<String>>, cfg: &HashMap<String, Value>) -> Result<(), Error> {
if let Some(checks) = checks {
let mut eval_ctx = evalexpr::HashMapContext::<I128NumericTypes>::new();
let mut eval_ctx = somni_expr::Context::<TypeSet128>::new_with_types();
for (k, v) in cfg.iter() {
eval_ctx
.set_value(
k.clone(),
match v {
Value::Bool(v) => evalexpr::Value::Boolean(*v),
Value::Integer(v) => evalexpr::Value::Int(I128(*v)),
Value::String(v) => evalexpr::Value::String(v.clone()),
},
)
.map_err(|err| Error::Parse(format!("Error setting value for {k} ({err})")))?;
match v {
Value::Bool(v) => eval_ctx.add_variable(k, *v),
Value::Integer(v) => eval_ctx.add_variable(k, *v),
Value::String(v) => eval_ctx.add_variable::<&str>(k, v),
}
}
for check in checks {
if !evalexpr::eval_with_context(check, &eval_ctx)
.and_then(|v| v.as_boolean())
.map_err(|err| Error::Validation(format!("Validation error: '{check}' ({err})")))?
if !eval_ctx
.evaluate::<bool>(check)
.map_err(|err| Error::Parse(format!("Validation error: {err:?}")))?
{
return Err(Error::Validation(format!("Validation error: '{check}'")));
}
@ -190,96 +200,27 @@ pub fn evaluate_yaml_config(
) -> Result<(Config, Vec<ConfigOption>), Error> {
let config: Config = serde_yaml::from_str(yaml).map_err(|err| Error::Parse(err.to_string()))?;
let mut options = Vec::new();
let mut eval_ctx = evalexpr::HashMapContext::<evalexpr::DefaultNumericTypes>::new();
let mut eval_ctx = somni_expr::Context::new();
if let Some(chip) = chip {
eval_ctx
.set_value(
"chip".into(),
evalexpr::Value::String(chip.name().to_string()),
)
.map_err(|err| Error::Parse(err.to_string()))?;
eval_ctx
.set_function(
"feature".into(),
evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
if let evalexpr::Value::String(which) = arg {
let res = chip.contains(which);
Ok(evalexpr::Value::Boolean(res))
} else {
Err(evalexpr::EvalexprError::CustomMessage(format!(
"Bad argument: {arg:?}"
)))
}
}),
)
.map_err(|err| Error::Parse(err.to_string()))?;
eval_ctx
.set_function(
"cargo_feature".into(),
evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
if let evalexpr::Value::String(which) = arg {
let res = features.contains(&which.to_uppercase().replace("-", "_"));
Ok(evalexpr::Value::Boolean(res))
} else {
Err(evalexpr::EvalexprError::CustomMessage(format!(
"Bad argument: {arg:?}"
)))
}
}),
)
.map_err(|err| Error::Parse(err.to_string()))?;
eval_ctx
.set_function(
"ignore_feature_gates".into(),
evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
if let evalexpr::Value::Empty = arg {
Ok(evalexpr::Value::Boolean(ignore_feature_gates))
} else {
Err(evalexpr::EvalexprError::CustomMessage(format!(
"Bad argument: {arg:?}"
)))
}
}),
)
.map_err(|err| Error::Parse(err.to_string()))?;
eval_ctx.add_variable("chip", chip.name());
eval_ctx.add_variable("ignore_feature_gates", ignore_feature_gates);
eval_ctx.add_function("feature", move |feature: &str| chip.contains(feature));
eval_ctx.add_function("cargo_feature", |feature: &str| {
features.contains(&feature.to_uppercase().replace("-", "_"))
});
}
for option in config.options.clone() {
let active = evalexpr::eval_with_context(&option.active, &eval_ctx)
.map_err(|err| {
Error::Parse(format!(
"Error evaluating '{}', error = {:?}",
option.active, err
))
})?
.as_boolean()
.map_err(|err| {
Error::Parse(format!(
"Error evaluating '{}', error = {:?}",
option.active, err
))
})?;
for option in &config.options {
let active = eval_ctx
.evaluate::<bool>(&option.active)
.map_err(|err| Error::Parse(format!("{err:?}")))?;
let constraint = {
let mut active_constraint = None;
if let Some(constraints) = &option.constraints {
for constraint in constraints {
if evalexpr::eval_with_context(&constraint.if_, &eval_ctx)
.map_err(|err| {
Error::Parse(format!(
"Error evaluating '{}', error = {err:?}",
constraint.if_
))
})?
.as_boolean()
.map_err(|err| {
Error::Parse(format!(
"Error evaluating '{}', error = {:?}",
constraint.if_, err
))
})?
if eval_ctx
.evaluate::<bool>(&constraint.if_)
.map_err(|err| Error::Parse(format!("{err:?}")))?
{
active_constraint = Some(constraint.type_.clone());
break;
@ -299,14 +240,12 @@ pub fn evaluate_yaml_config(
let default_value = {
let mut default_value = None;
for value in option.default.clone() {
if evalexpr::eval_with_context(&value.if_, &eval_ctx)
.and_then(|v| v.as_boolean())
.map_err(|err| {
Error::Parse(format!("Error evaluating '{}', error = {err:?}", value.if_))
})?
for value in &option.default {
if eval_ctx
.evaluate::<bool>(&value.if_)
.map_err(|err| Error::Parse(format!("{err:?}")))?
{
default_value = Some(value.value);
default_value = Some(value.value.clone());
break;
}
}
@ -323,13 +262,12 @@ pub fn evaluate_yaml_config(
let option = ConfigOption {
name: option.name.clone(),
description: option.description,
default_value: default_value.ok_or(Error::Parse(format!(
"No default value found for {}",
option.name
)))?,
description: option.description.clone(),
default_value: default_value.ok_or_else(|| {
Error::Parse(format!("No default value found for {}", option.name))
})?,
constraint,
stability: option.stability,
stability: option.stability.clone(),
active,
display_hint: option.display_hint.unwrap_or(DisplayHint::None),
};

View File

@ -1,47 +1,49 @@
crate: esp-hal-embassy
options:
- name: low-power-wait
description: "Enables the lower-power wait if no tasks are ready to run on the
thread-mode executor. This allows the MCU to use less power if the workload allows.
Recommended for battery-powered systems. May impact analog performance."
default:
- value: true
- name: low-power-wait
description:
"Enables the lower-power wait if no tasks are ready to run on the
thread-mode executor. This allows the MCU to use less power if the workload allows.
Recommended for battery-powered systems. May impact analog performance."
default:
- value: true
- name: timer-queue
description: "The flavour of the timer queue provided by this crate. Integrated
queues require the `executors` feature to be enabled.</p><p>If you use
embassy-executor, the `single-integrated` queue is recommended for ease of use,
while the `multiple-integrated` queue is recommended for performance. The
`multiple-integrated` option needs one timer per executor.</p><p>The `generic`
queue allows using embassy-time without the embassy executors."
default:
- if: 'ignore_feature_gates()'
value: '"single-integrated"'
- if: 'cargo_feature("executors")'
value: '"single-integrated"'
- if: 'true'
value: '"generic"'
constraints:
- if: 'cargo_feature("executors") || ignore_feature_gates()'
type:
validator: enumeration
value:
- 'generic'
- 'single-integrated'
- 'multiple-integrated'
- if: 'true'
type:
validator: enumeration
value:
- 'generic'
stability: Unstable
active: 'cargo_feature("executors") || ignore_feature_gates()'
- name: timer-queue
description:
"The flavour of the timer queue provided by this crate. Integrated
queues require the `executors` feature to be enabled.</p><p>If you use
embassy-executor, the `single-integrated` queue is recommended for ease of use,
while the `multiple-integrated` queue is recommended for performance. The
`multiple-integrated` option needs one timer per executor.</p><p>The `generic`
queue allows using embassy-time without the embassy executors."
default:
- if: "ignore_feature_gates"
value: '"single-integrated"'
- if: 'cargo_feature("executors")'
value: '"single-integrated"'
- if: "true"
value: '"generic"'
constraints:
- if: 'cargo_feature("executors") || ignore_feature_gates'
type:
validator: enumeration
value:
- "generic"
- "single-integrated"
- "multiple-integrated"
- if: "true"
type:
validator: enumeration
value:
- "generic"
stability: Unstable
active: 'cargo_feature("executors") || ignore_feature_gates'
- name: generic-queue-size
description: The capacity of the queue when the `generic` timer queue flavour is selected.
default:
- value: 64
constraints:
- type:
validator: positive_integer
- name: generic-queue-size
description: The capacity of the queue when the `generic` timer queue flavour is selected.
default:
- value: 64
constraints:
- type:
validator: positive_integer