1use 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#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct PracticeId(String);
16
17#[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 pub fn new(value: impl Into<String>) -> Result<Self, String> {
43 Ok(Self(validate_id($kind, value)?))
44 }
45
46 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub enum PracticeRuleKind {
74 Aggregate,
76 Order,
78 Repeats,
80 Doublings,
82 Simultaneity,
84 RowMixing,
86 ForeignMaterial,
88 ParameterExhaustion,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct PracticeRuleParameter {
95 pub name: String,
97 pub value: String,
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct PracticeRuleSpec {
104 pub id: PracticeRuleId,
106 pub kind: PracticeRuleKind,
108 pub expected_fact: String,
110 pub parameters: Vec<PracticeRuleParameter>,
112}
113
114pub trait PracticeRule: Send + Sync {
116 fn id(&self) -> &PracticeRuleId;
118
119 fn spec(&self) -> PracticeRuleSpec;
121
122 fn evaluate(
124 &self,
125 plan: &SerialPlan,
126 reading: SerialReading,
127 waivers: &DeclaredWaivers,
128 ) -> InvariantLedgerEntry<PracticeRuleId>;
129}
130
131#[derive(Clone, Debug, Default, PartialEq, Eq)]
133pub struct DeclaredWaivers {
134 by_rule: BTreeMap<PracticeRuleId, WaiverId>,
135}
136
137impl DeclaredWaivers {
138 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#[derive(Clone)]
152pub struct SerialPractice {
153 pub id: PracticeId,
155 pub rules: Vec<Arc<dyn PracticeRule>>,
157}
158
159impl SerialPractice {
160 pub fn new(id: PracticeId, rules: Vec<Arc<dyn PracticeRule>>) -> Self {
162 Self { id, rules }
163 }
164
165 pub fn rule_specs(&self) -> Vec<PracticeRuleSpec> {
167 self.rules.iter().map(|rule| rule.spec()).collect()
168 }
169
170 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#[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 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 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 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 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 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 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 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 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}