@devaloop/devalang 0.0.1-alpha.10 → 0.0.1-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.devalang +6 -1
  2. package/Cargo.toml +6 -2
  3. package/README.md +59 -142
  4. package/docs/CHANGELOG.md +60 -1
  5. package/docs/ROADMAP.md +1 -1
  6. package/docs/TODO.md +1 -1
  7. package/examples/bank.deva +9 -0
  8. package/examples/duration.deva +9 -0
  9. package/examples/index.deva +6 -6
  10. package/out-tsc/bin/devalang.exe +0 -0
  11. package/package.json +2 -1
  12. package/project-version.json +3 -3
  13. package/rust/cli/bank.rs +455 -0
  14. package/rust/cli/build.rs +1 -1
  15. package/rust/cli/check.rs +1 -1
  16. package/rust/cli/driver.rs +280 -0
  17. package/rust/cli/install.rs +17 -0
  18. package/rust/cli/mod.rs +5 -200
  19. package/rust/cli/play.rs +1 -1
  20. package/rust/cli/update.rs +4 -0
  21. package/rust/common/cdn.rs +11 -0
  22. package/rust/common/mod.rs +1 -0
  23. package/rust/config/driver.rs +76 -0
  24. package/rust/config/loader.rs +98 -1
  25. package/rust/config/mod.rs +1 -15
  26. package/rust/core/audio/engine.rs +151 -10
  27. package/rust/core/audio/interpreter/arrow_call.rs +17 -4
  28. package/rust/core/audio/interpreter/trigger.rs +56 -2
  29. package/rust/core/audio/loader/trigger.rs +12 -0
  30. package/rust/core/lexer/handler/driver.rs +12 -1
  31. package/rust/core/lexer/handler/mod.rs +1 -0
  32. package/rust/core/lexer/handler/slash.rs +21 -0
  33. package/rust/core/lexer/token.rs +1 -0
  34. package/rust/core/parser/driver.rs +36 -2
  35. package/rust/core/parser/handler/arrow_call.rs +29 -4
  36. package/rust/core/parser/handler/dot.rs +102 -37
  37. package/rust/core/preprocessor/loader.rs +93 -14
  38. package/rust/core/preprocessor/resolver/driver.rs +5 -0
  39. package/rust/core/shared/bank.rs +21 -0
  40. package/rust/core/shared/duration.rs +1 -0
  41. package/rust/core/shared/mod.rs +2 -1
  42. package/rust/core/shared/value.rs +1 -0
  43. package/rust/installer/bank.rs +55 -0
  44. package/rust/installer/mod.rs +2 -0
  45. package/rust/installer/utils.rs +56 -0
  46. package/rust/main.rs +62 -5
  47. package/docs/COMMANDS.md +0 -85
  48. package/docs/CONFIG.md +0 -30
  49. package/docs/SYNTAX.md +0 -230
@@ -1,5 +1,6 @@
1
1
  use std::{ fs, path::Path };
2
- use crate::config::Config;
2
+ use std::collections::HashMap;
3
+ use crate::config::driver::{ BankEntry, Config };
3
4
 
4
5
  pub fn load_config(path: Option<&Path>) -> Option<Config> {
5
6
  let config_path = path.unwrap_or_else(|| Path::new(".devalang"));
@@ -11,3 +12,99 @@ pub fn load_config(path: Option<&Path>) -> Option<Config> {
11
12
  None
12
13
  }
13
14
  }
