1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

expr: strict eval

This commit is contained in:
RGBCube 2025-06-04 22:50:22 +03:00
parent 917ed77255
commit 2c154cd589
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
2 changed files with 172 additions and 145 deletions

View file

@ -195,10 +195,11 @@ mod expression {
named!(cpu_temperature => "$cpu-temperature");
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
named!(cpu_idle_seconds => "$cpu-idle-seconds");
named!(power_supply_charge => "%power-supply-charge");
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
named!(charging => "?charging");
named!(on_battery => "?on-battery");
named!(discharging => "?discharging");
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
@ -225,65 +226,80 @@ pub enum Expression {
#[serde(with = "expression::power_supply_discharge_rate")]
PowerSupplyDischargeRate,
#[serde(with = "expression::charging")]
Charging,
#[serde(with = "expression::on_battery")]
OnBattery,
#[serde(with = "expression::discharging")]
Discharging,
Boolean(bool),
Number(f64),
Plus {
value: Box<Expression>,
plus: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "plus")]
b: Box<Expression>,
},
Minus {
value: Box<Expression>,
minus: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "minus")]
b: Box<Expression>,
},
Multiply {
value: Box<Expression>,
multiply: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "multiply")]
b: Box<Expression>,
},
Power {
value: Box<Expression>,
power: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "power")]
b: Box<Expression>,
},
Divide {
value: Box<Expression>,
divide: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "divide")]
b: Box<Expression>,
},
#[serde(rename_all = "kebab-case")]
LessThan {
value: Box<Expression>,
is_less_than: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-less-than")]
b: Box<Expression>,
},
#[serde(rename_all = "kebab-case")]
MoreThan {
value: Box<Expression>,
is_more_than: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-more-than")]
b: Box<Expression>,
},
#[serde(rename_all = "kebab-case")]
Equal {
value: Box<Expression>,
is_equal: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-equal")]
b: Box<Expression>,
leeway: Box<Expression>,
},
And {
value: Box<Expression>,
and: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "and")]
b: Box<Expression>,
},
All {
all: Vec<Expression>,
},
Or {
value: Box<Expression>,
or: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "or")]
b: Box<Expression>,
},
Any {
any: Vec<Expression>,
@ -318,6 +334,121 @@ impl Expression {
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EvalState {
pub cpu_usage: f64,
pub cpu_usage_volatility: Option<f64>,
pub cpu_temperature: f64,
pub cpu_temperature_volatility: Option<f64>,
pub cpu_idle_seconds: f64,
pub power_supply_charge: f64,
pub power_supply_discharge_rate: Option<f64>,
pub discharging: bool,
}
impl Expression {
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
use Expression::*;
macro_rules! try_ok {
($expression:expr) => {
match $expression {
Some(value) => value,
None => return Ok(None),
}
};
}
macro_rules! eval {
($expression:expr) => {
try_ok!($expression.eval(state)?)
};
}
// [e8dax09]: This may be look inefficient, and it definitely isn't optimal,
// but expressions in rules are usually so small that it doesn't matter or
// make a perceiveable performance difference.
//
// We also want to be strict, instead of lazy in binary operations, because
// we want to catch type errors immediately.
//
// FIXME: We currently cannot catch errors that will happen when propagating None.
// You can have a type error go uncaught on first startup by using $cpu-usage-volatility
// incorrectly, for example.
Ok(Some(match self {
CpuUsage => Number(state.cpu_usage),
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
CpuTemperature => Number(state.cpu_temperature),
CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)),
CpuIdleSeconds => Number(state.cpu_idle_seconds),
PowerSupplyCharge => Number(state.cpu_idle_seconds),
PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)),
Discharging => Boolean(state.discharging),
literal @ (Boolean(_) | Number(_)) => literal.clone(),
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?),
Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?),
Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)),
Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?),
MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?),
Equal { a, b, leeway } => {
let a = eval!(a).as_number()?;
let b = eval!(b).as_number()?;
let leeway = eval!(leeway).as_number()?;
let minimum = a - leeway;
let maximum = a + leeway;
Boolean(minimum < b && b < maximum)
}
And { a, b } => {
let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?;
Boolean(a && b)
}
All { all } => {
let mut result = true;
for value in all {
let value = eval!(value).as_boolean()?;
result = result && value;
}
Boolean(result)
}
Or { a, b } => {
let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?;
Boolean(a || b)
}
Any { any } => {
let mut result = false;
for value in any {
let value = eval!(value).as_boolean()?;
result = result || value;
}
Boolean(result)
}
Not { not } => Boolean(!eval!(not).as_boolean()?),
}))
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Rule {

View file

@ -306,121 +306,6 @@ impl Daemon {
}
}
impl Daemon {
fn eval(&self, expression: &config::Expression) -> anyhow::Result<Option<config::Expression>> {
use config::Expression::*;
macro_rules! try_ok {
($expression:expr) => {
match $expression {
Some(value) => value,
None => return Ok(None),
}
};
}
Ok(Some(match expression {
CpuUsage => Number(self.cpu_log.back().unwrap().usage),
CpuUsageVolatility => Number(try_ok!(self.cpu_volatility()).usage),
CpuTemperature => Number(self.cpu_log.back().unwrap().temperature),
CpuTemperatureVolatility => Number(try_ok!(self.cpu_volatility()).temperature),
CpuIdleSeconds => Number(self.last_user_activity.elapsed().as_secs_f64()),
PowerSupplyCharge => Number(self.power_supply_log.back().unwrap().charge),
PowerSupplyDischargeRate => Number(try_ok!(self.power_supply_discharge_rate())),
Charging => Boolean(!self.discharging()),
OnBattery => Boolean(self.discharging()),
literal @ Boolean(_) | literal @ Number(_) => literal.clone(),
Plus { value, plus } => Number(
try_ok!(self.eval(value)?).as_number()? + try_ok!(self.eval(plus)?).as_number()?,
),
Minus { value, minus } => Number(
try_ok!(self.eval(value)?).as_number()? - try_ok!(self.eval(minus)?).as_number()?,
),
Multiply { value, multiply } => Number(
try_ok!(self.eval(value)?).as_number()?
* try_ok!(self.eval(multiply)?).as_number()?,
),
Power { value, power } => Number(
try_ok!(self.eval(value)?)
.as_number()?
.powf(try_ok!(self.eval(power)?).as_number()?),
),
Divide { value, divide } => Number(
try_ok!(self.eval(value)?).as_number()?
/ try_ok!(self.eval(divide)?).as_number()?,
),
LessThan {
value,
is_less_than,
} => Boolean(
try_ok!(self.eval(value)?).as_number()?
< try_ok!(self.eval(is_less_than)?).as_number()?,
),
MoreThan {
value,
is_more_than,
} => Boolean(
try_ok!(self.eval(value)?).as_number()?
> try_ok!(self.eval(is_more_than)?).as_number()?,
),
Equal {
value,
is_equal,
leeway,
} => {
let value = try_ok!(self.eval(value)?).as_number()?;
let leeway = try_ok!(self.eval(leeway)?).as_number()?;
let is_equal = try_ok!(self.eval(is_equal)?).as_number()?;
let minimum = value - leeway;
let maximum = value + leeway;
Boolean(minimum < is_equal && is_equal < maximum)
}
And { value, and } => Boolean(
try_ok!(self.eval(value)?).as_boolean()?
&& try_ok!(self.eval(and)?).as_boolean()?,
),
All { all } => {
let mut result = true;
for value in all {
result = result && try_ok!(self.eval(value)?).as_boolean()?;
if !result {
break;
}
}
Boolean(result)
}
Or { value, or } => Boolean(
try_ok!(self.eval(value)?).as_boolean()? || try_ok!(self.eval(or)?).as_boolean()?,
),
Any { any } => {
let mut result = false;
for value in any {
result = result || try_ok!(self.eval(value)?).as_boolean()?;
if result {
break;
}
}
Boolean(result)
}
Not { not } => Boolean(!try_ok!(self.eval(not)?).as_boolean()?),
}))
}
}
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
assert!(config.rules.is_sorted_by_key(|rule| rule.priority));
@ -451,8 +336,19 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
let sleep_until = Instant::now() + daemon.polling_interval();
let state = config::EvalState {
cpu_usage: daemon.cpu_log.back().unwrap().usage,
cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage),
cpu_temperature: daemon.cpu_log.back().unwrap().temperature,
cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature),
cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(),
power_supply_charge: daemon.power_supply_log.back().unwrap().charge,
power_supply_discharge_rate: daemon.power_supply_discharge_rate(),
discharging: daemon.discharging(),
};
for rule in &config.rules {
let Some(condition) = daemon.eval(&rule.if_)? else {
let Some(condition) = rule.if_.eval(&state)? else {
continue;
};