@devaloop/devalang 0.0.1-alpha.17 → 0.0.1-alpha.18

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 (44) hide show
  1. package/.devalang +5 -1
  2. package/Cargo.toml +4 -4
  3. package/README.md +9 -6
  4. package/docs/CHANGELOG.md +46 -1
  5. package/docs/TODO.md +1 -1
  6. package/examples/index.deva +9 -6
  7. package/examples/pattern.deva +5 -5
  8. package/out-tsc/pkg/devalang_core.d.ts +1 -1
  9. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +7 -7
  10. package/package.json +1 -1
  11. package/project-version.json +3 -3
  12. package/rust/cli/build/commands.rs +10 -0
  13. package/rust/cli/install/addon.rs +84 -38
  14. package/rust/cli/telemetry/event_creator.rs +17 -17
  15. package/rust/core/audio/engine/helpers.rs +21 -9
  16. package/rust/core/audio/engine/sample.rs +68 -7
  17. package/rust/core/audio/engine/synth.rs +19 -4
  18. package/rust/core/audio/evaluator.rs +64 -26
  19. package/rust/core/audio/interpreter/arrow_call.rs +21 -16
  20. package/rust/core/audio/interpreter/call.rs +156 -1
  21. package/rust/core/audio/interpreter/spawn.rs +145 -1
  22. package/rust/core/audio/special/math.rs +22 -2
  23. package/rust/core/lexer/driver.rs +61 -0
  24. package/rust/core/lexer/handler/identifier.rs +3 -2
  25. package/rust/core/lexer/mod.rs +1 -62
  26. package/rust/core/lexer/token.rs +1 -0
  27. package/rust/core/parser/driver.rs +12 -9
  28. package/rust/core/parser/handler/dot.rs +3 -2
  29. package/rust/core/parser/handler/loop_.rs +2 -2
  30. package/rust/core/parser/handler/mod.rs +1 -0
  31. package/rust/core/parser/handler/pattern.rs +74 -0
  32. package/rust/core/preprocessor/loader.rs +87 -127
  33. package/rust/core/preprocessor/processor.rs +7 -7
  34. package/rust/core/preprocessor/resolver/call.rs +28 -0
  35. package/rust/core/preprocessor/resolver/driver.rs +15 -13
  36. package/rust/core/preprocessor/resolver/mod.rs +1 -0
  37. package/rust/core/preprocessor/resolver/pattern.rs +75 -0
  38. package/rust/core/preprocessor/resolver/spawn.rs +27 -0
  39. package/rust/core/store/variable.rs +15 -1
  40. package/rust/main.rs +4 -1
  41. package/rust/types/Cargo.toml +3 -0
  42. package/rust/types/src/ast.rs +4 -0
  43. package/rust/utils/Cargo.toml +4 -1
  44. package/rust/web/api.rs +2 -2
