@devaloop/devalang 0.0.1-alpha.11 → 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.
- package/.devalang +8 -8
- package/Cargo.toml +53 -54
- package/docs/CHANGELOG.md +18 -0
- package/examples/index.deva +7 -5
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +2 -1
- package/project-version.json +3 -3
- package/rust/core/audio/engine.rs +121 -8
- package/rust/core/audio/interpreter/trigger.rs +22 -1
- package/rust/core/parser/driver.rs +28 -2
- package/rust/core/parser/handler/dot.rs +9 -10
- package/rust/installer/bank.rs +1 -1
- package/rust/installer/mod.rs +2 -1
- package/rust/lib.rs +0 -1
- package/rust/utils/mod.rs +1 -2
- /package/rust/{utils/installer.rs → installer/utils.rs} +0 -0
package/.devalang
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
[defaults]
|
|
2
|
-
entry = "./examples"
|
|
3
|
-
output = "./output"
|
|
4
|
-
watch = false
|
|
5
|
-
|
|
6
|
-
[[banks]]
|
|
7
|
-
path = "devalang://bank/808"
|
|
8
|
-
version = "0.0.1"
|
|
1
|
+
[defaults]
|
|
2
|
+
entry = "./examples"
|
|
3
|
+
output = "./output"
|
|
4
|
+
watch = false
|
|
5
|
+
|
|
6
|
+
[[banks]]
|
|
7
|
+
path = "devalang://bank/808"
|
|
8
|
+
version = "0.0.1"
|
|
9
9
|
author = "devaloop"
|
package/Cargo.toml
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "devalang"
|
|
3
|
-
version = "0.0.1-alpha.
|
|
4
|
-
authors = ["Devaloop <contact@devaloop.com>"]
|
|
5
|
-
description = "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text."
|
|
6
|
-
license = "MIT"
|
|
7
|
-
repository = "https://github.com/devaloop-labs/devalang"
|
|
8
|
-
keywords = ["music", "dsl", "audio", "cli"]
|
|
9
|
-
categories = ["command-line-utilities", "development-tools", "parser-implementations"]
|
|
10
|
-
readme = "README.md"
|
|
11
|
-
homepage = "https://devalang.com"
|
|
12
|
-
documentation = "https://docs.devalang.com/"
|
|
13
|
-
license-file = "LICENSE"
|
|
14
|
-
edition = "2024"
|
|
15
|
-
|
|
16
|
-
[[bin]]
|
|
17
|
-
name = "devalang"
|
|
18
|
-
path = "rust/main.rs"
|
|
19
|
-
required-features = ["cli"]
|
|
20
|
-
|
|
21
|
-
[lib]
|
|
22
|
-
path = "rust/lib.rs"
|
|
23
|
-
crate-type = ["cdylib"]
|
|
24
|
-
|
|
25
|
-
[profile.release]
|
|
26
|
-
opt-level = "s"
|
|
27
|
-
|
|
28
|
-
[features]
|
|
29
|
-
default = ["cli"]
|
|
30
|
-
cli = ["crossterm", "indicatif", "inquire"]
|
|
31
|
-
|
|
32
|
-
[dependencies]
|
|
33
|
-
clap = { version = "4.5", features = ["derive"] }
|
|
34
|
-
serde = { version = "1.0", features = ["derive"] }
|
|
35
|
-
serde_json = "1.0"
|
|
36
|
-
rodio = "0.17"
|
|
37
|
-
hound = "3.4.0"
|
|
38
|
-
toml = "0.8"
|
|
39
|
-
notify = "6.1"
|
|
40
|
-
fs_extra = "1.3"
|
|
41
|
-
include_dir = "0.7"
|
|
42
|
-
wasm-bindgen = "0.2"
|
|
43
|
-
serde-wasm-bindgen = "0.4"
|
|
44
|
-
nom_locate = "4.0.0"
|
|
45
|
-
chrono = "0.4"
|
|
46
|
-
crossterm = { version = "0.27", optional = true }
|
|
47
|
-
indicatif = { version = "0.17", optional = true }
|
|
48
|
-
inquire = { version = "0.7.5", optional = true }
|
|
49
|
-
js-sys = "0.3"
|
|
50
|
-
reqwest = { version = "0.12.22", features = ["json"] }
|
|
51
|
-
flate2 = "1.0"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
zip = "4.3.0"
|
|
1
|
+
[package]
|
|
2
|
+
name = "devalang"
|
|
3
|
+
version = "0.0.1-alpha.12"
|
|
4
|
+
authors = ["Devaloop <contact@devaloop.com>"]
|
|
5
|
+
description = "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text."
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/devaloop-labs/devalang"
|
|
8
|
+
keywords = ["music", "dsl", "audio", "cli"]
|
|
9
|
+
categories = ["command-line-utilities", "development-tools", "parser-implementations"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
homepage = "https://devalang.com"
|
|
12
|
+
documentation = "https://docs.devalang.com/"
|
|
13
|
+
license-file = "LICENSE"
|
|
14
|
+
edition = "2024"
|
|
15
|
+
|
|
16
|
+
[[bin]]
|
|
17
|
+
name = "devalang"
|
|
18
|
+
path = "rust/main.rs"
|
|
19
|
+
required-features = ["cli"]
|
|
20
|
+
|
|
21
|
+
[lib]
|
|
22
|
+
path = "rust/lib.rs"
|
|
23
|
+
crate-type = ["cdylib"]
|
|
24
|
+
|
|
25
|
+
[profile.release]
|
|
26
|
+
opt-level = "s"
|
|
27
|
+
|
|
28
|
+
[features]
|
|
29
|
+
default = ["cli"]
|
|
30
|
+
cli = ["crossterm", "indicatif", "inquire", "zip", "reqwest", "flate2", "tokio"]
|
|
31
|
+
|
|
32
|
+
[dependencies]
|
|
33
|
+
clap = { version = "4.5", features = ["derive"] }
|
|
34
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
35
|
+
serde_json = "1.0"
|
|
36
|
+
rodio = "0.17"
|
|
37
|
+
hound = "3.4.0"
|
|
38
|
+
toml = "0.8"
|
|
39
|
+
notify = "6.1"
|
|
40
|
+
fs_extra = "1.3"
|
|
41
|
+
include_dir = "0.7"
|
|
42
|
+
wasm-bindgen = "0.2"
|
|
43
|
+
serde-wasm-bindgen = "0.4"
|
|
44
|
+
nom_locate = "4.0.0"
|
|
45
|
+
chrono = "0.4"
|
|
46
|
+
crossterm = { version = "0.27", optional = true }
|
|
47
|
+
indicatif = { version = "0.17", optional = true }
|
|
48
|
+
inquire = { version = "0.7.5", optional = true }
|
|
49
|
+
js-sys = "0.3"
|
|
50
|
+
reqwest = { version = "0.12.22", optional = true, features = ["json"] }
|
|
51
|
+
flate2 = { version = "1.0", optional = true }
|
|
52
|
+
tokio = { version = "1", features = ["full"], optional = true }
|
|
53
|
+
zip = { version = "4.3.0", optional = true }
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## Version 0.0.1-alpha.12 (2025-07-21)
|
|
8
|
+
|
|
9
|
+
### 🧩 Language Features
|
|
10
|
+
|
|
11
|
+
- Implemented `trigger` effects to apply effects to triggers, allowing for more dynamic sound manipulation.
|
|
12
|
+
- Example: `.myTrigger auto { reverb: 1.0, pitch: 1.5, gain: 0.8 }`
|
|
13
|
+
|
|
14
|
+
### 🧠 Core Engine
|
|
15
|
+
|
|
16
|
+
- Moved `utils::installer` to `installer::utils` to better organize the project structure.
|
|
17
|
+
- Set CLI dependencies as optional in `Cargo.toml` to allow for a cleaner build without CLI features.
|
|
18
|
+
- Patched `@load` relative path resolution to ensure correct loading of external resources.
|
|
19
|
+
- Patched `trigger` statement that was not correctly parsed when using namespaced banks of sounds.
|
|
20
|
+
|
|
21
|
+
### 🧩 Web Assembly
|
|
22
|
+
|
|
23
|
+
- Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
|
|
24
|
+
|
|
7
25
|
## Version 0.0.1-alpha.11 (2025-07-20)
|
|
8
26
|
|
|
9
27
|
### 📖 Documentation
|
package/examples/index.deva
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
@load "./samples/kick-808.wav" as kickCustom
|
|
8
8
|
@load "./samples/hat-808.wav" as hatCustom
|
|
9
9
|
|
|
10
|
-
bpm tempo
|
|
10
|
+
# bpm tempo
|
|
11
11
|
|
|
12
|
-
group myTrack:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# group myTrack:
|
|
13
|
+
# spawn myLoop
|
|
14
|
+
# spawn myLead
|
|
15
15
|
|
|
16
|
-
call myTrack
|
|
16
|
+
# call myTrack
|
|
17
|
+
|
|
18
|
+
.kickCustom duration { gain: 1, pitch: 1.25 }
|
package/out-tsc/bin/devalang.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devaloop/devalang",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.1-alpha.
|
|
4
|
+
"version": "0.0.1-alpha.12",
|
|
5
5
|
"description": "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text.",
|
|
6
6
|
"main": "out-tsc/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"prepublish": "cargo build --release && npm run script:postbuild",
|
|
12
12
|
"rust:dev:build": "cargo run build --entry examples --output output",
|
|
13
13
|
"rust:dev:check": "cargo run check --entry examples --output output",
|
|
14
|
+
"rust:dev:play": "cargo run play --entry examples --output output --repeat",
|
|
14
15
|
"rust:wasm:web": "wasm-pack build --target=web --no-default-features",
|
|
15
16
|
"rust:wasm:node": "wasm-pack build --target=nodejs --no-default-features",
|
|
16
17
|
"script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
|
package/project-version.json
CHANGED
|
@@ -3,6 +3,7 @@ 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,7 +168,7 @@ impl AudioEngine {
|
|
|
167
168
|
filepath: &str,
|
|
168
169
|
time_secs: f32,
|
|
169
170
|
dur_sec: f32,
|
|
170
|
-
effects: Option<HashMap<String,
|
|
171
|
+
effects: Option<HashMap<String, Value>>
|
|
171
172
|
) {
|
|
172
173
|
if filepath.is_empty() {
|
|
173
174
|
eprintln!("❌ Empty file path provided for audio sample.");
|
|
@@ -197,9 +198,26 @@ impl AudioEngine {
|
|
|
197
198
|
eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
|
|
198
199
|
return;
|
|
199
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;
|
|
200
216
|
}
|
|
201
217
|
|
|
202
|
-
let file = BufReader::new(
|
|
218
|
+
let file = BufReader::new(
|
|
219
|
+
File::open(&resolved_path).expect(&format!("Failed to open audio file {}", filepath))
|
|
220
|
+
);
|
|
203
221
|
let decoder = Decoder::new(file).expect("Failed to decode audio file");
|
|
204
222
|
|
|
205
223
|
// Mono or stereo reading possible here, we will duplicate in L/R
|
|
@@ -211,7 +229,7 @@ impl AudioEngine {
|
|
|
211
229
|
return;
|
|
212
230
|
}
|
|
213
231
|
|
|
214
|
-
//
|
|
232
|
+
// Pad the buffer to ensure it can accommodate the new samples
|
|
215
233
|
let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
216
234
|
let required_len = offset + samples.len() * (CHANNELS as usize);
|
|
217
235
|
let padded_required_len = if required_len % 2 == 1 {
|
|
@@ -221,21 +239,116 @@ impl AudioEngine {
|
|
|
221
239
|
};
|
|
222
240
|
|
|
223
241
|
self.buffer.resize(padded_required_len, 0);
|
|
224
|
-
|
|
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
|
+
}
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
fn pad_samples(
|
|
251
|
+
fn pad_samples(
|
|
252
|
+
&mut self,
|
|
253
|
+
samples: &[i16],
|
|
254
|
+
time_secs: f32,
|
|
255
|
+
effects_map: Option<HashMap<String, Value>>
|
|
256
|
+
) {
|
|
228
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;
|
|
229
301
|
|
|
230
302
|
for (i, &sample) in samples.iter().enumerate() {
|
|
231
|
-
|
|
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;
|
|
232
345
|
|
|
233
346
|
let left_pos = offset + i * 2;
|
|
234
347
|
let right_pos = left_pos + 1;
|
|
235
348
|
|
|
236
349
|
if right_pos < self.buffer.len() {
|
|
237
|
-
self.buffer[left_pos] = self.buffer[left_pos].saturating_add(
|
|
238
|
-
self.buffer[right_pos] = self.buffer[right_pos].saturating_add(
|
|
350
|
+
self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
|
|
351
|
+
self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
|
|
239
352
|
}
|
|
240
353
|
}
|
|
241
354
|
}
|
|
@@ -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 },
|
|
@@ -67,8 +69,13 @@ pub fn interprete_trigger_statement(
|
|
|
67
69
|
variable_table.clone()
|
|
68
70
|
);
|
|
69
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
|
+
|
|
70
78
|
let mut updated_engine = audio_engine.clone();
|
|
71
|
-
updated_engine.insert_sample(&src, cursor_time, duration_final, None);
|
|
72
79
|
|
|
73
80
|
let new_cursor_time = cursor_time + duration_final;
|
|
74
81
|
let new_max_end_time = new_cursor_time.max(max_end_time);
|
|
@@ -100,3 +107,17 @@ fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) ->
|
|
|
100
107
|
|
|
101
108
|
current
|
|
102
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
|
+
}
|
|
@@ -206,9 +206,35 @@ impl Parser {
|
|
|
206
206
|
Value::String(token.lexeme.clone())
|
|
207
207
|
}
|
|
208
208
|
TokenKind::Number => {
|
|
209
|
-
|
|
210
|
-
|
|
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))
|
|
211
236
|
}
|
|
237
|
+
|
|
212
238
|
TokenKind::Identifier => {
|
|
213
239
|
self.advance();
|
|
214
240
|
Value::Identifier(token.lexeme.clone())
|
|
@@ -15,15 +15,10 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
15
15
|
// Parse namespaced identifier: .808.kick.snare
|
|
16
16
|
let mut parts = Vec::new();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
let Some(token) = parser.peek_clone() else {
|
|
20
|
-
break;
|
|
21
|
-
};
|
|
22
|
-
|
|
18
|
+
while let Some(token) = parser.peek_clone() {
|
|
23
19
|
match token.kind {
|
|
24
|
-
// Stop if we encounter a likely duration keyword
|
|
25
20
|
TokenKind::Number => {
|
|
26
|
-
//
|
|
21
|
+
// Stop if it's part of a duration
|
|
27
22
|
if let Some(TokenKind::Slash) = parser.peek_nth_kind(1) {
|
|
28
23
|
break;
|
|
29
24
|
}
|
|
@@ -33,7 +28,11 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
TokenKind::Identifier => {
|
|
36
|
-
// Stop if
|
|
31
|
+
// Stop parsing entity name if next token is ':' or if already have one ident and current might be a param
|
|
32
|
+
if parts.len() >= 1 {
|
|
33
|
+
break; // we've already got the entity
|
|
34
|
+
}
|
|
35
|
+
|
|
37
36
|
if token.lexeme == "auto" {
|
|
38
37
|
break;
|
|
39
38
|
}
|
|
@@ -43,7 +42,7 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
TokenKind::Dot => {
|
|
46
|
-
parser.advance(); //
|
|
45
|
+
parser.advance(); // continue chaining
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
_ => {
|
|
@@ -52,7 +51,7 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
let entity = parts.join(".");
|
|
54
|
+
let entity = if parts.len() == 1 { parts[0].clone() } else { parts[..=1].join(".") };
|
|
56
55
|
|
|
57
56
|
if entity.is_empty() {
|
|
58
57
|
return Statement {
|
package/rust/installer/bank.rs
CHANGED
|
@@ -2,7 +2,7 @@ use std::path::{ Path, PathBuf };
|
|
|
2
2
|
use crate::{
|
|
3
3
|
common::cdn::get_cdn_url,
|
|
4
4
|
config::loader::{ add_bank_to_config, load_config },
|
|
5
|
-
utils::
|
|
5
|
+
installer::utils::{ download_file, extract_archive },
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
pub async fn install_bank(name: &str, target_dir: &Path) -> Result<(), String> {
|
package/rust/installer/mod.rs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
pub mod bank;
|
|
1
|
+
pub mod bank;
|
|
2
|
+
pub mod utils;
|
package/rust/lib.rs
CHANGED
package/rust/utils/mod.rs
CHANGED
|
File without changes
|