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