@@ -15,22 +15,46 @@ pub fn evaluate_condition_string(expr: &str, vars: &VariableTable) -> bool {
15
15
  let op = tokens[1];
16
16
  let right = tokens[2];
17
17
 
18
- let left_val = match vars.get(left) {
19
- Some(Value::Number(n)) => n,
20
- _ => {
21
- return false;
18
+ // Resolve left and right to numeric values where possible. Accept numbers, variables or env atoms.
19
+ fn resolve_for_cond(s: &str, vars: &VariableTable) -> Option<f32> {
20
+ if let Ok(n) = s.parse::<f32>() {
21
+ return Some(n);
22
+ }
23
+ if let Some(Value::Number(n)) = vars.get(s) {
24
+ return Some(*n);
25
+ }
26
+ if let Some(v) = resolve_env_atom(s, 120.0, 1.0) {
27
+ return Some(v);
22
28
  }
29
+ None
30
+ }
31
+
32
+ let left_val = match resolve_for_cond(left, vars) {
33
+ Some(v) => v,
34
+ None => return false,
23
35
  };
24
36
 
25
- let right_val: f32 = right.parse().unwrap_or(0.0);
37
+ let right_val = match resolve_for_cond(right, vars) {
38
+ Some(v) => v,
39
+ None => return false,
40
+ };
26
41
 
27
42
  match op {
28
- ">" => *left_val > right_val,
29
- "<" => *left_val < right_val,
30
- ">=" => *left_val >= right_val,
31
- "<=" => *left_val <= right_val,
32
- "==" => (*left_val - right_val).abs() < f32::EPSILON,
33
- "!=" => (*left_val - right_val).abs() > f32::EPSILON,
43
+ ">" => left_val > right_val,
44
+ "<" => left_val < right_val,
45
+ ">=" => left_val >= right_val,
46
+ "<=" => left_val <= right_val,
47
+ "==" => {
48
+ // relative epsilon for floating comparisons
49
+ let diff = (left_val - right_val).abs();
50
+ let largest = left_val.abs().max(right_val.abs()).max(1.0);
51
+ diff <= (f32::EPSILON * largest)
52
+ }
53
+ "!=" => {
54
+ let diff = (left_val - right_val).abs();
55
+ let largest = left_val.abs().max(right_val.abs()).max(1.0);
56
+ diff > (f32::EPSILON * largest)
57
+ }
34
58
  _ => false,
35
59
  }
36
60
  }
@@ -62,25 +86,39 @@ pub fn evaluate_numeric_expression(
62
86
  // Shunting-like, simplified: first evaluate any $math.func(...) calls anywhere in the expression,
63
87
  // then fold remaining parentheses and evaluate left-to-right.
64
88
  fn eval(expr: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
65
- // 1) Replace $math.* calls progressively
89
+ // 1) Replace $math/$easing/$mod calls progressively with a max iteration guard
66
90
  let mut s = expr.to_string();
91
+ let mut iterations = 0u32;
92
+ const MAX_ITER: u32 = 64;
93
+
67
94
  // Evaluate modulators first (they may feed easing/math)
68
- while let Some(next) =
69
- find_and_eval_first_mod_call(&s, evaluate_numeric_expression, vars, bpm, beat)
70
- {
71
- s = next;
95
+ while iterations < MAX_ITER {
96
+ if let Some(next) = find_and_eval_first_mod_call(&s, evaluate_numeric_expression, vars, bpm, beat) {
97
+ s = next;
98
+ iterations += 1;
99
+ continue;
100
+ }
101
+ break;
72
102
  }
73
- // Then easing functions
74
- while let Some(next) =
75
- find_and_eval_first_easing_call(&s, evaluate_numeric_expression, vars, bpm, beat)
76
- {
77
- s = next;
103
+
104
+ iterations = 0;
105
+ while iterations < MAX_ITER {
106
+ if let Some(next) = find_and_eval_first_easing_call(&s, evaluate_numeric_expression, vars, bpm, beat) {
107
+ s = next;
108
+ iterations += 1;
109
+ continue;
110
+ }
111
+ break;
78
112
  }
79
- // Finally math transforms
80
- while let Some(next) =
81
- find_and_eval_first_math_call(&s, evaluate_numeric_expression, vars, bpm, beat)
82
- {
83
- s = next;
113
+
114
+ iterations = 0;
115
+ while iterations < MAX_ITER {
116
+ if let Some(next) = find_and_eval_first_math_call(&s, evaluate_numeric_expression, vars, bpm, beat) {
117
+ s = next;
118
+ iterations += 1;
119
+ continue;
120
+ }
121
+ break;
84
122
  }
85
123
 
86
124
  // 2) Evaluate remaining (pure) parentheses starting from innermost
@@ -5,6 +5,7 @@ use crate::core::{
5
5
  store::{global::GlobalStore, variable::VariableTable},
6
6
  };
7
7
  use devalang_types::Value;
8
+ use devalang_utils::logger::{Logger, LogLevel};
8
9
 
9
10
  use std::collections::HashMap;
10
11
 
@@ -28,30 +29,32 @@ pub fn interprete_call_arrow_statement(
28
29
  } = &stmt.kind
29
30
  {
30
31
  let Some(Value::Statement(synth_stmt)) = variable_table.get(target) else {
31
- println!("❌ Synth '{}' not found in variable table", target);
32
+ let logger = Logger::new();
33
+ logger.log_message(LogLevel::Error, &format!("Synth '{}' not found in variable table", target));
32
34
  return (*max_end_time, cursor_copy);
33
35
  };
34
36
 
35
37
  let Value::Map(synth_map) = &synth_stmt.value else {
36
- println!(
37
- "Invalid synth statement for '{}', expected a map.",
38
- target
39
- );
38
+ let logger = Logger::new();
39
+ logger.log_message(LogLevel::Error, &format!("Invalid synth statement for '{}', expected a map.", target));
40
40
  return (*max_end_time, cursor_copy);
41
41
  };
42
42
 
43
43
  let Some(Value::String(entity)) = synth_map.get("entity") else {
44
- println!("❌ Missing 'entity' key in synth '{}'.", target);
44
+ let logger = Logger::new();
45
+ logger.log_message(LogLevel::Error, &format!("Missing 'entity' key in synth '{}'.", target));
45
46
  return (*max_end_time, cursor_copy);
46
47
  };
47
48
 
48
49
  if entity != "synth" {
49
- println!("❌ '{}' is not a synth, entity is '{}'.", target, entity);
50
+ let logger = Logger::new();
51
+ logger.log_message(LogLevel::Error, &format!("'{}' is not a synth, entity is '{}'.", target, entity));
50
52
  return (*max_end_time, cursor_copy);
51
53
  }
52
54
 
53
55
  let Some(Value::Map(value_map)) = synth_map.get("value") else {
54
- println!("❌ Missing 'value' map in synth '{}'.", target);
56
+ let logger = Logger::new();
57
+ logger.log_message(LogLevel::Error, &format!("Missing 'value' map in synth '{}'.", target));
55
58
  return (*max_end_time, cursor_copy);
56
59
  };
57
60
 
@@ -59,7 +62,8 @@ pub fn interprete_call_arrow_statement(
59
62
  Some(Value::String(s)) => s.clone(),
60
63
  Some(Value::Identifier(s)) => s.clone(),
61
64
  _ => {
62
- println!("? Missing or invalid 'waveform' in synth '{}'.", target);
65
+ let logger = Logger::new();
66
+ logger.log_message(LogLevel::Error, &format!("Missing or invalid 'waveform' in synth '{}'.", target));
63
67
  return (*max_end_time, cursor_copy);
64
68
  }
65
69
  };
@@ -196,16 +200,16 @@ pub fn interprete_call_arrow_statement(
196
200
  audio_engine.buffer[idx].saturating_add(s);
197
201
  }
198
202
  } else {
199
- println!(
200
- "? Plugin bytes not found for key '{}' (alias '{}').",
201
- key, alias
202
- );
203
+ let logger = Logger::new();
204
+ logger.log_message(LogLevel::Warning, &format!("Plugin bytes not found for key '{}' (alias '{}').", key, alias));
203
205
  }
204
206
  } else {
205
- println!("? Invalid plugin URI in alias '{}': {}", alias, uri);
207
+ let logger = Logger::new();
208
+ logger.log_message(LogLevel::Warning, &format!("Invalid plugin URI in alias '{}': {}", alias, uri));
206
209
  }
207
210
  } else {
208
- println!("? Plugin alias '{}' not found in variable table.", alias);
211
+ let logger = Logger::new();
212
+ logger.log_message(LogLevel::Warning, &format!("Plugin alias '{}' not found in variable table.", alias));
209
213
  }
210
214
  } else {
211
215
  audio_engine.insert_note(
@@ -230,7 +234,8 @@ pub fn interprete_call_arrow_statement(
230
234
 
231
235
  return (*max_end_time, end_time);
232
236
  } else {
233
- println!("❌ Unknown method '{}' on synth '{}'.", method, target);
237
+ let logger = Logger::new();
238
+ logger.log_message(LogLevel::Error, &format!("Unknown method '{}' on synth '{}'.", method, target));
234
239
  }
235
240
  }
236
241
 
@@ -1,4 +1,4 @@
1
- use devalang_types::Value;
1
+ use devalang_types::{Duration, Value};
2
2
 
3
3
  use crate::core::{
4
4
  audio::{engine::AudioEngine, interpreter::driver::execute_audio_block},
@@ -69,6 +69,111 @@ pub fn interprete_call_statement(
69
69
  }
70
70
  }
71
71
 
72
+ // Pattern case
73
+ if let Some(pattern_stmt) = find_pattern(name, variable_table, global_store) {
74
+ // Extract pattern string from statement value
75
+ if let Value::String(pat) = &pattern_stmt.value {
76
+ // Determine target entity (explicit or inferred)
77
+ let mut target_entity = name.clone();
78
+ if let StatementKind::Pattern { name: _n, target } = &pattern_stmt.kind {
79
+ if let Some(t) = target {
80
+ target_entity = t.clone();
81
+ }
82
+ }
83
+
84
+ // Build a variable table snapshot for resolution like triggers do
85
+ // Preserve the full parent chain so lookups behave the same as runtime
86
+ fn clone_with_parents(orig: &crate::core::store::variable::VariableTable) -> crate::core::store::variable::VariableTable {
87
+ crate::core::store::variable::VariableTable {
88
+ variables: orig.variables.clone(),
89
+ parent: orig.parent.as_ref().map(|p| Box::new(clone_with_parents(p))),
90
+ }
91
+ }
92
+
93
+ let final_variable_table = clone_with_parents(variable_table);
94
+
95
+ // Normalize pattern: remove spaces and line breaks
96
+ let pattern_str: String = pat.chars().filter(|c| !c.is_whitespace()).collect();
97
+ if pattern_str.is_empty() {
98
+ return (max_end_time, cursor_time);
99
+ }
100
+
101
+ let step_count = pattern_str.len() as f32;
102
+ // Assume pattern spans one bar (4 beats)
103
+ let total_bar = 4.0 * base_duration;
104
+ let step_duration = total_bar / step_count; // seconds per step
105
+
106
+ let mut updated_max = max_end_time;
107
+
108
+ for (i, ch) in pattern_str.chars().enumerate() {
109
+ if ch == '-' {
110
+ continue; // rest
111
+ }
112
+
113
+ // Schedule a trigger at cursor_time + offset
114
+ let event_time = cursor_time + (i as f32) * step_duration;
115
+
116
+ // Resolve trigger value similarly to interprete_trigger_statement
117
+ let mut trigger_val = Value::String(target_entity.clone());
118
+ if let Some(val) = variable_table.variables.get(&target_entity) {
119
+ match val {
120
+ Value::Identifier(id) => {
121
+ // resolve from parent if available
122
+ if let Some(parent) = &variable_table.parent {
123
+ if let Some(v) = parent.get(id) {
124
+ trigger_val = v.clone();
125
+ }
126
+ } else if let Some(v) = variable_table.get(id) {
127
+ trigger_val = v.clone();
128
+ }
129
+ }
130
+ Value::Map(map) => {
131
+ if let Some(Value::String(src)) = map.get("entity") {
132
+ trigger_val = Value::String(src.clone());
133
+ } else if let Some(Value::Identifier(src)) = map.get("entity") {
134
+ trigger_val = Value::Identifier(src.clone());
135
+ }
136
+ }
137
+ Value::Sample(sample_src) => {
138
+ trigger_val = Value::Sample(sample_src.clone());
139
+ }
140
+ _ => {
141
+ // leave as string
142
+ }
143
+ }
144
+ }
145
+
146
+ // Use loader to get sample path and sample length
147
+ let (src, sample_length) =
148
+ crate::core::audio::loader::trigger::load_trigger(&trigger_val, &Duration::Number(step_duration), &None, base_duration, final_variable_table.clone());
149
+
150
+ let play_length = step_duration.min(sample_length);
151
+
152
+ let trigger_src = match trigger_val.get("entity") {
153
+ Some(Value::String(s)) => s.clone(),
154
+ Some(Value::Identifier(id)) => id.clone(),
155
+ Some(Value::Statement(stmt)) => {
156
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
157
+ entity.clone()
158
+ } else {
159
+ src.clone()
160
+ }
161
+ }
162
+ _ => src.clone(),
163
+ };
164
+
165
+ audio_engine.insert_sample(&trigger_src, event_time, play_length, None, &final_variable_table);
166
+
167
+ let end_time = event_time + play_length;
168
+ if end_time > updated_max {
169
+ updated_max = end_time;
170
+ }
171
+ }
172
+
173
+ return (updated_max, cursor_time);
174
+ }
175
+ }
176
+
72
177
  // Function or group not found; keep as debug-free fail path
73
178
  }
74
179
 
@@ -117,3 +222,53 @@ fn find_group(
117
222
 
118
223
  None
119
224
  }
225
+
226
+ fn find_pattern(
227
+ name: &str,
228
+ variable_table: &VariableTable,
229
+ global_store: &GlobalStore,
230
+ ) -> Option<Statement> {
231
+ use crate::core::parser::statement::Statement;
232
+ use crate::core::parser::statement::StatementKind;
233
+
234
+ if let Some(Value::Statement(stmt_box)) = variable_table.get(name) {
235
+ if let StatementKind::Pattern { .. } = stmt_box.kind {
236
+ return Some(*stmt_box.clone());
237
+ }
238
+ }
239
+
240
+ if let Some(val) = global_store.variables.variables.get(name) {
241
+ match val {
242
+ Value::Statement(stmt_box) => {
243
+ if let StatementKind::Pattern { .. } = stmt_box.kind {
244
+ return Some(*stmt_box.clone());
245
+ }
246
+ }
247
+ Value::Map(map) => {
248
+ if let Some(Value::String(_pat)) = map.get("pattern") {
249
+ // Rebuild a Pattern statement from stored map if possible
250
+ let stmt = Statement {
251
+ kind: StatementKind::Pattern {
252
+ name: name.to_string(),
253
+ target: map.get("target").and_then(|v| match v {
254
+ Value::String(s) => Some(s.clone()),
255
+ _ => None,
256
+ }),
257
+ },
258
+ value: Value::String(map.get("pattern").and_then(|v| match v {
259
+ Value::String(s) => Some(s.clone()),
260
+ _ => None,
261
+ }).unwrap_or_default()),
262
+ indent: 0,
263
+ line: 0,
264
+ column: 0,
265
+ };
266
+ return Some(stmt);
267
+ }
268
+ }
269
+ _ => {}
270
+ }
271
+ }
272
+
273
+ None
274
+ }
@@ -1,4 +1,4 @@
1
- use devalang_types::Value;
1
+ use devalang_types::{Duration, Value};
2
2
 
3
3
  use crate::core::{
4
4
  audio::{engine::AudioEngine, interpreter::driver::execute_audio_block},
@@ -68,6 +68,100 @@ pub fn interprete_spawn_statement(
68
68
  }
69
69
  }
70
70
 
71
+ // Pattern case: allow spawning a pattern similar to call
72
+ if let Some(pattern_stmt) = find_pattern(name, variable_table, global_store) {
73
+ if let Value::String(pat) = &pattern_stmt.value {
74
+ let mut target_entity = name.clone();
75
+ if let StatementKind::Pattern { name: _n, target } = &pattern_stmt.kind {
76
+ if let Some(t) = target {
77
+ target_entity = t.clone();
78
+ }
79
+ }
80
+
81
+ let final_variable_table = if let Some(parent) = &variable_table.parent {
82
+ crate::core::store::variable::VariableTable {
83
+ variables: parent.variables.clone(),
84
+ parent: None,
85
+ }
86
+ } else {
87
+ variable_table.clone()
88
+ };
89
+
90
+ let pattern_str: String = pat.chars().filter(|c| !c.is_whitespace()).collect();
91
+ if pattern_str.is_empty() {
92
+ return (max_end_time, cursor_time);
93
+ }
94
+
95
+ let step_count = pattern_str.len() as f32;
96
+ let total_bar = 4.0 * base_duration;
97
+ let step_duration = total_bar / step_count;
98
+
99
+ let mut updated_max = max_end_time;
100
+
101
+ for (i, ch) in pattern_str.chars().enumerate() {
102
+ if ch == '-' {
103
+ continue;
104
+ }
105
+
106
+ let event_time = cursor_time + (i as f32) * step_duration;
107
+
108
+ let mut trigger_val = Value::String(target_entity.clone());
109
+ if let Some(val) = variable_table.variables.get(&target_entity) {
110
+ match val {
111
+ Value::Identifier(id) => {
112
+ if let Some(parent) = &variable_table.parent {
113
+ if let Some(v) = parent.get(id) {
114
+ trigger_val = v.clone();
115
+ }
116
+ } else if let Some(v) = variable_table.get(id) {
117
+ trigger_val = v.clone();
118
+ }
119
+ }
120
+ Value::Map(map) => {
121
+ if let Some(Value::String(src)) = map.get("entity") {
122
+ trigger_val = Value::String(src.clone());
123
+ } else if let Some(Value::Identifier(src)) = map.get("entity") {
124
+ trigger_val = Value::Identifier(src.clone());
125
+ }
126
+ }
127
+ Value::Sample(sample_src) => {
128
+ trigger_val = Value::Sample(sample_src.clone());
129
+ }
130
+ _ => {}
131
+ }
132
+ }
133
+
134
+ let (src, sample_length) =
135
+ crate::core::audio::loader::trigger::load_trigger(&trigger_val, &Duration::Number(step_duration), &None, base_duration, final_variable_table.clone());
136
+
137
+ let play_length = step_duration.min(sample_length);
138
+
139
+ let trigger_src = match trigger_val.get("entity") {
140
+ Some(Value::String(s)) => s.clone(),
141
+ Some(Value::Identifier(id)) => id.clone(),
142
+ Some(Value::Statement(stmt)) => {
143
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
144
+ entity.clone()
145
+ } else {
146
+ src.clone()
147
+ }
148
+ }
149
+ _ => src.clone(),
150
+ };
151
+
152
+ audio_engine.insert_sample(&trigger_src, event_time, play_length, None, &final_variable_table);
153
+
154
+ let end_time = event_time + play_length;
155
+ if end_time > updated_max {
156
+ updated_max = end_time;
157
+ }
158
+ }
159
+
160
+ audio_engine.merge_with(local_engine);
161
+ return (updated_max.max(max_end_time), cursor_time);
162
+ }
163
+ }
164
+
71
165
  // Function or group not found
72
166
  }
73
167
 
@@ -91,3 +185,53 @@ fn find_group<'a>(
91
185
  }