15
+
16
+ pub fn update_bank_version_in_config(config: &mut Config, dependency: &str, new_version: &str) {
17
+ // Si le vecteur banks n'existe pas, on ne fait rien
18
+ if config.banks.is_none() {
19
+ println!("No banks configured.");
20
+ return;
21
+ }
22
+
23
+ let banks = config.banks.as_mut().unwrap();
24
+
25
+ if let Some(bank) = banks.iter_mut().find(|b| b.path.contains(dependency)) {
26
+ bank.version = Some(new_version.to_string());
27
+
28
+ if let Err(e) = config.write(config) {
29
+ eprintln!("❌ Failed to write config: {}", e);
30
+ } else {
31
+ println!("✅ Bank '{}' updated to version '{}'", dependency, new_version);
32
+ }
33
+ } else {
34
+ println!("Bank '{}' not found in config", dependency);
35
+ }
36
+ }
37
+
38
+ pub fn remove_bank_from_config(config: &mut Config, dependency: &str) {
39
+ if config.banks.is_none() {
40
+ println!("No banks configured.");
41
+ return;
42
+ }
43
+
44
+ let banks = config.banks.as_mut().unwrap();
45
+
46
+ if let Some(index) = banks.iter().position(|b| b.path.contains(dependency)) {
47
+ banks.remove(index);
48
+
49
+ if let Err(e) = config.write(config) {
50
+ eprintln!("❌ Failed to write config: {}", e);
51
+ } else {
52
+ println!("✅ Bank '{}' removed from config", dependency);
53
+ }
54
+ } else {
55
+ println!("Bank '{}' not found in config", dependency);
56
+ }
57
+ }
58
+
59
+ pub fn add_bank_to_config(config: &mut Config, real_path: &Path, dependency: &str) {
60
+ if config.banks.is_none() {
61
+ config.banks = Some(Vec::new());
62
+ }
63
+
64
+ let banks = config.banks.as_mut().unwrap();
65
+
66
+ let exists = banks.iter().any(|b| b.path == dependency);
67
+ if exists {
68
+ println!("Bank '{}' already in config", dependency);
69
+ return;
70
+ }
71
+
72
+ let metadata_path = Path::new(real_path).join("bank.toml");
73
+
74
+ if !metadata_path.exists() {
75
+ eprintln!("❌ Bank metadata file '{}' does not exist", metadata_path.display());
76
+ return;
77
+ }
78
+
79
+ let metadata_content = fs
80
+ ::read_to_string(&metadata_path)
81
+ .expect("Failed to read bank metadata file");
82
+
83
+ let metadata: HashMap<String, String> = toml
84
+ ::from_str(&metadata_content)
85
+ .expect("Failed to parse bank metadata file");
86
+
87
+ let bank_to_insert = BankEntry {
88
+ path: dependency.to_string(),
89
+ version: Some(
90
+ metadata
91
+ .get("version")
92
+ .cloned()
93
+ .unwrap_or_else(|| "0.0.1".to_string())
94
+ ),
95
+ author: Some(
96
+ metadata
97
+ .get("author")
98
+ .cloned()
99
+ .unwrap_or_else(|| "unknown".to_string())
100
+ ),
101
+ };
102
+
103
+ banks.push(bank_to_insert);
104
+
105
+ if let Err(e) = config.write(config) {
106
+ eprintln!("❌ Failed to write config: {}", e);
107
+ } else {
108
+ println!("✅ Bank '{}' added to config", dependency);
109
+ }
110
+ }
@@ -1,16 +1,2 @@
1
1
  pub mod loader;
2
-
3
- use serde::Deserialize;
4
-
5
- #[derive(Debug, Deserialize, Clone)]
6
- pub struct Config {
7
- pub defaults: ConfigDefaults,
8
- }
9
-
10
- #[derive(Debug, Deserialize, Clone)]
11
- pub struct ConfigDefaults {
12
- pub entry: Option<String>,
13
- pub output: Option<String>,
14
- pub watch: Option<bool>,
15
- pub repeat: Option<bool>,
16
- }
2
+ pub mod driver;
@@ -1,8 +1,9 @@
1
- use std::{ collections::HashMap, fs::File, io::BufReader };
1
+ use std::{ collections::HashMap, fs::File, io::BufReader, path::Path };
2
2
  use hound::{ SampleFormat, WavSpec, WavWriter };
3
3
  use rodio::{ Decoder, Source };
4
4
 
5
5
  use crate::core::{
6
+ shared::value::Value,
6
7
  store::variable::VariableTable,
7
8
  utils::path::{ normalize_path, resolve_relative_path },
8
9
  };
