use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use crate::practice_builtin::evaluate_builtin;
use crate::{
InvariantLedger, InvariantLedgerEntry, SerialPlan, SerialPracticeReport, SerialReading,
WaiverId,
};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PracticeId(String);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PracticeRuleId(String);
fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
let value = value.into();
if value.trim().is_empty() {
return Err(format!("{kind} cannot be empty"));
}
if value
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
{
return Err(format!(
"{kind} must use ASCII letters, digits, /, -, _, or ."
));
}
Ok(value)
}
macro_rules! stable_id {
($name:ident, $kind:literal, $doc:literal) => {
#[doc = $doc]
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, String> {
Ok(Self(validate_id($kind, value)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for $name {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
};
}
stable_id!(
PracticeId,
"practice-id",
"Stable identity for one named serial practice."
);
stable_id!(
PracticeRuleId,
"practice-rule-id",
"Stable identity for one serial practice rule."
);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PracticeRuleKind {
Aggregate,
Order,
Repeats,
Doublings,
Simultaneity,
RowMixing,
ForeignMaterial,
ParameterExhaustion,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PracticeRuleParameter {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PracticeRuleSpec {
pub id: PracticeRuleId,
pub kind: PracticeRuleKind,
pub expected_fact: String,
pub parameters: Vec<PracticeRuleParameter>,
}
pub trait PracticeRule: Send + Sync {
fn id(&self) -> &PracticeRuleId;
fn spec(&self) -> PracticeRuleSpec;
fn evaluate(
&self,
plan: &SerialPlan,
reading: SerialReading,
waivers: &DeclaredWaivers,
) -> InvariantLedgerEntry<PracticeRuleId>;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DeclaredWaivers {
by_rule: BTreeMap<PracticeRuleId, WaiverId>,
}
impl DeclaredWaivers {
pub fn new(entries: impl IntoIterator<Item = (PracticeRuleId, WaiverId)>) -> Self {
Self {
by_rule: entries.into_iter().collect(),
}
}
pub(crate) fn waiver_for(&self, rule_id: &PracticeRuleId) -> Option<WaiverId> {
self.by_rule.get(rule_id).cloned()
}
}
#[derive(Clone)]
pub struct SerialPractice {
pub id: PracticeId,
pub rules: Vec<Arc<dyn PracticeRule>>,
}
impl SerialPractice {
pub fn new(id: PracticeId, rules: Vec<Arc<dyn PracticeRule>>) -> Self {
Self { id, rules }
}
pub fn rule_specs(&self) -> Vec<PracticeRuleSpec> {
self.rules.iter().map(|rule| rule.spec()).collect()
}
pub fn evaluate(
&self,
plan: &SerialPlan,
reading: SerialReading,
waivers: &DeclaredWaivers,
) -> SerialPracticeReport {
let entries = self
.rules
.iter()
.map(|rule| rule.evaluate(plan, reading, waivers))
.collect();
SerialPracticeReport {
practice_id: self.id.clone(),
reading,
ledger: InvariantLedger::new(entries),
}
}
}
#[derive(Clone, Debug)]
pub struct BuiltInPracticeRule {
spec: PracticeRuleSpec,
evaluator: BuiltInRuleEvaluator,
}
#[derive(Clone, Debug)]
pub(crate) enum BuiltInRuleEvaluator {
Aggregate,
Order,
Repeats,
Doublings,
Simultaneity { allow: bool },
RowMixing,
ForeignMaterial { allow_external: bool },
ParameterExhaustion,
}
impl BuiltInPracticeRule {
pub fn aggregate(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::Aggregate,
expected_fact: "each structural ordinal appears exactly once".to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::Aggregate,
}
}
pub fn order(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::Order,
expected_fact: "first structural appearances preserve row order".to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::Order,
}
}
pub fn repeats(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::Repeats,
expected_fact: "no structural ordinal repeats".to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::Repeats,
}
}
pub fn doublings(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::Doublings,
expected_fact: "simultaneous groups avoid doubled row pitch classes".to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::Doublings,
}
}
pub fn simultaneity(id: PracticeRuleId, allow: bool) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::Simultaneity,
expected_fact: if allow {
"simultaneous groups are explicitly permitted".to_owned()
} else {
"no simultaneous groups occur".to_owned()
},
parameters: vec![PracticeRuleParameter {
name: "allow".to_owned(),
value: allow.to_string(),
}],
},
evaluator: BuiltInRuleEvaluator::Simultaneity { allow },
}
}
pub fn row_mixing(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::RowMixing,
expected_fact: "each event cites exactly one row instance".to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::RowMixing,
}
}
pub fn foreign_material(id: PracticeRuleId, allow_external: bool) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::ForeignMaterial,
expected_fact: if allow_external {
"external material is explicitly permitted".to_owned()
} else {
"no external material occurs".to_owned()
},
parameters: vec![PracticeRuleParameter {
name: "allow_external".to_owned(),
value: allow_external.to_string(),
}],
},
evaluator: BuiltInRuleEvaluator::ForeignMaterial { allow_external },
}
}
pub fn parameter_exhaustion(id: PracticeRuleId) -> Self {
Self {
spec: PracticeRuleSpec {
id,
kind: PracticeRuleKind::ParameterExhaustion,
expected_fact: "non-structural events do not reuse exhausted structural parameters"
.to_owned(),
parameters: Vec::new(),
},
evaluator: BuiltInRuleEvaluator::ParameterExhaustion,
}
}
}
impl PracticeRule for BuiltInPracticeRule {
fn id(&self) -> &PracticeRuleId {
&self.spec.id
}
fn spec(&self) -> PracticeRuleSpec {
self.spec.clone()
}
fn evaluate(
&self,
plan: &SerialPlan,
reading: SerialReading,
waivers: &DeclaredWaivers,
) -> InvariantLedgerEntry<PracticeRuleId> {
let waived = waivers.waiver_for(self.id());
evaluate_builtin(&self.evaluator, plan, reading, &self.spec, waived)
}
}