92
186
  None
93
187
  }
188
+
189
+ fn find_pattern(
190
+ name: &str,
191
+ variable_table: &VariableTable,
192
+ global_store: &GlobalStore,
193
+ ) -> Option<Statement> {
194
+ use crate::core::parser::statement::Statement;
195
+ use crate::core::parser::statement::StatementKind;
196
+
197
+ if let Some(Value::Statement(stmt_box)) = variable_table.get(name) {
198
+ if let StatementKind::Pattern { .. } = stmt_box.kind {
199
+ return Some(*stmt_box.clone());
200
+ }
201
+ }
202
+
203
+ if let Some(val) = global_store.variables.variables.get(name) {
204
+ match val {
205
+ Value::Statement(stmt_box) => {
206
+ if let StatementKind::Pattern { .. } = stmt_box.kind {
207
+ return Some(*stmt_box.clone());
208
+ }
209
+ }
210
+ Value::Map(map) => {
211
+ if let Some(Value::String(_pat)) = map.get("pattern") {
212
+ // Rebuild a Pattern statement from stored map if possible
213
+ let stmt = Statement {
214
+ kind: StatementKind::Pattern {
215
+ name: name.to_string(),
216
+ target: map.get("target").and_then(|v| match v {
217
+ Value::String(s) => Some(s.clone()),
218
+ _ => None,
219
+ }),
220
+ },
221
+ value: Value::String(map.get("pattern").and_then(|v| match v {
222
+ Value::String(s) => Some(s.clone()),
223
+ _ => None,
224
+ }).unwrap_or_default()),
225
+ indent: 0,
226
+ line: 0,
227
+ column: 0,
228
+ };
229
+ return Some(stmt);
230
+ }
231
+ }
232
+ _ => {}
233
+ }
234
+ }
235
+
236
+ None
237
+ }
@@ -1,4 +1,5 @@
1
1
  use crate::core::store::variable::VariableTable;
