1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use sim_lib_pitch_serial::RowForm;
5use thiserror::Error;
6
7use crate::{
8 BuiltInPracticeRule, EventPlacement, OrdinalRef, PlannedSerialEvent, PracticeRule,
9 PracticeRuleId, RowInstanceId, SerialEventId, SerialOrigin, SerialPlan, SerialPractice,
10 SerialRole, StructuralLicense, VoiceId,
11};
12
13#[derive(Clone, Debug, PartialEq, Eq, Error)]
15pub enum SerialDeployError {
16 #[error("{0}")]
18 InvalidTechniqueId(String),
19 #[error("technique {0} must contain at least one deployer")]
21 EmptyTechnique(String),
22 #[error("deployer {deployer} references unknown row {row_id}")]
24 UnknownRow {
25 deployer: String,
27 row_id: RowInstanceId,
29 },
30 #[error("deployer {deployer} attempted to reuse event id {event_id}")]
32 DuplicateEventId {
33 deployer: String,
35 event_id: SerialEventId,
37 },
38 #[error("deployer {deployer} expected {expected} voices but received {actual}")]
40 VoiceCountMismatch {
41 deployer: String,
43 expected: usize,
45 actual: usize,
47 },
48 #[error("aggregate rotation block lengths must sum to 12, received {0}")]
50 InvalidRotationCoverage(usize),
51 #[error("deployer {deployer} requires an interlocking partition witness")]
53 NotInterlocking {
54 deployer: String,
56 },
57 #[error(
59 "rows {source_row_id} and {partner_row_id} are not combinatorial at block size {block_size}"
60 )]
61 NotCombinatorial {
62 source_row_id: RowInstanceId,
64 partner_row_id: RowInstanceId,
66 block_size: usize,
68 },
69 #[error("partition build failed: {0}")]
71 Partition(String),
72 #[error("serial plan validation failed: {0}")]
74 Plan(String),
75}
76
77pub(crate) fn validate_technique_id(value: impl Into<String>) -> Result<String, SerialDeployError> {
78 let value = value.into();
79 if value.trim().is_empty() {
80 return Err(SerialDeployError::InvalidTechniqueId(
81 "technique id cannot be empty".to_owned(),
82 ));
83 }
84 if value
85 .chars()
86 .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
87 {
88 return Err(SerialDeployError::InvalidTechniqueId(
89 "technique id must use ASCII letters, digits, /, -, _, or .".to_owned(),
90 ));
91 }
92 Ok(value)
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct SerialDeployerParameter {
98 pub name: String,
100 pub value: String,
102}
103
104#[derive(Copy, Clone, Debug, PartialEq, Eq)]
106pub enum SerialDeployerKind {
107 CompleteHorizontalStatement,
109 MotivicPartition,
111 VerticalBlocks,
113 InterlockingPartition,
115 MelodyAccompanimentDistribution,
117 AggregateRotation,
119 SimultaneousForms,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct SerialDeployerSpec {
126 pub kind: SerialDeployerKind,
128 pub label: String,
130 pub expected_fact: String,
132 pub parameters: Vec<SerialDeployerParameter>,
134}
135
136#[derive(Clone)]
137pub(crate) enum SerialDeployerInner {
138 Horizontal(super::components::HorizontalStatementSpec),
139 Motivic(super::components::MotivicPartitionSpec),
140 Vertical(super::components::VerticalBlocksSpec),
141 Interlocking(super::components::InterlockingPartitionSpec),
142 MelodyAccompaniment(super::components::MelodyAccompanimentSpec),
143 AggregateRotation(super::components::AggregateRotationSpec),
144 SimultaneousForms(super::components::SimultaneousFormsSpec),
145}
146
147#[derive(Clone)]
149pub struct SerialDeployer {
150 pub(crate) inner: SerialDeployerInner,
151}
152
153impl SerialDeployer {
154 pub(crate) fn spec(&self) -> SerialDeployerSpec {
155 match &self.inner {
156 SerialDeployerInner::Horizontal(spec) => SerialDeployerSpec {
157 kind: SerialDeployerKind::CompleteHorizontalStatement,
158 label: spec.event_id.as_str().to_owned(),
159 expected_fact: "one complete row sounds in sequence".to_owned(),
160 parameters: vec![
161 param("row", spec.row_id.as_str()),
162 param("voice", &spec.voice.to_string()),
163 param("reading", spec.license.reading_id.as_str()),
164 ],
165 },
166 SerialDeployerInner::Motivic(spec) => SerialDeployerSpec {
167 kind: SerialDeployerKind::MotivicPartition,
168 label: spec.event_prefix.clone(),
169 expected_fact: "partition blocks sound sequentially as inspectable motives"
170 .to_owned(),
171 parameters: vec![
172 param("row", spec.row_id.as_str()),
173 param("blocks", &spec.partition.block_count().to_string()),
174 param("reading", spec.license.reading_id.as_str()),
175 ],
176 },
177 SerialDeployerInner::Vertical(spec) => SerialDeployerSpec {
178 kind: SerialDeployerKind::VerticalBlocks,
179 label: spec.event_prefix.clone(),
180 expected_fact: "selected blocks sound as chordal vertical events".to_owned(),
181 parameters: vec![
182 param("row", spec.row_id.as_str()),
183 param("blocks", &format!("{:?}", spec.selected_blocks)),
184 param("reading", spec.license.reading_id.as_str()),
185 ],
186 },
187 SerialDeployerInner::Interlocking(spec) => SerialDeployerSpec {
188 kind: SerialDeployerKind::InterlockingPartition,
189 label: spec.event_prefix.clone(),
190 expected_fact: "interlocking block evidence licenses sequential partition exchange"
191 .to_owned(),
192 parameters: vec![
193 param("row", spec.row_id.as_str()),
194 param("blocks", &spec.partition.block_count().to_string()),
195 param("reading", spec.license.reading_id.as_str()),
196 ],
197 },
198 SerialDeployerInner::MelodyAccompaniment(spec) => SerialDeployerSpec {
199 kind: SerialDeployerKind::MelodyAccompanimentDistribution,
200 label: spec.event_prefix.clone(),
201 expected_fact: "each block splits into melody lead and accompaniment residue"
202 .to_owned(),
203 parameters: vec![
204 param("row", spec.row_id.as_str()),
205 param("blocks", &spec.partition.block_count().to_string()),
206 param("reading", spec.license.reading_id.as_str()),
207 ],
208 },
209 SerialDeployerInner::AggregateRotation(spec) => SerialDeployerSpec {
210 kind: SerialDeployerKind::AggregateRotation,
211 label: spec.event_prefix.clone(),
212 expected_fact: "a rotated aggregate is reblocked without losing row coverage"
213 .to_owned(),
214 parameters: vec![
215 param("row", spec.row_id.as_str()),
216 param("rotation", &spec.rotation.to_string()),
217 param("reading", spec.license.reading_id.as_str()),
218 ],
219 },
220 SerialDeployerInner::SimultaneousForms(spec) => SerialDeployerSpec {
221 kind: SerialDeployerKind::SimultaneousForms,
222 label: spec.event_prefix.clone(),
223 expected_fact: "simultaneous form blocks preserve each form identity and alignment"
224 .to_owned(),
225 parameters: vec![
226 param(
227 "forms",
228 &spec
229 .row_ids
230 .iter()
231 .map(RowInstanceId::as_str)
232 .collect::<Vec<_>>()
233 .join(","),
234 ),
235 param("block-size", &spec.block_size.to_string()),
236 param("reading", spec.license.reading_id.as_str()),
237 ],
238 },
239 }
240 }
241
242 pub(crate) fn apply(
243 &self,
244 rows: &BTreeMap<RowInstanceId, RowForm>,
245 builder: &mut PlanBuilder,
246 ) -> Result<(), SerialDeployError> {
247 match &self.inner {
248 SerialDeployerInner::Horizontal(spec) => spec.apply(rows, builder),
249 SerialDeployerInner::Motivic(spec) => spec.apply(rows, builder),
250 SerialDeployerInner::Vertical(spec) => spec.apply(rows, builder),
251 SerialDeployerInner::Interlocking(spec) => spec.apply(rows, builder),
252 SerialDeployerInner::MelodyAccompaniment(spec) => spec.apply(rows, builder),
253 SerialDeployerInner::AggregateRotation(spec) => spec.apply(rows, builder),
254 SerialDeployerInner::SimultaneousForms(spec) => spec.apply(rows, builder),
255 }
256 }
257}
258
259pub(crate) fn param(name: &str, value: &str) -> SerialDeployerParameter {
260 SerialDeployerParameter {
261 name: name.to_owned(),
262 value: value.to_owned(),
263 }
264}
265
266#[derive(Clone, Debug)]
267pub(crate) struct PlanBuilder {
268 pub(crate) events: BTreeMap<SerialEventId, PlannedSerialEvent>,
269 pub(crate) precedence: Vec<(SerialEventId, SerialEventId)>,
270}
271
272impl PlanBuilder {
273 pub(crate) fn add_event(
274 &mut self,
275 deployer: &str,
276 event: PlannedSerialEvent,
277 ) -> Result<(), SerialDeployError> {
278 if self.events.contains_key(&event.id) {
279 return Err(SerialDeployError::DuplicateEventId {
280 deployer: deployer.to_owned(),
281 event_id: event.id,
282 });
283 }
284 self.events.insert(event.id.clone(), event);
285 Ok(())
286 }
287
288 pub(crate) fn add_precedence(&mut self, before: &SerialEventId, after: &SerialEventId) {
289 self.precedence.push((before.clone(), after.clone()));
290 }
291}
292
293#[derive(Clone)]
295pub struct TechniquePlan {
296 id: String,
297 rules: Vec<Arc<dyn PracticeRule>>,
298 deployers: Vec<SerialDeployer>,
299}
300
301impl TechniquePlan {
302 pub fn builder(id: impl Into<String>) -> Result<TechniquePlanBuilder, SerialDeployError> {
304 Ok(TechniquePlanBuilder {
305 id: validate_technique_id(id)?,
306 rules: Vec::new(),
307 deployers: Vec::new(),
308 })
309 }
310
311 pub fn id(&self) -> &str {
313 &self.id
314 }
315
316 pub fn rule_specs(&self) -> Vec<crate::PracticeRuleSpec> {
318 self.practice().rule_specs()
319 }
320
321 pub fn deployer_specs(&self) -> Vec<SerialDeployerSpec> {
323 self.deployers.iter().map(SerialDeployer::spec).collect()
324 }
325
326 pub fn practice(&self) -> SerialPractice {
328 SerialPractice::new(
329 crate::PracticeId::new(self.id.clone()).expect("validated technique id"),
330 self.rules.clone(),
331 )
332 }
333
334 pub fn deploy(
336 &self,
337 rows: BTreeMap<RowInstanceId, RowForm>,
338 ) -> Result<SerialPlan, SerialDeployError> {
339 let mut builder = PlanBuilder {
340 events: BTreeMap::new(),
341 precedence: Vec::new(),
342 };
343 for deployer in &self.deployers {
344 deployer.apply(&rows, &mut builder)?;
345 }
346 SerialPlan::try_new(rows, builder.events, builder.precedence)
347 .map_err(|error| SerialDeployError::Plan(error.to_string()))
348 }
349}
350
351pub struct TechniquePlanBuilder {
353 id: String,
354 rules: Vec<Arc<dyn PracticeRule>>,
355 deployers: Vec<SerialDeployer>,
356}
357
358impl TechniquePlanBuilder {
359 pub fn rule(mut self, rule: Arc<dyn PracticeRule>) -> Self {
361 self.rules.push(rule);
362 self
363 }
364
365 pub fn deployer(mut self, deployer: SerialDeployer) -> Self {
367 self.deployers.push(deployer);
368 self
369 }
370
371 pub fn build(self) -> Result<TechniquePlan, SerialDeployError> {
373 if self.deployers.is_empty() {
374 return Err(SerialDeployError::EmptyTechnique(self.id));
375 }
376 Ok(TechniquePlan {
377 id: self.id,
378 rules: self.rules,
379 deployers: self.deployers,
380 })
381 }
382}
383
384pub fn strict_aggregate() -> Arc<dyn PracticeRule> {
386 Arc::new(BuiltInPracticeRule::aggregate(
387 PracticeRuleId::new("rule/aggregate").expect("static rule id"),
388 ))
389}
390
391pub(crate) fn require_row(
392 deployer: &str,
393 rows: &BTreeMap<RowInstanceId, RowForm>,
394 row_id: &RowInstanceId,
395) -> Result<(), SerialDeployError> {
396 if rows.contains_key(row_id) {
397 Ok(())
398 } else {
399 Err(SerialDeployError::UnknownRow {
400 deployer: deployer.to_owned(),
401 row_id: row_id.clone(),
402 })
403 }
404}
405
406pub(crate) fn require_voices(
407 deployer: &str,
408 expected: usize,
409 actual: usize,
410) -> Result<(), SerialDeployError> {
411 if expected == actual {
412 Ok(())
413 } else {
414 Err(SerialDeployError::VoiceCountMismatch {
415 deployer: deployer.to_owned(),
416 expected,
417 actual,
418 })
419 }
420}
421
422pub(crate) fn structural_event(
423 event_id: SerialEventId,
424 ordinals: &[u8],
425 row_id: RowInstanceId,
426 voice: VoiceId,
427 rationale: String,
428 license: StructuralLicense,
429 placement: EventPlacement,
430) -> PlannedSerialEvent {
431 PlannedSerialEvent {
432 id: event_id,
433 ordinals: ordinals
434 .iter()
435 .copied()
436 .map(|ordinal| OrdinalRef::new(row_id.clone(), usize::from(ordinal)))
437 .collect(),
438 role: SerialRole::Structural,
439 origin: SerialOrigin::Structural { rationale },
440 voice,
441 placement,
442 parents: Vec::new(),
443 licenses: vec![license],
444 }
445}