1mod instrument;
9mod lines;
10mod mini;
11mod statements;
12
13pub use instrument::parse_instrument_def;
14pub use lines::parse_line;
15pub use mini::parse_mini;
16pub use statements::{Statement, statements};
17
18use instrument::count_braces;
19
20use crate::ast::{Program, SourceLine};
21use crate::error::{CompileError, CompileErrorKind, SourceLocation};
22
23pub fn parse_program(source: &str) -> (Program, Vec<CompileError>) {
29 let mut lines = Vec::new();
30 let mut errors = Vec::new();
31 let mut block: Option<(usize, Vec<&str>)> = None;
33 let mut depth = 0usize;
34 let mut open_group: Option<(usize, String)> = None;
37 let mut open_section: Option<(usize, String)> = None;
40
41 let report = |errors: &mut Vec<CompileError>, line: usize, message: String| {
42 errors.push(CompileError {
43 kind: CompileErrorKind::ParseError,
44 location: SourceLocation {
45 line,
46 column: 1,
47 file: None,
48 },
49 message,
50 suggestion: None,
51 });
52 };
53
54 let statements = statements(source);
57 for statement in &statements {
58 let line_idx = statement.first_line;
59 let raw = statement.text.as_str();
60 if let Some((_, collected)) = &mut block {
61 depth += count_braces(raw, '{');
62 depth -= count_braces(raw, '}').min(depth);
63 collected.push(raw);
64 if depth == 0 {
65 let (start, collected) = block.take().expect("inside a block");
66 match parse_instrument_def(&collected) {
67 Ok(definition) => lines.push(SourceLine::Def(Box::new(definition))),
68 Err(message) => {
69 report(&mut errors, start + 1, message);
70 for line in collected {
72 lines.push(SourceLine::Comment(line.to_string()));
73 }
74 }
75 }
76 }
77 continue;
78 }
79
80 if raw.trim_start().starts_with("def ") || raw.trim() == "def" {
81 if let Some((_, group)) = &open_group {
82 report(
83 &mut errors,
84 line_idx + 1,
85 format!("def: an instrument definition cannot live inside group '{group}'"),
86 );
87 } else if let Some((_, section)) = &open_section {
88 report(
89 &mut errors,
90 line_idx + 1,
91 format!(
92 "def: an instrument definition cannot live inside section '{section}' \
93 — move it above the section"
94 ),
95 );
96 }
97 depth = count_braces(raw, '{');
98 depth -= count_braces(raw, '}').min(depth);
99 if depth == 0 {
100 match parse_instrument_def(&[raw]) {
102 Ok(definition) => lines.push(SourceLine::Def(Box::new(definition))),
103 Err(message) => {
104 report(&mut errors, line_idx + 1, message);
105 lines.push(SourceLine::Comment(raw.to_string()));
106 }
107 }
108 } else {
109 block = Some((line_idx, vec![raw]));
110 }
111 continue;
112 }
113
114 match parse_line(raw) {
115 Ok(source_line) => {
116 match source_line {
117 SourceLine::GroupStart { muted, name } => {
118 if let Some((_, outer)) = &open_group {
119 report(
120 &mut errors,
121 line_idx + 1,
122 format!("group: groups don't nest; '{outer}' is still open"),
123 );
124 }
125 open_group = Some((line_idx, name.clone()));
126 lines.push(SourceLine::GroupStart { muted, name });
127 }
128 SourceLine::GroupEnd(transforms) => {
131 if open_group.take().is_some() {
132 lines.push(SourceLine::GroupEnd(transforms));
133 } else if open_section.take().is_some() {
134 if !transforms.is_empty() {
135 report(
136 &mut errors,
137 line_idx + 1,
138 "section: '}' takes no filters — a section is a span of \
139 time, not a bus; put them on a group inside it"
140 .to_string(),
141 );
142 }
143 lines.push(SourceLine::SectionEnd);
144 } else {
145 report(
146 &mut errors,
147 line_idx + 1,
148 "'}' without an open group or section".to_string(),
149 );
150 lines.push(SourceLine::GroupEnd(transforms));
151 }
152 }
153 SourceLine::SectionStart {
154 muted,
155 name,
156 cycles,
157 } => {
158 if let Some((_, group)) = &open_group {
159 report(
160 &mut errors,
161 line_idx + 1,
162 format!(
163 "section: '{name}' cannot open inside group '{group}' — a \
164 group nests inside a section, not the other way round"
165 ),
166 );
167 } else if let Some((_, outer)) = &open_section {
168 report(
169 &mut errors,
170 line_idx + 1,
171 format!("section: sections don't nest; '{outer}' is still open"),
172 );
173 }
174 open_section = Some((line_idx, name.clone()));
175 lines.push(SourceLine::SectionStart {
176 muted,
177 name,
178 cycles,
179 });
180 }
181 SourceLine::SectionEnd => lines.push(SourceLine::SectionEnd),
182 SourceLine::Pattern(mut def) => {
183 def.group = open_group.as_ref().map(|(_, name)| name.clone());
184 def.section = open_section.as_ref().map(|(_, name)| name.clone());
185 if def.span.is_some() && def.section.is_none() {
186 report(
187 &mut errors,
188 line_idx + 1,
189 format!(
190 "'{}': a span needs a section — its cycles are counted \
191 from the section's start (§8.3)",
192 def.name
193 ),
194 );
195 def.span = None;
196 }
197 lines.push(SourceLine::Pattern(def));
198 }
199 directive @ (SourceLine::Bpm(_)
202 | SourceLine::Sig(_, _)
203 | SourceLine::Phrase(_)
204 | SourceLine::Scale(_, _)
205 | SourceLine::Load(_)
206 | SourceLine::Include(_)
207 | SourceLine::Arrange(_)
208 | SourceLine::Tail(_)
209 | SourceLine::Seed(_))
210 if open_group.is_some() =>
211 {
212 report(
213 &mut errors,
214 line_idx + 1,
215 "group: directives cannot live inside a group — move this above it"
216 .to_string(),
217 );
218 lines.push(directive);
219 }
220 directive @ (SourceLine::Phrase(_)
225 | SourceLine::Load(_)
226 | SourceLine::Include(_)
227 | SourceLine::Arrange(_)
228 | SourceLine::Tail(_)
229 | SourceLine::Seed(_))
230 if open_section.is_some() =>
231 {
232 report(
233 &mut errors,
234 line_idx + 1,
235 format!(
236 "section: '{}' configures the whole piece — move it above the \
237 section (a section scopes only bpm, sig and scale)",
238 directive_keyword(&directive)
239 ),
240 );
241 lines.push(directive);
242 }
243 other => lines.push(other),
244 }
245 }
246 Err(msg) => {
247 report(&mut errors, line_idx + 1, msg);
248 lines.push(SourceLine::Comment(raw.to_string()));
250 }
251 }
252 }
253
254 if let Some((start, name)) = open_group {
255 report(
256 &mut errors,
257 start + 1,
258 format!("group: '{name}' is never closed with '}}'"),
259 );
260 }
261
262 if let Some((start, name)) = open_section {
263 report(
264 &mut errors,
265 start + 1,
266 format!("section: '{name}' is never closed with '}}'"),
267 );
268 }
269
270 if let Some((start, collected)) = block {
271 report(
272 &mut errors,
273 start + 1,
274 "def: the block is never closed with '}'".to_string(),
275 );
276 for line in collected {
277 lines.push(SourceLine::Comment(line.to_string()));
278 }
279 }
280
281 (Program { lines }, errors)
282}
283
284fn directive_keyword(line: &SourceLine) -> &'static str {
286 match line {
287 SourceLine::Bpm(_) => "bpm",
288 SourceLine::Sig(_, _) => "sig",
289 SourceLine::Phrase(_) => "phrase",
290 SourceLine::Scale(_, _) => "scale",
291 SourceLine::Load(_) => "load",
292 SourceLine::Include(_) => "include",
293 SourceLine::Arrange(_) => "arrange",
294 SourceLine::Tail(_) => "tail",
295 SourceLine::Seed(_) => "seed",
296 _ => "directive",
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::ast::*;
304
305 #[test]
306 fn test_group_block() {
307 let source = "\
308include kick
309include hihat
310
311group drums {
312 kick kick \"x ~ x ~\"
313 ; hat hihat \"x*8\"
314} | lpf 1800 | gain 0.9
315
316lead bass \"0 3 5 3\"";
317 let (prog, errs) = parse_program(source);
318 assert!(errs.is_empty(), "{errs:?}");
319 let patterns: Vec<&PatternDef> = prog
320 .lines
321 .iter()
322 .filter_map(|line| match line {
323 SourceLine::Pattern(def) => Some(def),
324 _ => None,
325 })
326 .collect();
327 assert_eq!(patterns[0].group.as_deref(), Some("drums"));
329 assert_eq!(patterns[1].group.as_deref(), Some("drums"));
330 assert!(patterns[1].muted, "member mute survives inside a group");
331 assert_eq!(patterns[2].group, None);
332 let end = prog
334 .lines
335 .iter()
336 .find_map(|line| match line {
337 SourceLine::GroupEnd(transforms) => Some(transforms),
338 _ => None,
339 })
340 .unwrap();
341 assert_eq!(end.len(), 2);
342 }
343
344 #[test]
345 fn test_muted_group_and_bare_close() {
346 let (prog, errs) = parse_program("; group drums {\nkick kick \"x\"\n}");
347 assert!(errs.is_empty(), "{errs:?}");
348 assert!(matches!(
349 &prog.lines[0],
350 SourceLine::GroupStart { muted: true, name } if name == "drums"
351 ));
352 assert!(matches!(&prog.lines[2], SourceLine::GroupEnd(t) if t.is_empty()));
353 }
354
355 #[test]
356 fn test_group_structural_errors() {
357 assert!(!parse_program("group drums {").1.is_empty());
359 assert!(!parse_program("}").1.is_empty());
360 assert!(
361 !parse_program("group a {\ngroup b {\n}\n}").1.is_empty(),
362 "groups must not nest"
363 );
364 assert!(!parse_program("group a {\nbpm 130\n}").1.is_empty());
365 assert!(
366 !parse_program("group a {\ndef x { tone sine }\n}")
367 .1
368 .is_empty(),
369 "defs must not live inside groups"
370 );
371 assert!(
373 !parse_program("group drums { kick kick \"x\" }")
374 .1
375 .is_empty()
376 );
377 }
378
379 #[test]
380 fn test_parse_empty_program() {
381 let (prog, errs) = parse_program("");
382 assert!(errs.is_empty());
383 assert_eq!(prog.lines.len(), 0);
385 }
386
387 #[test]
388 fn test_parse_full_example() {
389 let source = "\
390-- techno
391bpm 128
392sig 4/4
393
394kick kick \"x ~ x ~\"
395; snare snare \"~ x ~ x\"
396lead saw \"c4 eb4 g4\" | rev | slow 2";
397
398 let (prog, errs) = parse_program(source);
399 assert!(errs.is_empty(), "errors: {:?}", errs);
400 assert_eq!(prog.lines.len(), 7);
401 assert!(matches!(prog.lines[0], SourceLine::Comment(_)));
402 assert!(matches!(prog.lines[1], SourceLine::Bpm(128)));
403 assert!(matches!(prog.lines[2], SourceLine::Sig(4, 4)));
404 assert!(matches!(prog.lines[3], SourceLine::Blank));
405 if let SourceLine::Pattern(ref p) = prog.lines[4] {
407 assert_eq!(p.name, "kick");
408 assert_eq!(p.instrument, "kick");
409 assert!(!p.muted);
410 assert!(p.transforms.is_empty());
411 } else {
412 panic!("expected pattern, got {:?}", prog.lines[4]);
413 }
414 if let SourceLine::Pattern(ref p) = prog.lines[5] {
416 assert!(p.muted);
417 assert_eq!(p.name, "snare");
418 } else {
419 panic!("expected muted pattern, got {:?}", prog.lines[5]);
420 }
421 if let SourceLine::Pattern(ref p) = prog.lines[6] {
423 assert_eq!(p.name, "lead");
424 assert_eq!(p.transforms.len(), 2);
425 assert_eq!(p.transforms[0], Transform::Rev);
426 assert_eq!(p.transforms[1], Transform::Slow(Ramp::fixed(2.0)));
427 } else {
428 panic!("expected pattern, got {:?}", prog.lines[6]);
429 }
430 }
431
432 #[test]
433 fn test_error_recovery() {
434 let source = "\
435bpm 128
436this is garbage ???
437kick kick \"x ~ x ~\"";
438 let (prog, errs) = parse_program(source);
439 assert_eq!(errs.len(), 1);
440 assert_eq!(errs[0].location.line, 2);
441 assert!(matches!(prog.lines[0], SourceLine::Bpm(128)));
443 assert!(matches!(prog.lines[1], SourceLine::Comment(_)));
445 assert!(matches!(prog.lines[2], SourceLine::Pattern(_)));
447 }
448}