2
+ use devalang_utils::logger::{Logger, LogLevel};
2
3
 
3
4
  // Parse comma-separated arguments at top level (no nested parentheses split)
4
5
  fn parse_top_level_args(s: &str) -> Vec<&str> {
@@ -56,9 +57,21 @@ pub fn find_and_eval_first_math_call<EvalFn>(
56
57
  where
57
58
  EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
58
59
  {
60
+ let logger = Logger::new();
61
+
59
62
  let start = s.find("$math.")?;
60
- let open_rel = s[start..].find('(')?;
63
+ let open_rel = match s[start..].find('(') {
64
+ Some(i) => i,
65
+ None => {
66
+ logger.log_message(LogLevel::Error, &format!("Malformed $math call: missing '(' in '{}'", s));
67
+ return None;
68
+ }
69
+ };
61
70
  let open = start + open_rel;
71
+ if open <= start + 6 {
72
+ logger.log_message(LogLevel::Error, &format!("Malformed $math call: missing function name in '{}'", s));
73
+ return None;
74
+ }
62
75
  let func = &s[start + 6..open];
63
76
 
64
77
  // Find matching close parenthesis, handling nesting
@@ -77,7 +90,13 @@ where
77
90
  _ => {}
78
91
  }
79
92
  }
80
- let close = close_abs?;
93
+ let close = match close_abs {
94
+ Some(c) => c,
95
+ None => {
96
+ logger.log_message(LogLevel::Error, &format!("Malformed $math call: missing closing ')' in '{}'", s));
97
+ return None;
98
+ }
99
+ };
81
100
 
82
101
  let inner = &s[open + 1..close];
83
102
  let raw_args = parse_top_level_args(inner);
@@ -86,6 +105,7 @@ where
86
105
  if let Some(v) = eval(a, vars, bpm, beat) {
87
106
  args.push(v);
88
107
  } else {
108
+ logger.log_message(LogLevel::Error, &format!("Failed to evaluate argument '{}' for $math.{}", a, func));
89
109
  return None;
90
110
  }
91
111
  }
