devalang_wasm/engine/special_vars/
mod.rs

1/// Special variables system for Devalang
2/// Provides runtime-computed variables like $beat, $time, $random, etc.
3use crate::language::syntax::ast::Value;
4use std::collections::HashMap;
5
6/// Special variable prefix
7pub const SPECIAL_VAR_PREFIX: char = '$';
8
9/// Special variable categories
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SpecialVarCategory {
12    Time,     // $time, $beat, $bar
13    Random,   // $random.noise, $random.float, $random.int
14    Music,    // $bpm, $tempo, $duration
15    Position, // $position, $progress
16    Midi,     // $midi.note, $midi.velocity
17    System,   // $sampleRate, $channels
18}
19
20/// Special variable context - holds runtime state
21#[derive(Debug, Clone)]
22pub struct SpecialVarContext {
23    pub current_time: f32,   // Current playback time in seconds
24    pub current_beat: f32,   // Current beat position
25    pub current_bar: f32,    // Current bar position
26    pub bpm: f32,            // Current BPM
27    pub duration: f32,       // Beat duration in seconds
28    pub sample_rate: u32,    // Sample rate
29    pub channels: usize,     // Number of channels
30    pub position: f32,       // Normalized position (0.0-1.0)
31    pub total_duration: f32, // Total duration in seconds
32}
33
34impl Default for SpecialVarContext {
35    fn default() -> Self {
36        Self {
37            current_time: 0.0,
38            current_beat: 0.0,
39            current_bar: 0.0,
40            bpm: 120.0,
41            duration: 0.5,
42            sample_rate: 44100,
43            channels: 2,
44            position: 0.0,
45            total_duration: 0.0,
46        }
47    }
48}
49
50impl SpecialVarContext {
51    pub fn new(bpm: f32, sample_rate: u32) -> Self {
52        Self {
53            bpm,
54            duration: 60.0 / bpm,
55            sample_rate,
56            ..Default::default()
57        }
58    }
59
60    /// Update time-based variables
61    pub fn update_time(&mut self, time: f32) {
62        self.current_time = time;
63        self.current_beat = time / self.duration;
64        self.current_bar = self.current_beat / 4.0;
65
66        if self.total_duration > 0.0 {
67            self.position = (time / self.total_duration).clamp(0.0, 1.0);
68        }
69    }
70
71    /// Update BPM
72    pub fn update_bpm(&mut self, bpm: f32) {
73        self.bpm = bpm;
74        self.duration = 60.0 / bpm;
75    }
76}
77
78/// Check if a variable name is a special variable
79pub fn is_special_var(name: &str) -> bool {
80    name.starts_with(SPECIAL_VAR_PREFIX)
81}
82
83/// Resolve a special variable to its current value
84pub fn resolve_special_var(name: &str, context: &SpecialVarContext) -> Option<Value> {
85    if !is_special_var(name) {
86        return None;
87    }
88
89    match name {
90        // Time variables
91        "$time" => Some(Value::Number(context.current_time)),
92        "$beat" => Some(Value::Number(context.current_beat)),
93        "$bar" => Some(Value::Number(context.current_bar)),
94        "$currentTime" => Some(Value::Number(context.current_time)),
95        "$currentBeat" => Some(Value::Number(context.current_beat)),
96        "$currentBar" => Some(Value::Number(context.current_bar)),
97
98        // Music variables
99        "$bpm" => Some(Value::Number(context.bpm)),
100        "$tempo" => Some(Value::Number(context.bpm)),
101        "$duration" => Some(Value::Number(context.duration)),
102
103        // Position variables
104        "$position" => Some(Value::Number(context.position)),
105        "$progress" => Some(Value::Number(context.position)),
106
107        // System variables
108        "$sampleRate" => Some(Value::Number(context.sample_rate as f32)),
109        "$channels" => Some(Value::Number(context.channels as f32)),
110
111        // Random variables (computed on-demand)
112        #[cfg(any(feature = "cli", feature = "wasm"))]
113        "$random" | "$random.float" => Some(Value::Number(rand::random::<f32>())),
114        #[cfg(any(feature = "cli", feature = "wasm"))]
115        "$random.noise" => Some(Value::Number(rand::random::<f32>() * 2.0 - 1.0)), // -1.0 to 1.0
116        #[cfg(any(feature = "cli", feature = "wasm"))]
117        "$random.int" => Some(Value::Number((rand::random::<u32>() % 100) as f32)),
118        #[cfg(any(feature = "cli", feature = "wasm"))]
119        "$random.bool" => Some(Value::Boolean(rand::random::<bool>())),
120
121        // Nested random with ranges
122        #[cfg(any(feature = "cli", feature = "wasm"))]
123        _ if name.starts_with("$random.range(") => {
124            // Parse $random.range(min, max)
125            parse_random_range(name)
126        }
127
128        _ => None,
129    }
130}
131
132/// Parse $random.range(min, max) syntax
133#[cfg(any(feature = "cli", feature = "wasm"))]
134fn parse_random_range(name: &str) -> Option<Value> {
135    // Extract content between parentheses
136    let start = name.find('(')?;
137    let end = name.rfind(')')?;
138    let content = &name[start + 1..end];
139
140    // Split by comma
141    let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
142    if parts.len() != 2 {
143        return None;
144    }
145
146    // Parse min and max
147    let min: f32 = parts[0].parse().ok()?;
148    let max: f32 = parts[1].parse().ok()?;
149
150    // Generate random value in range
151    let value = min + rand::random::<f32>() * (max - min);
152    Some(Value::Number(value))
153}
154
155/// Get all available special variables as a map
156pub fn get_all_special_vars(context: &SpecialVarContext) -> HashMap<String, Value> {
157    let mut vars = HashMap::new();
158
159    // Time
160    vars.insert("$time".to_string(), Value::Number(context.current_time));
161    vars.insert("$beat".to_string(), Value::Number(context.current_beat));
162    vars.insert("$bar".to_string(), Value::Number(context.current_bar));
163    vars.insert(
164        "$currentTime".to_string(),
165        Value::Number(context.current_time),
166    );
167    vars.insert(
168        "$currentBeat".to_string(),
169        Value::Number(context.current_beat),
170    );
171    vars.insert(
172        "$currentBar".to_string(),
173        Value::Number(context.current_bar),
174    );
175
176    // Music
177    vars.insert("$bpm".to_string(), Value::Number(context.bpm));
178    vars.insert("$tempo".to_string(), Value::Number(context.bpm));
179    vars.insert("$duration".to_string(), Value::Number(context.duration));
180
181    // Position
182    vars.insert("$position".to_string(), Value::Number(context.position));
183    vars.insert("$progress".to_string(), Value::Number(context.position));
184
185    // System
186    vars.insert(
187        "$sampleRate".to_string(),
188        Value::Number(context.sample_rate as f32),
189    );
190    vars.insert(
191        "$channels".to_string(),
192        Value::Number(context.channels as f32),
193    );
194
195    vars
196}
197
198/// List all special variable categories with examples
199pub fn list_special_vars() -> HashMap<&'static str, Vec<(&'static str, &'static str)>> {
200    let mut categories = HashMap::new();
201
202    categories.insert(
203        "Time",
204        vec![
205            ("$time", "Current time in seconds"),
206            ("$beat", "Current beat position"),
207            ("$bar", "Current bar position"),
208            ("$currentTime", "Alias for $time"),
209            ("$currentBeat", "Alias for $beat"),
210            ("$currentBar", "Alias for $bar"),
211        ],
212    );
213
214    categories.insert(
215        "Music",
216        vec![
217            ("$bpm", "Current BPM"),
218            ("$tempo", "Alias for $bpm"),
219            ("$duration", "Beat duration in seconds"),
220        ],
221    );
222
223    categories.insert(
224        "Position",
225        vec![
226            ("$position", "Normalized position (0.0-1.0)"),
227            ("$progress", "Alias for $position"),
228        ],
229    );
230
231    categories.insert(
232        "Random",
233        vec![
234            ("$random", "Random float 0.0-1.0"),
235            ("$random.float", "Random float 0.0-1.0"),
236            ("$random.noise", "Random float -1.0 to 1.0"),
237            ("$random.int", "Random integer 0-99"),
238            ("$random.bool", "Random boolean"),
239            ("$random.range(min, max)", "Random float in range"),
240        ],
241    );
242
243    categories.insert(
244        "System",
245        vec![
246            ("$sampleRate", "Sample rate in Hz"),
247            ("$channels", "Number of audio channels"),
248        ],
249    );
250
251    categories
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_is_special_var() {
260        assert!(is_special_var("$time"));
261        assert!(is_special_var("$beat"));
262        assert!(is_special_var("$random"));
263        assert!(!is_special_var("time"));
264        assert!(!is_special_var("myVar"));
265    }
266
267    #[test]
268    fn test_resolve_time_vars() {
269        let mut context = SpecialVarContext::default();
270        context.update_time(2.0);
271
272        let time = resolve_special_var("$time", &context);
273        assert_eq!(time, Some(Value::Number(2.0)));
274
275        let beat = resolve_special_var("$beat", &context);
276        assert!(matches!(beat, Some(Value::Number(_))));
277    }
278
279    #[test]
280    fn test_resolve_random_vars() {
281        let context = SpecialVarContext::default();
282
283        let rand1 = resolve_special_var("$random", &context);
284        assert!(matches!(rand1, Some(Value::Number(_))));
285
286        let rand2 = resolve_special_var("$random.noise", &context);
287        assert!(matches!(rand2, Some(Value::Number(_))));
288    }
289
290    #[test]
291    fn test_context_update_time() {
292        let mut context = SpecialVarContext::new(120.0, 44100);
293        context.update_time(1.0);
294
295        assert_eq!(context.current_time, 1.0);
296        assert!(context.current_beat > 0.0);
297    }
298
299    #[test]
300    fn test_parse_random_range() {
301        let result = parse_random_range("$random.range(0, 10)");
302        assert!(result.is_some());
303
304        if let Some(Value::Number(n)) = result {
305            assert!(n >= 0.0 && n <= 10.0);
306        }
307    }
308}