@@ -167,11 +168,56 @@ impl AudioEngine {
167
168
  filepath: &str,
168
169
  time_secs: f32,
169
170
  dur_sec: f32,
170
- effects: Option<HashMap<String, f32>>
171
+ effects: Option<HashMap<String, Value>>
171
172
  ) {
172
- let resolved = resolve_relative_path(&self.module_name.clone(), filepath);
173
+ if filepath.is_empty() {
174
+ eprintln!("❌ Empty file path provided for audio sample.");
175
+ return;
176
+ }
177
+
178
+ let mut resolved_path = String::new();
179
+
180
+ if filepath.starts_with("devalang://") {
181
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
182
+ let parts = filepath.split("devalang://").collect::<Vec<&str>>();
183
+ let object_parts = parts.get(1).unwrap_or(&"").split("/").collect::<Vec<&str>>();
184
+ let object_type = object_parts.get(0).unwrap_or(&"").to_lowercase();
185
+ let object_dir = object_parts.get(1).unwrap_or(&"").to_string();
186
+ let object_name = object_parts.get(2).unwrap_or(&"").to_string();
187
+
188
+ if object_type.contains("bank") {
189
+ resolved_path = root
190
+ .join(".deva")
191
+ .join("bank")
192
+ .join(object_dir)
193
+ .join(format!("{}.wav", object_name))
194
+ .to_str()
195
+ .unwrap_or("")
196
+ .to_string();
197
+ } else {
198
+ eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
199
+ return;
200
+ }
201
+ } else {
202
+ let module_path = &self.module_name;
203
+ let root = Path::new(module_path).parent();
204
+
205
+ if let Some(root_path) = root {
206
+ resolved_path = root_path.join(filepath).to_str().unwrap_or("").to_string();
207
+ } else {
208
+ eprintln!("❌ Could not resolve root path for module: {}", module_path);
209
+ return;
210
+ }
211
+ }
212
+
213
+ if !Path::new(&resolved_path).exists() {
214
+ eprintln!("❌ Audio file not found at: {}", resolved_path);
215
+ return;
216
+ }
173
217
 
174
- let file = BufReader::new(File::open(resolved).expect("Failed to open audio file"));
218
+ let file = BufReader::new(
219
+ File::open(&resolved_path).expect(&format!("Failed to open audio file {}", filepath))
220
+ );
175
221
  let decoder = Decoder::new(file).expect("Failed to decode audio file");
176
222
 
177
223
  // Mono or stereo reading possible here, we will duplicate in L/R
@@ -183,7 +229,7 @@ impl AudioEngine {
183
229
  return;
184
230
  }
185
231
 
186
- // TODO Apply effects here if needed
232
+ // Pad the buffer to ensure it can accommodate the new samples
187
233
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
188
234
  let required_len = offset + samples.len() * (CHANNELS as usize);
189
235
  let padded_required_len = if required_len % 2 == 1 {
@@ -193,21 +239,116 @@ impl AudioEngine {
193
239
  };
194
240
 
195
241
  self.buffer.resize(padded_required_len, 0);
196
- self.pad_samples(&samples, time_secs);
242
+
243
+ // Apply effects
244
+ if let Some(effects_map) = effects {
245
+ self.pad_samples(&samples, time_secs, Some(effects_map));
246
+ } else {
247
+ self.pad_samples(&samples, time_secs, None);
248
+ }
197
249
  }
198
250
 
199
- fn pad_samples(&mut self, samples: &[i16], time_secs: f32) {
251
+ fn pad_samples(
252
+ &mut self,
253
+ samples: &[i16],
254
+ time_secs: f32,
255
+ effects_map: Option<HashMap<String, Value>>
256
+ ) {
200
257
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
258
+ let total_samples = samples.len();
259
+
260
+ // Default values
261
+ let mut gain = 1.0;
262
+ let mut pan = 0.0;
263
+ let mut fade_in = 0.0;
264
+ let mut fade_out = 0.0;
265
+ let mut pitch = 1.0;
266
+ let mut drive = 0.0;
267
+ let mut reverb = 0.0;
268
+
269
+ if let Some(map) = &effects_map {
270
+ for (key, val) in map {
271
+ match (key.as_str(), val) {
272
+ ("gain", Value::Number(v)) => {
273
+ gain = *v;
274
+ }
275
+ ("pan", Value::Number(v)) => {
276
+ pan = *v;
277
+ }
278
+ ("fadeIn", Value::Number(v)) => {
279
+ fade_in = *v;
280
+ }
281
+ ("fadeOut", Value::Number(v)) => {
282
+ fade_out = *v;
283
+ }
284
+ ("pitch", Value::Number(v)) => {
285
+ pitch = *v;
286
+ }
287
+ ("drive", Value::Number(v)) => {
288
+ // Drive effect can be implemented here if needed
289
+ drive = *v;
290
+ }
291
+ ("reverb", Value::Number(v)) => {
292
+ reverb = *v;
293
+ }
294
+ _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
295
+ }
296
+ }
297
+ }
298
+
299
+ let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
300
+ let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
201
301
 
202
302
  for (i, &sample) in samples.iter().enumerate() {
203
- let adjusted_sample = ((sample as f32) * self.volume).round() as i16;
303
+ // Gain
304
+ let mut adjusted = (sample as f32) * gain;
305
+
306
+ // Fade in
307
+ if fade_in_samples > 0 && i < fade_in_samples {
308
+ adjusted *= (i as f32) / (fade_in_samples as f32);
309
+ }
310
+
311
+ // Fade out
312
+ if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
313
+ adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
314
+ }
315
+
316
+ // Pitch adjustment
317
+ if pitch != 1.0 {
318
+ let pitch_adjusted_index = ((i as f32) / pitch) as usize;
319
+ if pitch_adjusted_index < total_samples {
320
+ adjusted = (samples[pitch_adjusted_index] as f32) * gain;
321
+ } else {
322
+ adjusted = 0.0; // Out of bounds, set to zero
323
+ }
324
+ }
325
+
326
+ // Drive effect
327
+ if drive > 0.0 {
328
+ adjusted = adjusted.tanh() * (1.0 + drive);
329
+ }
330
+
331
+ // Reverb effect
332
+ if reverb > 0.0 {
333
+ let reverb_delay = (reverb * (SAMPLE_RATE as f32)) as usize;
334
+ if i >= reverb_delay {
335
+ adjusted += self.buffer[offset + i - reverb_delay] as f32 * 0.5; // Simple feedback
336
+ }
337
+ }
338
+
339
+ // Clamp
340
+ let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
341
+
342
+ // Pan (L/R split)
343
+ let left = ((adjusted_sample as f32) * (1.0 - pan.clamp(0.0, 1.0))) as i16;
344
+ let right = ((adjusted_sample as f32) * (1.0 + pan.clamp(-1.0, 0.0)).abs()) as i16;
204
345
 
205
346
  let left_pos = offset + i * 2;
206
347
  let right_pos = left_pos + 1;
207
348
 
208
349
  if right_pos < self.buffer.len() {
209
- self.buffer[left_pos] = self.buffer[left_pos].saturating_add(adjusted_sample); // gauche
210
- self.buffer[right_pos] = self.buffer[right_pos].saturating_add(adjusted_sample); // droite
350
+ self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
351
+ self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
211
352
  }
212
353
  }
213
354
  }
@@ -53,8 +53,8 @@ pub fn interprete_call_arrow_statement(
53
53
  return (*max_end_time, cursor_copy);
54
54
  };
55
55
 
56
- let freq = extract_f32(params, "freq").unwrap_or(440.0);
57
- let amp = extract_f32(params, "amp").unwrap_or(1.0);
56
+ let freq = extract_f32(params, "freq", base_bpm).unwrap_or(440.0);
57
+ let amp = extract_f32(params, "amp", base_bpm).unwrap_or(1.0);
58
58
 
59
59
  if method == "note" {
60
60
  let Some(Value::Identifier(note_name)) = args.get(0) else {
@@ -69,7 +69,9 @@ pub fn interprete_call_arrow_statement(
69
69
  }
70
70
  }
71
71
 
72
- let duration_ms = extract_f32(&final_note_params, "duration").unwrap_or(base_duration);
72
+ let duration_ms = extract_f32(&final_note_params, "duration", base_bpm).unwrap_or(
73
+ base_duration
74
+ );
73
75
  let duration_secs = duration_ms / 1000.0;
74
76
 
75
77
  let final_freq = note_to_freq(note_name);
@@ -101,10 +103,21 @@ pub fn interprete_call_arrow_statement(
101
103
  (*max_end_time, cursor_copy)
102
104
  }
103
105
 
104
- fn extract_f32(map: &HashMap<String, Value>, key: &str) -> Option<f32> {
106
+ fn extract_f32(map: &HashMap<String, Value>, key: &str, base_bpm: f32) -> Option<f32> {
105
107
  map.get(key).and_then(|v| {
106
108
  match v {
107
109
  Value::Number(n) => Some(*n),
110
+ Value::Beat(beat_str) => {
111
+ let parts: Vec<&str> = beat_str.split('/').collect();
112
+ if parts.len() == 2 {
113
+ let numerator = parts[0].parse::<f32>().ok()?;
114
+ let denominator = parts[1].parse::<f32>().ok()?;
115
+
116
+ Some((numerator / denominator) * ((60.0 / base_bpm) * 1000.0))
117
+ } else {
118
+ None
119
+ }
120
+ }
108
121
  _ => None,
109
122
  }
110
123
  })
@@ -1,3 +1,5 @@
1
+ use std::collections::HashMap;
2
+
1
3
  use crate::core::{
2
4
  audio::{ engine::AudioEngine, loader::trigger::load_trigger },
3
5
  parser::statement::{ Statement, StatementKind },
@@ -14,7 +16,7 @@ pub fn interprete_trigger_statement(
14
16
  max_end_time: f32
15
17
  ) -> Option<(f32, f32, AudioEngine)> {
16
18
  if let StatementKind::Trigger { entity, duration } = &stmt.kind {
17
- if let Some(trigger_val) = variable_table.get(entity) {
19
+ if let Some(trigger_val) = resolve_namespaced_variable(entity, variable_table) {
18
20
  let duration_secs = match duration {
19
21
  Duration::Number(n) => *n,
20
22
 
@@ -41,6 +43,20 @@ pub fn interprete_trigger_statement(
41
43
  }
42
44
  }
43
45
 
46
+ Duration::Beat(beat_str) => {
47
+ // Assuming beat_str is in the format "numerator/denominator"
48
+ let parts: Vec<&str> = beat_str.split('/').collect();
49
+
50
+ if parts.len() != 2 {
51
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
52
+ return None;
53
+ }
54
+
55
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
56
+ let denominator: f32 = parts[1].parse().unwrap_or(1.0);
57
+ numerator / denominator
58
+ }
59
+
44
60
  Duration::Auto => 1.0,
45
61
  };
46
62
 
@@ -53,8 +69,13 @@ pub fn interprete_trigger_statement(
53
69
  variable_table.clone()
54
70
  );
55
71
 
72
+ if let Some(effects) = extract_effects(stmt.value.clone()) {
73
+ audio_engine.insert_sample(&src, cursor_time, duration_final, Some(effects));
74
+ } else {
75
+ audio_engine.insert_sample(&src, cursor_time, duration_final, None);
76
+ }
77
+
56
78
  let mut updated_engine = audio_engine.clone();
57
- updated_engine.insert_sample(&src, cursor_time, duration_final, None);
58
79
 
59
80
  let new_cursor_time = cursor_time + duration_final;
60
81
  let new_max_end_time = new_cursor_time.max(max_end_time);
@@ -67,3 +88,36 @@ pub fn interprete_trigger_statement(
67
88
 
68
89
  None
69
90
  }
91
+
92
+ fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) -> Option<&'a Value> {
93
+ let mut current: Option<&Value> = None;
94
+
95
+ for (i, part) in path.split('.').enumerate() {
96
+ if i == 0 {
97
+ current = variables.get(part);
98
+ } else {
99
+ current = match current {
100
+ Some(Value::Map(map)) => map.get(part),
101
+ _ => {
102
+ return None;
103
+ }
104
+ };
105
+ }
106
+ }
107
+
108
+ current
109
+ }
110
+
111
+ fn extract_effects(value: Value) -> Option<HashMap<String, Value>> {
112
+ if let Value::Map(map) = value.clone() {
113
+ let mut effects = HashMap::new();
114
+
115
+ for (key, val) in map {
116
+ effects.insert(key.clone(), val);
117
+ }
118
+
119
+ Some(effects)
120
+ } else {
121
+ None
122
+ }
123
+ }
@@ -43,6 +43,18 @@ pub fn load_trigger(
43
43
  duration_as_secs = base_duration;
44
44
  }
45
45
 
46
+ Duration::Beat(beat_str) => {
47
+ let parts: Vec<&str> = beat_str.split('/').collect();
48
+
49
+ if parts.len() == 2 {
50
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
51
+ let denominator: f32 = parts[1].parse().unwrap_or(1.0);
52
+ duration_as_secs = numerator / denominator * base_duration;
53
+ } else {
54
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
55
+ }
56
+ }
57
+
46
58
  _ => {
47
59
  eprintln!("❌ Invalid duration type. Expected an identifier.");
48
60
  }
@@ -1,6 +1,6 @@
1
1
  use crate::core::lexer::{
2
2
  handler::{
3
- arrow::handle_arrow_lexer, at::handle_at_lexer, brace::{ handle_lbrace_lexer, handle_rbrace_lexer }, colon::handle_colon_lexer, comment::handle_comment_lexer, dot::handle_dot_lexer, identifier::handle_identifier_lexer, indent::handle_indent_lexer, newline::handle_newline_lexer, number::handle_number_lexer, operator::handle_operator_lexer, string::handle_string_lexer
3
+ arrow::handle_arrow_lexer, at::handle_at_lexer, brace::{ handle_lbrace_lexer, handle_rbrace_lexer }, colon::handle_colon_lexer, comment::handle_comment_lexer, dot::handle_dot_lexer, identifier::handle_identifier_lexer, indent::handle_indent_lexer, newline::handle_newline_lexer, number::handle_number_lexer, operator::handle_operator_lexer, slash::handle_slash_lexer, string::handle_string_lexer
4
4
  },
5
5
  token::{ Token, TokenKind },
6
6
  };
@@ -101,6 +101,17 @@ pub fn handle_content_lexing(content: String) -> Result<Vec<Token>, String> {
101
101
  &mut column
102
102
  );
103
103
  }
104
+ '/' => {
105
+ handle_slash_lexer(
106
+ ch,
107
+ &mut chars,
108
+ &mut current_indent,
109
+ &mut indent_stack,
110
+ &mut tokens,
111
+ &mut line,
112
+ &mut column
113
+ );
114
+ }
104
115
  '-' => {
105
116
  handle_arrow_lexer(
106
117
  ch,
@@ -12,3 +12,4 @@ pub mod number;
12
12
  pub mod indent;
13
13
  pub mod string;
14
14
  pub mod arrow;
15
+ pub mod slash;
@@ -0,0 +1,21 @@
1
+ use crate::core::lexer::token::{ Token, TokenKind };
2
+
3
+ pub fn handle_slash_lexer(
4
+ char: char,
5
+ chars: &mut std::iter::Peekable<std::str::Chars>,
6
+ current_indent: &mut usize,
7
+ indent_stack: &mut Vec<usize>,
8
+ tokens: &mut Vec<Token>,
9
+ line: &mut usize,
10
+ column: &mut usize
11
+ ) {
12
+ let mut slash = char.to_string();
13
+
14
+ tokens.push(Token {
15
+ kind: TokenKind::Slash,
16
+ lexeme: slash,
17
+ line: *line,
18
+ column: *column,
19
+ indent: *current_indent,
20
+ });
21
+ }
@@ -49,6 +49,7 @@ pub enum TokenKind {
49
49
  Comma,
50
50
  Equals,
51
51
  Dot,
52
+ Slash,
52
53
 
53
54
  // ───── Operators ─────
54
55
  DoubleEquals,
@@ -64,6 +64,10 @@ impl Parser {
64
64
  }
65
65
  }
66
66
 
67
+ pub fn peek_nth_kind(&self, n: usize) -> Option<TokenKind> {
68
+ self.peek_nth(n).map(|t| t.kind.clone())
69
+ }
70
+
67
71
  pub fn advance_if(&mut self, kind: TokenKind) -> bool {
68
72
  if self.match_token(kind) { true } else { false }
69
73
  }
@@ -172,6 +176,10 @@ impl Parser {
172
176
  self.peek().map_or(false, |t| t.kind == kind)
173
177
  }
174
178
 
179
+ pub fn peek_kind(&self) -> Option<TokenKind> {
180
+ self.peek().map(|t| t.kind.clone())
181
+ }
182
+
175
183
  pub fn parse_map_value(&mut self) -> Option<Value> {
176
184
  if !self.match_token(TokenKind::LBrace) {
177
185
  return None;
@@ -198,9 +206,35 @@ impl Parser {
198
206
  Value::String(token.lexeme.clone())
199
207
  }
200
208
  TokenKind::Number => {
201
- self.advance();
202
- Value::Number(token.lexeme.parse().unwrap_or(0.0))
209
+ let mut number_str = token.lexeme.clone();
210
+ self.advance(); // consume the first number
211
+
212
+ if let Some(dot_token) = self.peek_clone() {
213
+ if dot_token.kind == TokenKind::Dot {
214
+ self.advance(); // consume the dot
215
+
216
+ if let Some(decimal_token) = self.peek_clone() {
217
+ if decimal_token.kind == TokenKind::Number {
218
+ self.advance(); // consume the number after the dot
219
+ number_str.push('.');
220
+ number_str.push_str(&decimal_token.lexeme);
221
+ } else {
222
+ println!(
223
+ "Expected number after dot, got {:?}",
224
+ decimal_token
225
+ );
226
+ return Some(Value::Null);
227
+ }
228
+ } else {
229
+ println!("Expected number after dot, but reached EOF");
230
+ return Some(Value::Null);
231
+ }
232
+ }
233
+ }
234
+
235
+ Value::Number(number_str.parse::<f32>().unwrap_or(0.0))
203
236
  }
237
+
204
238
  TokenKind::Identifier => {
205
239
  self.advance();
206
240
  Value::Identifier(token.lexeme.clone())
@@ -89,14 +89,39 @@ pub fn parse_arrow_call(parser: &mut Parser, global_store: &mut GlobalStore) ->
89
89
  TokenKind::Identifier =>
90
90
  Value::Identifier(value_token.lexeme.clone()),
91
91
  TokenKind::String => Value::String(value_token.lexeme.clone()),
92
- TokenKind::Number =>
93
- Value::Number(
94
- value_token.lexeme.parse::<f32>().unwrap_or(0.0)
95
- ),
92
+ TokenKind::Number => {
93
+ if let Some(TokenKind::Slash) = parser.peek_kind() {
94
+ parser.advance(); // consume slash
95
+ if let Some(denominator_token) = parser.peek_clone() {
96
+ if denominator_token.kind == TokenKind::Number {
97
+ parser.advance(); // consume denominator
98
+ let denominator =
99
+ denominator_token.lexeme.clone();
100
+ Value::Beat(
101
+ format!(
102
+ "{}/{}",
103
+ value_token.lexeme,
104
+ denominator
105
+ )
106
+ )
107
+ } else {
108
+ Value::Unknown
109
+ }
110
+ } else {
111
+ Value::Unknown
112
+ }
113
+ } else {
114
+ // Regular number without slash
115
+ Value::Number(
116
+ value_token.lexeme.parse::<f32>().unwrap_or(0.0)
117
+ )
118
+ }
119
+ }
96
120
  TokenKind::Boolean =>
97
121
  Value::Boolean(
98
122
  value_token.lexeme.parse::<bool>().unwrap_or(false)
99
123
  ),
124
+
100
125
  _ => Value::Unknown,
101
126
  };
102
127
  map.insert(key, value);