@@ -0,0 +1,61 @@
1
+ use crate::core::{
2
+ lexer::{handler::driver::handle_content_lexing, token::Token},
3
+ utils::path::normalize_path,
4
+ };
5
+ use std::fs;
6
+ use std::path::Path;
7
+
8
+ pub struct Lexer {}
9
+
10
+ impl Default for Lexer {
11
+ fn default() -> Self {
12
+ Self::new()
13
+ }
14
+ }
15
+
16
+ impl Lexer {
17
+ pub fn new() -> Self {
18
+ Lexer {}
19
+ }
20
+
21
+ pub fn lex_from_source(&self, source: &str) -> Result<Vec<Token>, String> {
22
+ handle_content_lexing(source.to_string())
23
+ }
24
+
25
+ pub fn lex_tokens(&self, entrypoint: &str) -> Result<Vec<Token>, String> {
26
+ let path = normalize_path(entrypoint);
27
+ let resolved_path = Self::resolve_entry_path(&path)?;
28
+
29
+ let file_content = fs::read_to_string(&resolved_path).map_err(|e| {
30
+ format!(
31
+ "Failed to read the entrypoint file '{}': {}",
32
+ resolved_path, e
33
+ )
34
+ })?;
35
+
36
+ handle_content_lexing(file_content).map_err(|e| format!("Failed to lex the content: {}", e))
37
+ }
38
+
39
+ fn resolve_entry_path(path: &str) -> Result<String, String> {
40
+ let candidate = Path::new(path);
41
+
42
+ if candidate.is_dir() {
43
+ let index_path = candidate.join("index.deva");
44
+ if index_path.exists() {
45
+ Ok(index_path.to_string_lossy().replace("\\", "/"))
46
+ } else {
47
+ Err(format!(
48
+ "Expected 'index.deva' in directory '{}', but it was not found",
49
+ path
50
+ ))
51
+ }
52
+ } else if candidate.is_file() {
53
+ return Ok(path.to_string());
54
+ } else {
55
+ return Err(format!(
56
+ "Provided entrypoint '{}' is not a valid file or directory",
57
+ path
58
+ ));
59
+ }
60
+ }
61
+ }
@@ -1,4 +1,4 @@
1
- use crate::core::lexer::token::{Token, TokenKind};
1
+ use crate::core::lexer::token::{ Token, TokenKind };
2
2
 
3
3
  pub fn handle_identifier_lexer(
4
4
  ch: char,
@@ -7,7 +7,7 @@ pub fn handle_identifier_lexer(
7
7
  _indent_stack: &mut [usize],
8
8
  tokens: &mut Vec<Token>,
9
9
  line: &mut usize,
10
- column: &mut usize,
10
+ column: &mut usize
11
11
  ) {
12
12
  let mut ident = ch.to_string();
13
13
 
@@ -25,6 +25,7 @@ pub fn handle_identifier_lexer(
25
25
  "if" => TokenKind::If,
26
26
  "else" => TokenKind::Else,
27
27
  "bank" => TokenKind::Bank,
28
+ "pattern" => TokenKind::Pattern,
28
29
  "bpm" => TokenKind::Tempo,
29
30
  "loop" => TokenKind::Loop,
30
31
  "for" => TokenKind::Loop,