Skip to main content

sim_lib_music_serial/
practice.rs

1//! Inspectable serial-practice policies built from open rule components.
2
3use std::collections::BTreeMap;
4use std::fmt::{Display, Formatter};
5use std::sync::Arc;
6
7use crate::practice_builtin::evaluate_builtin;
8use crate::{
9    InvariantLedger, InvariantLedgerEntry, SerialPlan, SerialPracticeReport, SerialReading,
10    WaiverId,
11};
12
13/// Stable identity for one named serial practice.
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct PracticeId(String);
16
17/// Stable identity for one inspectable practice rule.
18#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct PracticeRuleId(String);
20
21fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
22    let value = value.into();
23    if value.trim().is_empty() {
24        return Err(format!("{kind} cannot be empty"));
25    }
26    if value
27        .chars()
28        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
29    {
30        return Err(format!(
31            "{kind} must use ASCII letters, digits, /, -, _, or ."
32        ));
33    }
34    Ok(value)
35}
36
37macro_rules! stable_id {
38    ($name:ident, $kind:literal, $doc:literal) => {
39        #[doc = $doc]
40        impl $name {
41            /// Creates a validated stable identifier.
42            pub fn new(value: impl Into<String>) -> Result<Self, String> {
43                Ok(Self(validate_id($kind, value)?))
44            }
45
46            /// Returns the stable wire text.
47            pub fn as_str(&self) -> &str {
48                &self.0
49            }
50        }
51
52        impl Display for $name {
53            fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
54                formatter.write_str(&self.0)
55            }
56        }
57    };
58}
59
60stable_id!(
61    PracticeId,
62    "practice-id",
63    "Stable identity for one named serial practice."
64);
65stable_id!(
66    PracticeRuleId,
67    "practice-rule-id",
68    "Stable identity for one serial practice rule."
69);
70
71/// Public category of one built-in practice rule.
72#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub enum PracticeRuleKind {
74    /// Enforce one aggregate expectation over structural ordinals.
75    Aggregate,
76    /// Enforce one order expectation over first structural appearances.
77    Order,
78    /// Enforce one no-repeat expectation over ordinal references.
79    Repeats,
80    /// Enforce one no-doubling expectation inside simultaneous groups.
81    Doublings,
82    /// Enforce one simultaneity policy.
83    Simultaneity,
84    /// Enforce one no-row-mixing expectation inside an event.
85    RowMixing,
86    /// Enforce one policy for externally sourced material.
87    ForeignMaterial,
88    /// Enforce one policy for non-structural reuse after parameter exhaustion.
89    ParameterExhaustion,
90}
91
92/// One inspectable parameter attached to a practice rule.
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct PracticeRuleParameter {
95    /// Stable parameter name.
96    pub name: String,
97    /// Stable printable value.
98    pub value: String,
99}
100
101/// Public inspectable description of one practice rule.
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct PracticeRuleSpec {
104    /// Stable rule identity.
105    pub id: PracticeRuleId,
106    /// Built-in rule kind.
107    pub kind: PracticeRuleKind,
108    /// Expected fact enforced by the rule.
109    pub expected_fact: String,
110    /// Inspectable policy parameters.
111    pub parameters: Vec<PracticeRuleParameter>,
112}
113
114/// Open rule component used by one serial practice.
115pub trait PracticeRule: Send + Sync {
116    /// Returns the stable rule identity.
117    fn id(&self) -> &PracticeRuleId;
118
119    /// Returns the inspectable rule specification.
120    fn spec(&self) -> PracticeRuleSpec;
121
122    /// Evaluates this rule over one named reading.
123    fn evaluate(
124        &self,
125        plan: &SerialPlan,
126        reading: SerialReading,
127        waivers: &DeclaredWaivers,
128    ) -> InvariantLedgerEntry<PracticeRuleId>;
129}
130
131/// Explicit waivers declared for one practice run.
132#[derive(Clone, Debug, Default, PartialEq, Eq)]
133pub struct DeclaredWaivers {
134    by_rule: BTreeMap<PracticeRuleId, WaiverId>,
135}
136
137impl DeclaredWaivers {
138    /// Creates a waiver set from stable rule/waiver pairs.
139    pub fn new(entries: impl IntoIterator<Item = (PracticeRuleId, WaiverId)>) -> Self {
140        Self {
141            by_rule: entries.into_iter().collect(),
142        }
143    }
144
145    pub(crate) fn waiver_for(&self, rule_id: &PracticeRuleId) -> Option<WaiverId> {
146        self.by_rule.get(rule_id).cloned()
147    }
148}
149
150/// Named serial practice composed from open rule components.
151#[derive(Clone)]
152pub struct SerialPractice {
153    /// Stable practice identity.
154    pub id: PracticeId,
155    /// Open rule components evaluated in order.
156    pub rules: Vec<Arc<dyn PracticeRule>>,
157}
158
159impl SerialPractice {
160    /// Builds one named serial practice from open rule components.
161    pub fn new(id: PracticeId, rules: Vec<Arc<dyn PracticeRule>>) -> Self {
162        Self { id, rules }
163    }
164
165    /// Returns the inspectable built-in and custom rule specifications.
166    pub fn rule_specs(&self) -> Vec<PracticeRuleSpec> {
167        self.rules.iter().map(|rule| rule.spec()).collect()
168    }
169
170    /// Evaluates every rule over one named reading.
171    pub fn evaluate(
172        &self,
173        plan: &SerialPlan,
174        reading: SerialReading,
175        waivers: &DeclaredWaivers,
176    ) -> SerialPracticeReport {
177        let entries = self
178            .rules
179            .iter()
180            .map(|rule| rule.evaluate(plan, reading, waivers))
181            .collect();
182        SerialPracticeReport {
183            practice_id: self.id.clone(),
184            reading,
185            ledger: InvariantLedger::new(entries),
186        }
187    }
188}
189
190/// Built-in inspectable rule implementations for common serial-practice checks.
191#[derive(Clone, Debug)]
192pub struct BuiltInPracticeRule {
193    spec: PracticeRuleSpec,
194    evaluator: BuiltInRuleEvaluator,
195}
196
197#[derive(Clone, Debug)]
198pub(crate) enum BuiltInRuleEvaluator {
199    Aggregate,
200    Order,
201    Repeats,
202    Doublings,
203    Simultaneity { allow: bool },
204    RowMixing,
205    ForeignMaterial { allow_external: bool },
206    ParameterExhaustion,
207}
208
209impl BuiltInPracticeRule {
210    /// Requires every structural ordinal to appear exactly once under the reading.
211    pub fn aggregate(id: PracticeRuleId) -> Self {
212        Self {
213            spec: PracticeRuleSpec {
214                id,
215                kind: PracticeRuleKind::Aggregate,
216                expected_fact: "each structural ordinal appears exactly once".to_owned(),
217                parameters: Vec::new(),
218            },
219            evaluator: BuiltInRuleEvaluator::Aggregate,
220        }
221    }
222
223    /// Requires first structural appearances to remain in row order.
224    pub fn order(id: PracticeRuleId) -> Self {
225        Self {
226            spec: PracticeRuleSpec {
227                id,
228                kind: PracticeRuleKind::Order,
229                expected_fact: "first structural appearances preserve row order".to_owned(),
230                parameters: Vec::new(),
231            },
232            evaluator: BuiltInRuleEvaluator::Order,
233        }
234    }
235
236    /// Forbids repeated ordinal references under the reading.
237    pub fn repeats(id: PracticeRuleId) -> Self {
238        Self {
239            spec: PracticeRuleSpec {
240                id,
241                kind: PracticeRuleKind::Repeats,
242                expected_fact: "no structural ordinal repeats".to_owned(),
243                parameters: Vec::new(),
244            },
245            evaluator: BuiltInRuleEvaluator::Repeats,
246        }
247    }
248
249    /// Forbids simultaneous doublings of one row pitch class.
250    pub fn doublings(id: PracticeRuleId) -> Self {
251        Self {
252            spec: PracticeRuleSpec {
253                id,
254                kind: PracticeRuleKind::Doublings,
255                expected_fact: "simultaneous groups avoid doubled row pitch classes".to_owned(),
256                parameters: Vec::new(),
257            },
258            evaluator: BuiltInRuleEvaluator::Doublings,
259        }
260    }
261
262    /// Controls whether simultaneous groups are accepted.
263    pub fn simultaneity(id: PracticeRuleId, allow: bool) -> Self {
264        Self {
265            spec: PracticeRuleSpec {
266                id,
267                kind: PracticeRuleKind::Simultaneity,
268                expected_fact: if allow {
269                    "simultaneous groups are explicitly permitted".to_owned()
270                } else {
271                    "no simultaneous groups occur".to_owned()
272                },
273                parameters: vec![PracticeRuleParameter {
274                    name: "allow".to_owned(),
275                    value: allow.to_string(),
276                }],
277            },
278            evaluator: BuiltInRuleEvaluator::Simultaneity { allow },
279        }
280    }
281
282    /// Forbids one event from mixing ordinals from multiple row instances.
283    pub fn row_mixing(id: PracticeRuleId) -> Self {
284        Self {
285            spec: PracticeRuleSpec {
286                id,
287                kind: PracticeRuleKind::RowMixing,
288                expected_fact: "each event cites exactly one row instance".to_owned(),
289                parameters: Vec::new(),
290            },
291            evaluator: BuiltInRuleEvaluator::RowMixing,
292        }
293    }
294
295    /// Controls whether externally sourced material is accepted.
296    pub fn foreign_material(id: PracticeRuleId, allow_external: bool) -> Self {
297        Self {
298            spec: PracticeRuleSpec {
299                id,
300                kind: PracticeRuleKind::ForeignMaterial,
301                expected_fact: if allow_external {
302                    "external material is explicitly permitted".to_owned()
303                } else {
304                    "no external material occurs".to_owned()
305                },
306                parameters: vec![PracticeRuleParameter {
307                    name: "allow_external".to_owned(),
308                    value: allow_external.to_string(),
309                }],
310            },
311            evaluator: BuiltInRuleEvaluator::ForeignMaterial { allow_external },
312        }
313    }
314
315    /// Requires non-structural reuse after full aggregate exposure to be declared as a relaxation.
316    pub fn parameter_exhaustion(id: PracticeRuleId) -> Self {
317        Self {
318            spec: PracticeRuleSpec {
319                id,
320                kind: PracticeRuleKind::ParameterExhaustion,
321                expected_fact: "non-structural events do not reuse exhausted structural parameters"
322                    .to_owned(),
323                parameters: Vec::new(),
324            },
325            evaluator: BuiltInRuleEvaluator::ParameterExhaustion,
326        }
327    }
328}
329
330impl PracticeRule for BuiltInPracticeRule {
331    fn id(&self) -> &PracticeRuleId {
332        &self.spec.id
333    }
334
335    fn spec(&self) -> PracticeRuleSpec {
336        self.spec.clone()
337    }
338
339    fn evaluate(
340        &self,
341        plan: &SerialPlan,
342        reading: SerialReading,
343        waivers: &DeclaredWaivers,
344    ) -> InvariantLedgerEntry<PracticeRuleId> {
345        let waived = waivers.waiver_for(self.id());
346        evaluate_builtin(&self.evaluator, plan, reading, &self.spec, waived)
347    }
348}