devalang_wasm/language/syntax/parser/driver/statements/
extended.rs

1use super::super::duration::parse_duration_token;
2use crate::language::syntax::ast::{DurationValue, Statement, StatementKind, Value};
3use anyhow::{Result, anyhow};
4
5/// Parse fade statement: fade <in|out> <entity>.<property> during <duration>
6/// Example: fade in synth.gain during 2 bars
7pub fn parse_fade(line: &str, line_number: usize) -> Result<Statement> {
8    let trimmed = line.trim_start();
9
10    // Skip 'fade' keyword
11    let rest = if trimmed.starts_with("fade ") {
12        &trimmed[5..]
13    } else {
14        return Err(anyhow!("Invalid fade statement"));
15    };
16
17    // Parse: fade <in|out> <entity>.<property> during <duration>
18    let parts: Vec<&str> = rest.split_whitespace().collect();
19    if parts.len() < 5 {
20        return Err(anyhow!(
21            "fade requires format: fade <in|out> <entity>.<property> during <duration>"
22        ));
23    }
24
25    let direction = parts[0].to_string();
26    if direction != "in" && direction != "out" {
27        return Err(anyhow!(
28            "fade direction must be 'in' or 'out', got '{}'",
29            direction
30        ));
31    }
32
33    let target = parts[1].to_string();
34    if !target.contains('.') {
35        return Err(anyhow!(
36            "fade target must be in format entity.property, got '{}'",
37            target
38        ));
39    }
40
41    if parts[2] != "during" {
42        return Err(anyhow!("Expected 'during' keyword in fade statement"));
43    }
44
45    // Parse duration (everything after 'over')
46    let duration_str = parts[3..].join(" ");
47    let duration = parse_duration_token(&duration_str)?;
48
49    Ok(Statement::new(
50        StatementKind::Fade {
51            direction,
52            target,
53            duration,
54        },
55        Value::Null,
56        0,
57        line_number,
58        1,
59    ))
60}
61
62/// Parse section statement: section <name> [during <duration>]:
63/// Example: section intro during 8 bars:
64/// Example: section intro:  (duration auto-calculated from body)
65pub fn parse_section(
66    parts: impl Iterator<Item = impl AsRef<str>>,
67    line_number: usize,
68) -> Result<Statement> {
69    let parts_vec: Vec<String> = parts.map(|p| p.as_ref().to_string()).collect();
70
71    if parts_vec.is_empty() {
72        return Err(anyhow!("section requires a name"));
73    }
74
75    let name = parts_vec[0].trim_end_matches(':').to_string();
76
77    // Check if "during" keyword is present (optional)
78    let duration = if parts_vec.len() > 1 && parts_vec[1] == "during" {
79        // "during" keyword found
80        if parts_vec.len() > 2 {
81            // Parse duration (everything after 'during')
82            let duration_str = parts_vec[2..].join(" ").trim_end_matches(':').to_string();
83            Some(parse_duration_token(&duration_str)?)
84        } else {
85            return Err(anyhow!("section 'during' requires a duration value"));
86        }
87    } else {
88        // No "during" keyword, duration will be auto-calculated
89        None
90    };
91
92    Ok(Statement::new(
93        StatementKind::Section {
94            name,
95            duration,
96            body: Vec::new(), // Will be filled during indentation parsing
97        },
98        Value::Null,
99        0,
100        line_number,
101        1,
102    ))
103}
104
105/// Parse timeline statement: timeline [section1 during 4 bars, section2 during 8 beats, ...]
106/// Example: timeline [intro during 8 bars, verse during 16 beats, chorus]
107pub fn parse_timeline(line: &str, line_number: usize) -> Result<Statement> {
108    let trimmed = line.trim_start();
109
110    // Skip 'timeline' keyword
111    let rest = if trimmed.starts_with("timeline ") {
112        &trimmed[9..]
113    } else {
114        return Err(anyhow!("Invalid timeline statement"));
115    };
116
117    // Parse: timeline [item1, item2, ...]
118    if !rest.starts_with('[') || !rest.ends_with(']') {
119        return Err(anyhow!(
120            "timeline requires format: timeline [section_name, ...]"
121        ));
122    }
123
124    let inner = &rest[1..rest.len() - 1];
125    let mut sections = Vec::new();
126
127    // Split by comma and parse each section entry
128    for entry in inner.split(',') {
129        let entry = entry.trim();
130
131        // Check if it has "during"
132        if let Some(during_idx) = entry.find(" during ") {
133            let section_name = entry[..during_idx].trim().to_string();
134            let duration_str = entry[during_idx + 8..].trim();
135
136            let duration = parse_duration_token(duration_str)?;
137            sections.push((section_name, Some(duration)));
138        } else {
139            // Just section name, no duration override
140            sections.push((entry.to_string(), None));
141        }
142    }
143
144    if sections.is_empty() {
145        return Err(anyhow!("timeline must have at least one section"));
146    }
147
148    Ok(Statement::new(
149        StatementKind::Timeline { sections },
150        Value::Null,
151        0,
152        line_number,
153        1,
154    ))
155}
156
157/// Parse schedule statement: schedule:
158///   at <time>:
159///     <statements>
160///   at <time>:
161///     <statements>
162/// Example: schedule:
163///   at 0 bars:
164///     call myGroup
165///   at 5 bars:
166///     call myGroup2
167pub fn parse_schedule(_line: &str, line_number: usize) -> Result<Statement> {
168    // The actual parsing of schedule events happens during block indentation processing
169    // This just creates an empty Schedule that will be populated by parse_lines()
170    Ok(Statement::new(
171        StatementKind::Schedule {
172            events: Vec::new(), // Will be filled during indentation parsing
173        },
174        Value::Null,
175        0,
176        line_number,
177        1,
178    ))
179}
180
181/// Parse stop statement: stop <target> / stop all [after <duration>]
182/// Aliases: silence, mute
183/// Examples:
184///   - stop all
185///   - stop all after 2 bars
186///   - stop synth.gain
187///   - silence all
188///   - mute myEntity
189pub fn parse_stop(line: &str, line_number: usize) -> Result<Statement> {
190    let trimmed = line.trim_start();
191
192    // Handle aliases: "silence" and "mute"
193    let rest = if trimmed.starts_with("stop ") {
194        &trimmed[5..]
195    } else if trimmed.starts_with("silence") {
196        // "silence" is an alias for "stop all"
197        return Ok(Statement::new(
198            StatementKind::Stop {
199                target: None,
200                after: None,
201            },
202            Value::Null,
203            0,
204            line_number,
205            1,
206        ));
207    } else if trimmed.starts_with("mute ") {
208        &trimmed[5..]
209    } else {
210        return Err(anyhow!("Invalid stop statement"));
211    };
212
213    let parts: Vec<&str> = rest.split_whitespace().collect();
214
215    if parts.is_empty() {
216        return Err(anyhow!(
217            "stop requires format: stop <entity|all> [after <duration>]"
218        ));
219    }
220
221    // Check if it's "stop all" or "stop <entity>"
222    if parts[0] == "all" {
223        // Parse optional "after" clause
224        let after = if parts.len() > 2 && parts[1] == "after" {
225            let duration_str = parts[2..].join(" ");
226            Some(parse_duration_token(&duration_str)?)
227        } else if parts.len() > 1 {
228            return Err(anyhow!(
229                "Invalid stop all syntax. Expected: stop all [after <duration>]"
230            ));
231        } else {
232            None
233        };
234
235        Ok(Statement::new(
236            StatementKind::Stop {
237                target: None,
238                after,
239            },
240            Value::Null,
241            0,
242            line_number,
243            1,
244        ))
245    } else {
246        // stop <entity> format
247        let target = parts[0].to_string();
248
249        // Check for optional "after" clause
250        let after = if parts.len() > 2 && parts[1] == "after" {
251            let duration_str = parts[2..].join(" ");
252            Some(parse_duration_token(&duration_str)?)
253        } else if parts.len() > 1 {
254            return Err(anyhow!(
255                "Invalid stop syntax. Expected: stop <entity> [after <duration>]"
256            ));
257        } else {
258            None
259        };
260
261        Ok(Statement::new(
262            StatementKind::Stop {
263                target: Some(target),
264                after,
265            },
266            Value::Null,
267            0,
268            line_number,
269            1,
270        ))
271    }
272}
273
274/// Parse a schedule event header: at <duration> [and during <optional_duration>]:
275/// Returns the tuple (start_time, optional_duration) if valid
276/// Examples:
277///   - at 1 bars:
278///   - at 1 bars and during 2 bars:
279///   - at 0 bars and during 4 bars:
280pub fn parse_schedule_event_header(line: &str) -> Result<(DurationValue, Option<DurationValue>)> {
281    let trimmed = line.trim_start();
282
283    if !trimmed.starts_with("at ") {
284        return Err(anyhow!("Schedule event must start with 'at'"));
285    }
286
287    let rest = &trimmed[3..];
288    let rest = rest.trim_end_matches(':').trim();
289
290    // Check if there's "and during" in the line
291    if let Some(and_during_idx) = rest.find(" and during ") {
292        // Split into start time and optional duration
293        let start_str = rest[..and_during_idx].trim();
294        let duration_str = rest[and_during_idx + 12..].trim(); // 12 = len(" and during ")
295
296        let start_time = parse_duration_token(start_str)?;
297        let optional_duration = parse_duration_token(duration_str)?;
298
299        Ok((start_time, Some(optional_duration)))
300    } else {
301        // No "and during" clause - only start time
302        let start_time = parse_duration_token(rest)?;
303        Ok((start_time, None))
304    }
305}