1use std::collections::HashMap;
9
10use crate::ast::{GroupDef, InstrumentDef, PatternDef, PitchRoot, Program, ScaleMode, SourceLine};
11use crate::error::CompileError;
12use crate::parser::parse_program;
13use crate::piece::{self, Piece};
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum Delta {
18 Add(String),
20 Modify(String),
22 Remove(String),
24 Mute(String),
26 Unmute(String),
28 AddInstrument(String),
30 ModifyInstrument(String),
32 RemoveInstrument(String),
34 AddGroup(String),
36 ModifyGroup(String),
38 RemoveGroup(String),
40 Load(String),
46 Unload(String),
48}
49
50#[derive(Debug, Clone)]
52pub struct EvalResult {
53 pub errors: Vec<CompileError>,
55 pub deltas: Vec<Delta>,
57 pub patterns_active: usize,
59 pub patterns_muted: usize,
60 pub piece: Option<Piece>,
67}
68
69fn group_error(line: usize, message: String) -> CompileError {
70 CompileError {
71 kind: crate::error::CompileErrorKind::ParseError,
72 location: crate::error::SourceLocation {
73 line,
74 column: 1,
75 file: None,
76 },
77 message,
78 suggestion: None,
79 }
80}
81
82#[derive(Clone)]
84pub struct Session {
85 pub bpm: u32,
87 pub sig: (u8, u8),
89 pub phrase: u32,
92 pub seed: u64,
94 pub scale: Option<(PitchRoot, ScaleMode)>,
96 patterns: HashMap<String, PatternDef>,
98 definitions: HashMap<String, InstrumentDef>,
100 groups: HashMap<String, GroupDef>,
102 loads: Vec<String>,
105 pending: Vec<Delta>,
107 last_program: Option<Program>,
109 piece: Option<Piece>,
111}
112
113impl Session {
114 pub fn new() -> Self {
115 Self {
116 bpm: 120,
117 sig: (4, 4),
118 phrase: 1,
119 seed: 0,
120 scale: None,
121 patterns: HashMap::new(),
122 definitions: HashMap::new(),
123 groups: HashMap::new(),
124 loads: Vec::new(),
125 pending: Vec::new(),
126 last_program: None,
127 piece: None,
128 }
129 }
130
131 pub fn piece(&self) -> Option<&Piece> {
139 self.piece.as_ref()
140 }
141
142 pub fn is_piece(&self) -> bool {
144 self.piece.is_some()
145 }
146
147 pub fn evaluate(&mut self, source: &str) -> EvalResult {
150 let (program, errors) = parse_program(source);
151
152 if !errors.is_empty() {
155 return EvalResult {
156 errors,
157 deltas: Vec::new(),
158 patterns_active: self.patterns.values().filter(|p| !p.muted).count(),
159 patterns_muted: self.patterns.values().filter(|p| p.muted).count(),
160 piece: self.piece.clone(),
161 };
162 }
163
164 let is_piece = piece::is_piece(&program);
168
169 let mut new_bpm = self.bpm;
171 let mut new_sig = self.sig;
172 let mut new_phrase = self.phrase;
173 let mut new_seed = self.seed;
174 let mut new_scale = self.scale;
175 let mut new_patterns: HashMap<String, PatternDef> = HashMap::new();
176 let mut new_definitions: HashMap<String, InstrumentDef> = HashMap::new();
177 let mut new_groups: HashMap<String, GroupDef> = HashMap::new();
178 let mut new_loads: Vec<String> = Vec::new();
179 let mut open_group: Option<(String, bool)> = None;
183 let mut group_errors: Vec<CompileError> = Vec::new();
184
185 for (line_idx, line) in program.lines.iter().enumerate() {
186 match line {
187 SourceLine::Bpm(val) => new_bpm = *val,
188 SourceLine::Sig(num, den) => new_sig = (*num, *den),
189 SourceLine::Phrase(cycles) => new_phrase = *cycles,
190 SourceLine::Scale(root, mode) => new_scale = Some((*root, *mode)),
191 SourceLine::Load(path) => new_loads.push(path.clone()),
192 SourceLine::Seed(value) => new_seed = *value,
193 SourceLine::Meta(_, _) => {}
194 directive @ (SourceLine::Arrange(_) | SourceLine::Tail(_)) if !is_piece => {
195 group_errors.push(group_error(
196 line_idx + 1,
197 format!(
198 "{}: a live buffer has no sections to arrange or tail — add a \
199 'section' block to make this a piece (§8.1)",
200 match directive {
201 SourceLine::Arrange(_) => "arrange",
202 _ => "tail",
203 }
204 ),
205 ));
206 }
207 SourceLine::Pattern(def) => {
208 new_patterns.insert(def.name.clone(), def.clone());
209 }
210 SourceLine::Def(definition) => {
211 new_definitions.insert(definition.name.clone(), (**definition).clone());
212 }
213 SourceLine::GroupStart { muted, name } => {
214 if !is_piece && new_groups.contains_key(name) {
218 group_errors.push(group_error(
219 line_idx + 1,
220 format!("group '{name}' is declared twice"),
221 ));
222 }
223 open_group = Some((name.clone(), *muted));
224 }
225 SourceLine::GroupEnd(transforms) => {
226 if let Some((name, muted)) = open_group.take() {
227 new_groups.insert(
228 name.clone(),
229 GroupDef {
230 muted,
231 name,
232 transforms: transforms.clone(),
233 },
234 );
235 }
236 }
237 _ => {}
238 }
239 }
240 for name in new_groups.keys().filter(|_| !is_piece) {
244 if new_patterns.contains_key(name) {
245 group_errors.push(group_error(
246 1,
247 format!("'{name}' names both a group and a pattern; pick two names"),
248 ));
249 }
250 }
251 if !group_errors.is_empty() {
252 return EvalResult {
253 errors: group_errors,
254 deltas: Vec::new(),
255 patterns_active: self.patterns.values().filter(|p| !p.muted).count(),
256 patterns_muted: self.patterns.values().filter(|p| p.muted).count(),
257 piece: self.piece.clone(),
258 };
259 }
260
261 let mut deltas = self.diff(&new_patterns);
263 deltas.extend(self.diff_definitions(&new_definitions));
264 deltas.extend(self.diff_groups(&new_groups));
265 deltas.extend(self.diff_loads(&new_loads));
266
267 let mut piece = None;
271 if is_piece {
272 let (resolved, piece_errors) = piece::resolve(&program, (new_bpm, new_sig, new_scale));
273 if !piece_errors.is_empty() {
274 return EvalResult {
275 errors: piece_errors,
276 deltas: Vec::new(),
277 patterns_active: self.patterns.values().filter(|p| !p.muted).count(),
278 patterns_muted: self.patterns.values().filter(|p| p.muted).count(),
279 piece: self.piece.clone(),
280 };
281 }
282 piece = Some(resolved);
283 }
284
285 self.bpm = new_bpm;
287 self.sig = new_sig;
288 self.phrase = new_phrase;
289 self.seed = new_seed;
290 self.scale = new_scale;
291
292 self.pending = deltas.clone();
294 self.patterns = new_patterns;
295 self.definitions = new_definitions;
296 self.groups = new_groups;
297 self.loads = new_loads;
298 self.last_program = Some(program);
299 self.piece = piece.clone();
300
301 let patterns_active = self.patterns.values().filter(|p| !p.muted).count();
302 let patterns_muted = self.patterns.values().filter(|p| p.muted).count();
303
304 EvalResult {
305 errors,
306 deltas,
307 patterns_active,
308 patterns_muted,
309 piece,
310 }
311 }
312
313 fn diff(&self, new_patterns: &HashMap<String, PatternDef>) -> Vec<Delta> {
315 let mut deltas = Vec::new();
316
317 for (name, new_def) in new_patterns {
319 match self.patterns.get(name) {
320 None => deltas.push(Delta::Add(name.clone())),
321 Some(old_def) => {
322 if old_def.muted && !new_def.muted {
323 deltas.push(Delta::Unmute(name.clone()));
324 } else if !old_def.muted && new_def.muted {
325 deltas.push(Delta::Mute(name.clone()));
326 } else if old_def != new_def {
327 deltas.push(Delta::Modify(name.clone()));
328 }
329 }
330 }
331 }
332
333 for name in self.patterns.keys() {
335 if !new_patterns.contains_key(name) {
336 deltas.push(Delta::Remove(name.clone()));
337 }
338 }
339
340 deltas
341 }
342
343 fn diff_definitions(&self, new_definitions: &HashMap<String, InstrumentDef>) -> Vec<Delta> {
345 let mut deltas = Vec::new();
346 for (name, definition) in new_definitions {
347 match self.definitions.get(name) {
348 None => deltas.push(Delta::AddInstrument(name.clone())),
349 Some(previous) if previous != definition => {
350 deltas.push(Delta::ModifyInstrument(name.clone()));
351 }
352 Some(_) => {}
353 }
354 }
355 for name in self.definitions.keys() {
356 if !new_definitions.contains_key(name) {
357 deltas.push(Delta::RemoveInstrument(name.clone()));
358 }
359 }
360 deltas
361 }
362
363 fn diff_groups(&self, new_groups: &HashMap<String, GroupDef>) -> Vec<Delta> {
364 let mut deltas = Vec::new();
365 for (name, group) in new_groups {
366 match self.groups.get(name) {
367 None => deltas.push(Delta::AddGroup(name.clone())),
368 Some(previous) if previous != group => {
369 deltas.push(Delta::ModifyGroup(name.clone()));
370 }
371 Some(_) => {}
372 }
373 }
374 for name in self.groups.keys() {
375 if !new_groups.contains_key(name) {
376 deltas.push(Delta::RemoveGroup(name.clone()));
377 }
378 }
379 deltas
380 }
381
382 fn diff_loads(&self, new_loads: &[String]) -> Vec<Delta> {
388 let mut deltas = Vec::new();
389 for path in new_loads {
390 if !self.loads.contains(path) {
391 deltas.push(Delta::Load(path.clone()));
392 }
393 }
394 for path in self.loads.iter() {
395 if !new_loads.contains(path) {
396 deltas.push(Delta::Unload(path.clone()));
397 }
398 }
399 if deltas.is_empty() && self.loads != new_loads {
402 deltas.extend(new_loads.iter().cloned().map(Delta::Load));
403 }
404 deltas
405 }
406
407 pub fn loads(&self) -> &[String] {
409 &self.loads
410 }
411
412 pub fn definitions(&self) -> &HashMap<String, InstrumentDef> {
414 &self.definitions
415 }
416
417 pub fn groups(&self) -> &HashMap<String, GroupDef> {
419 &self.groups
420 }
421
422 pub fn pending_deltas(&self) -> &[Delta] {
424 &self.pending
425 }
426
427 pub fn apply_pending(&mut self) {
429 self.pending.clear();
430 }
431
432 pub fn active_patterns(&self) -> Vec<&PatternDef> {
434 self.patterns.values().filter(|p| !p.muted).collect()
435 }
436
437 pub fn all_patterns(&self) -> &HashMap<String, PatternDef> {
439 &self.patterns
440 }
441}
442
443impl Default for Session {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::ast::{Accidental, NoteLetter, PitchRoot, ScaleMode};
457
458 #[test]
459 fn test_groups_are_tracked_and_diffed() {
460 let mut session = Session::new();
461 let source = "group drums {\nkick kick \"x ~\"\n} | lpf 800";
462 let result = session.evaluate(source);
463 assert!(result.errors.is_empty(), "{:?}", result.errors);
464 assert!(result.deltas.contains(&Delta::AddGroup("drums".into())));
465 let group = &session.groups()["drums"];
466 assert!(!group.muted);
467 assert_eq!(group.transforms.len(), 1);
468
469 let result = session.evaluate("group drums {\nkick kick \"x ~\"\n} | lpf 400");
471 assert!(result.deltas.contains(&Delta::ModifyGroup("drums".into())));
472 let result = session.evaluate("kick kick \"x ~\"");
473 assert!(result.deltas.contains(&Delta::RemoveGroup("drums".into())));
474 assert!(session.groups().is_empty());
475 }
476
477 #[test]
478 fn test_group_name_collisions_are_rejected() {
479 let mut session = Session::new();
480 let twice = "group a {\nkick kick \"x\"\n}\ngroup a {\nsn snare \"x\"\n}";
481 assert!(!session.evaluate(twice).errors.is_empty());
482 let shadowing = "group kick {\nkick kick \"x\"\n}";
483 assert!(!session.evaluate(shadowing).errors.is_empty());
484 assert!(session.groups().is_empty());
486 }
487
488 #[test]
489 fn test_loads_are_tracked_and_diffed() {
490 let mut session = Session::new();
491 let result = session.evaluate("load \"pads.trbl\"\nkick kick \"x ~\"");
492 assert!(result.errors.is_empty(), "{:?}", result.errors);
493 assert!(result.deltas.contains(&Delta::Load("pads.trbl".into())));
494 assert_eq!(session.loads(), ["pads.trbl"]);
495
496 let result = session.evaluate("load \"pads.trbl\"\nkick kick \"x ~\"");
498 assert!(result.deltas.is_empty(), "{:?}", result.deltas);
499
500 let result = session.evaluate("load \"pads.trbl\"\nload \"keys.trbl\"\nkick kick \"x ~\"");
502 assert_eq!(result.deltas, vec![Delta::Load("keys.trbl".into())]);
503
504 let result = session.evaluate("load \"keys.trbl\"\nload \"pads.trbl\"\nkick kick \"x ~\"");
506 assert_eq!(
507 result.deltas,
508 vec![
509 Delta::Load("keys.trbl".into()),
510 Delta::Load("pads.trbl".into())
511 ]
512 );
513
514 let result = session.evaluate("kick kick \"x ~\"");
515 assert!(result.deltas.contains(&Delta::Unload("pads.trbl".into())));
516 assert!(result.deltas.contains(&Delta::Unload("keys.trbl".into())));
517 assert!(session.loads().is_empty());
518 }
519
520 #[test]
521 fn test_session_initial_state() {
522 let session = Session::new();
523 assert_eq!(session.bpm, 120);
524 assert_eq!(session.sig, (4, 4));
525 assert!(session.patterns.is_empty());
526 }
527
528 #[test]
529 fn test_evaluate_directives() {
530 let mut session = Session::new();
531 let result = session.evaluate("bpm 140\nsig 3/4");
532 assert!(result.errors.is_empty());
533 assert_eq!(session.bpm, 140);
534 assert_eq!(session.sig, (3, 4));
535 }
536
537 #[test]
538 fn test_evaluate_adds_patterns() {
539 let mut session = Session::new();
540 let result = session.evaluate("kick kick \"x ~ x ~\"\nbass sine \"c2 eb2\"");
541 assert!(result.errors.is_empty());
542 assert_eq!(result.deltas.len(), 2);
543 assert!(result.deltas.iter().all(|d| matches!(d, Delta::Add(_))));
544 assert_eq!(result.patterns_active, 2);
545 }
546
547 #[test]
548 fn test_evaluate_detects_removal() {
549 let mut session = Session::new();
550 session.evaluate("kick kick \"x ~ x ~\"\nbass sine \"c2 eb2\"");
551
552 let result = session.evaluate("kick kick \"x ~ x ~\"");
553 assert!(
554 result
555 .deltas
556 .iter()
557 .any(|d| *d == Delta::Remove("bass".into()))
558 );
559 }
560
561 #[test]
562 fn test_evaluate_detects_modify() {
563 let mut session = Session::new();
564 session.evaluate("kick kick \"x ~ x ~\"");
565
566 let result = session.evaluate("kick kick \"x x x x\"");
567 assert!(
568 result
569 .deltas
570 .iter()
571 .any(|d| *d == Delta::Modify("kick".into()))
572 );
573 }
574
575 #[test]
576 fn test_evaluate_detects_mute() {
577 let mut session = Session::new();
578 session.evaluate("kick kick \"x ~ x ~\"");
579
580 let result = session.evaluate("; kick kick \"x ~ x ~\"");
581 assert!(
582 result
583 .deltas
584 .iter()
585 .any(|d| *d == Delta::Mute("kick".into()))
586 );
587 assert_eq!(result.patterns_muted, 1);
588 assert_eq!(result.patterns_active, 0);
589 }
590
591 #[test]
592 fn test_evaluate_detects_unmute() {
593 let mut session = Session::new();
594 session.evaluate("; kick kick \"x ~ x ~\"");
595
596 let result = session.evaluate("kick kick \"x ~ x ~\"");
597 assert!(
598 result
599 .deltas
600 .iter()
601 .any(|d| *d == Delta::Unmute("kick".into()))
602 );
603 }
604
605 #[test]
606 fn test_unchanged_no_delta() {
607 let mut session = Session::new();
608 session.evaluate("kick kick \"x ~ x ~\"");
609
610 let result = session.evaluate("kick kick \"x ~ x ~\"");
611 assert!(result.deltas.is_empty());
612 }
613
614 #[test]
615 fn test_error_recovery_preserves_valid() {
616 let mut session = Session::new();
617 session.evaluate("bpm 130\nkick kick \"x ~ x ~\"");
618 let result = session.evaluate("bpm 140\nthis is broken ???\nsnare snare \"x ~ x ~\"");
619 assert_eq!(result.errors.len(), 1);
620 assert_eq!(session.bpm, 130);
621 assert_eq!(result.patterns_active, 1);
622 assert!(session.all_patterns().contains_key("kick"));
623 assert!(!session.all_patterns().contains_key("snare"));
624 }
625
626 #[test]
627 fn test_evaluate_scale_directive() {
628 let mut session = Session::new();
629 let result = session.evaluate("scale C minor\nkick kick \"x ~ x ~\"");
630 assert!(result.errors.is_empty());
631 assert_eq!(
632 session.scale,
633 Some((
634 PitchRoot {
635 name: NoteLetter::C,
636 accidental: Accidental::Natural,
637 },
638 ScaleMode::Minor,
639 ))
640 );
641 }
642
643 #[test]
644 fn test_full_example() {
645 let mut session = Session::new();
646 let source = "\
647-- techno loop
648bpm 128
649sig 4/4
650
651kick kick \"x ~ x ~\"
652snare snare \"~ x ~ x\"
653hats hihat \"x*8\"
654bass saw \"c2 _ eb2 _ g1 _ f2 _\"
655lead piano \"c4 eb4 g4 bb4\" | slow 2
656; pad pad \"[c3,eb3,g3] ~ [f3,ab3,c4] ~\"";
657
658 let result = session.evaluate(source);
659 assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
660 assert_eq!(session.bpm, 128);
661 assert_eq!(session.sig, (4, 4));
662 assert_eq!(result.patterns_active, 5);
663 assert_eq!(result.patterns_muted, 1);
664 assert_eq!(result.deltas.len(), 6); }
666}