@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.
- package/.devalang +5 -1
- package/Cargo.toml +4 -4
- package/README.md +9 -6
- package/docs/CHANGELOG.md +46 -1
- package/docs/TODO.md +1 -1
- package/examples/index.deva +9 -6
- package/examples/pattern.deva +5 -5
- package/out-tsc/pkg/devalang_core.d.ts +1 -1
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +7 -7
- package/package.json +1 -1
- package/project-version.json +3 -3
- package/rust/cli/build/commands.rs +10 -0
- package/rust/cli/install/addon.rs +84 -38
- package/rust/cli/telemetry/event_creator.rs +17 -17
- package/rust/core/audio/engine/helpers.rs +21 -9
- package/rust/core/audio/engine/sample.rs +68 -7
- package/rust/core/audio/engine/synth.rs +19 -4
- package/rust/core/audio/evaluator.rs +64 -26
- package/rust/core/audio/interpreter/arrow_call.rs +21 -16
- package/rust/core/audio/interpreter/call.rs +156 -1
- package/rust/core/audio/interpreter/spawn.rs +145 -1
- package/rust/core/audio/special/math.rs +22 -2
- package/rust/core/lexer/driver.rs +61 -0
- package/rust/core/lexer/handler/identifier.rs +3 -2
- package/rust/core/lexer/mod.rs +1 -62
- package/rust/core/lexer/token.rs +1 -0
- package/rust/core/parser/driver.rs +12 -9
- package/rust/core/parser/handler/dot.rs +3 -2
- package/rust/core/parser/handler/loop_.rs +2 -2
- package/rust/core/parser/handler/mod.rs +1 -0
- package/rust/core/parser/handler/pattern.rs +74 -0
- package/rust/core/preprocessor/loader.rs +87 -127
- package/rust/core/preprocessor/processor.rs +7 -7
- package/rust/core/preprocessor/resolver/call.rs +28 -0
- package/rust/core/preprocessor/resolver/driver.rs +15 -13
- package/rust/core/preprocessor/resolver/mod.rs +1 -0
- package/rust/core/preprocessor/resolver/pattern.rs +75 -0
- package/rust/core/preprocessor/resolver/spawn.rs +27 -0
- package/rust/core/store/variable.rs +15 -1
- package/rust/main.rs +4 -1
- package/rust/types/Cargo.toml +3 -0
- package/rust/types/src/ast.rs +4 -0
- package/rust/utils/Cargo.toml +4 -1
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
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
|
|
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
|
-
">" =>
|
|
29
|
-
"<" =>
|
|
30
|
-
">=" =>
|
|
31
|
-
"<=" =>
|
|
32
|
-
"==" =>
|
|
33
|
-
|
|
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
|
|
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
|
|
69
|
-
find_and_eval_first_mod_call(&s, evaluate_numeric_expression, vars, bpm, beat)
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|