@devaloop/devalang 0.0.1-alpha.10 → 0.0.1-alpha.11
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 +9 -4
- package/Cargo.toml +54 -49
- package/README.md +59 -142
- package/docs/CHANGELOG.md +42 -1
- package/docs/ROADMAP.md +1 -1
- package/docs/TODO.md +1 -1
- package/examples/bank.deva +9 -0
- package/examples/duration.deva +9 -0
- package/examples/index.deva +0 -2
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +1 -1
- package/project-version.json +3 -3
- package/rust/cli/bank.rs +455 -0
- package/rust/cli/build.rs +1 -1
- package/rust/cli/check.rs +1 -1
- package/rust/cli/driver.rs +280 -0
- package/rust/cli/install.rs +17 -0
- package/rust/cli/mod.rs +5 -200
- package/rust/cli/play.rs +1 -1
- package/rust/cli/update.rs +4 -0
- package/rust/common/cdn.rs +11 -0
- package/rust/common/mod.rs +1 -0
- package/rust/config/driver.rs +76 -0
- package/rust/config/loader.rs +98 -1
- package/rust/config/mod.rs +1 -15
- package/rust/core/audio/engine.rs +31 -3
- package/rust/core/audio/interpreter/arrow_call.rs +17 -4
- package/rust/core/audio/interpreter/trigger.rs +34 -1
- package/rust/core/audio/loader/trigger.rs +12 -0
- package/rust/core/lexer/handler/driver.rs +12 -1
- package/rust/core/lexer/handler/mod.rs +1 -0
- package/rust/core/lexer/handler/slash.rs +21 -0
- package/rust/core/lexer/token.rs +1 -0
- package/rust/core/parser/driver.rs +8 -0
- package/rust/core/parser/handler/arrow_call.rs +29 -4
- package/rust/core/parser/handler/dot.rs +103 -37
- package/rust/core/preprocessor/loader.rs +93 -14
- package/rust/core/preprocessor/resolver/driver.rs +5 -0
- package/rust/core/shared/bank.rs +21 -0
- package/rust/core/shared/duration.rs +1 -0
- package/rust/core/shared/mod.rs +2 -1
- package/rust/core/shared/value.rs +1 -0
- package/rust/installer/bank.rs +55 -0
- package/rust/installer/mod.rs +1 -0
- package/rust/lib.rs +1 -0
- package/rust/main.rs +62 -5
- package/rust/utils/installer.rs +56 -0
- package/rust/utils/mod.rs +2 -1
- package/docs/COMMANDS.md +0 -85
- package/docs/CONFIG.md +0 -30
- package/docs/SYNTAX.md +0 -230
package/rust/config/loader.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::{ fs, path::Path };
|
|
2
|
-
use
|
|
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
|
+
}
|
package/rust/config/mod.rs
CHANGED
|
@@ -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,4 +1,4 @@
|
|
|
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
|
|
|
@@ -169,9 +169,37 @@ impl AudioEngine {
|
|
|
169
169
|
dur_sec: f32,
|
|
170
170
|
effects: Option<HashMap<String, f32>>
|
|
171
171
|
) {
|
|
172
|
-
|
|
172
|
+
if filepath.is_empty() {
|
|
173
|
+
eprintln!("❌ Empty file path provided for audio sample.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let mut resolved_path = String::new();
|
|
178
|
+
|
|
179
|
+
if filepath.starts_with("devalang://") {
|
|
180
|
+
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
181
|
+
let parts = filepath.split("devalang://").collect::<Vec<&str>>();
|
|
182
|
+
let object_parts = parts.get(1).unwrap_or(&"").split("/").collect::<Vec<&str>>();
|
|
183
|
+
let object_type = object_parts.get(0).unwrap_or(&"").to_lowercase();
|
|
184
|
+
let object_dir = object_parts.get(1).unwrap_or(&"").to_string();
|
|
185
|
+
let object_name = object_parts.get(2).unwrap_or(&"").to_string();
|
|
186
|
+
|
|
187
|
+
if object_type.contains("bank") {
|
|
188
|
+
resolved_path = root
|
|
189
|
+
.join(".deva")
|
|
190
|
+
.join("bank")
|
|
191
|
+
.join(object_dir)
|
|
192
|
+
.join(format!("{}.wav", object_name))
|
|
193
|
+
.to_str()
|
|
194
|
+
.unwrap_or("")
|
|
195
|
+
.to_string();
|
|
196
|
+
} else {
|
|
197
|
+
eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
173
201
|
|
|
174
|
-
let file = BufReader::new(File::open(
|
|
202
|
+
let file = BufReader::new(File::open(resolved_path).expect("Failed to open audio file"));
|
|
175
203
|
let decoder = Decoder::new(file).expect("Failed to decode audio file");
|
|
176
204
|
|
|
177
205
|
// Mono or stereo reading possible here, we will duplicate in L/R
|
|
@@ -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(
|
|
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
|
})
|
|
@@ -14,7 +14,7 @@ pub fn interprete_trigger_statement(
|
|
|
14
14
|
max_end_time: f32
|
|
15
15
|
) -> Option<(f32, f32, AudioEngine)> {
|
|
16
16
|
if let StatementKind::Trigger { entity, duration } = &stmt.kind {
|
|
17
|
-
if let Some(trigger_val) =
|
|
17
|
+
if let Some(trigger_val) = resolve_namespaced_variable(entity, variable_table) {
|
|
18
18
|
let duration_secs = match duration {
|
|
19
19
|
Duration::Number(n) => *n,
|
|
20
20
|
|
|
@@ -41,6 +41,20 @@ pub fn interprete_trigger_statement(
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
Duration::Beat(beat_str) => {
|
|
45
|
+
// Assuming beat_str is in the format "numerator/denominator"
|
|
46
|
+
let parts: Vec<&str> = beat_str.split('/').collect();
|
|
47
|
+
|
|
48
|
+
if parts.len() != 2 {
|
|
49
|
+
eprintln!("❌ Invalid beat duration format: {}", beat_str);
|
|
50
|
+
return None;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let numerator: f32 = parts[0].parse().unwrap_or(1.0);
|
|
54
|
+
let denominator: f32 = parts[1].parse().unwrap_or(1.0);
|
|
55
|
+
numerator / denominator
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
Duration::Auto => 1.0,
|
|
45
59
|
};
|
|
46
60
|
|
|
@@ -67,3 +81,22 @@ pub fn interprete_trigger_statement(
|
|
|
67
81
|
|
|
68
82
|
None
|
|
69
83
|
}
|
|
84
|
+
|
|
85
|
+
fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) -> Option<&'a Value> {
|
|
86
|
+
let mut current: Option<&Value> = None;
|
|
87
|
+
|
|
88
|
+
for (i, part) in path.split('.').enumerate() {
|
|
89
|
+
if i == 0 {
|
|
90
|
+
current = variables.get(part);
|
|
91
|
+
} else {
|
|
92
|
+
current = match current {
|
|
93
|
+
Some(Value::Map(map)) => map.get(part),
|
|
94
|
+
_ => {
|
|
95
|
+
return None;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
current
|
|
102
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
package/rust/core/lexer/token.rs
CHANGED
|
@@ -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;
|
|
@@ -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
|
-
|
|
94
|
-
|
|
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);
|
|
@@ -6,14 +6,55 @@ use crate::core::{
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) -> Statement {
|
|
9
|
-
parser.advance(); // consume the dot
|
|
9
|
+
parser.advance(); // consume the first dot
|
|
10
10
|
|
|
11
11
|
let Some(dot_token) = parser.previous_clone() else {
|
|
12
12
|
return Statement::unknown();
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
// .kick
|
|
16
|
-
let
|
|
15
|
+
// Parse namespaced identifier: .808.kick.snare
|
|
16
|
+
let mut parts = Vec::new();
|
|
17
|
+
|
|
18
|
+
loop {
|
|
19
|
+
let Some(token) = parser.peek_clone() else {
|
|
20
|
+
break;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
match token.kind {
|
|
24
|
+
// Stop if we encounter a likely duration keyword
|
|
25
|
+
TokenKind::Number => {
|
|
26
|
+
// If there's a slash after the number, it's probably a fraction (1/4)
|
|
27
|
+
if let Some(TokenKind::Slash) = parser.peek_nth_kind(1) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
parts.push(token.lexeme.clone());
|
|
32
|
+
parser.advance();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
TokenKind::Identifier => {
|
|
36
|
+
// Stop if it's the duration keyword "auto"
|
|
37
|
+
if token.lexeme == "auto" {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
parts.push(token.lexeme.clone());
|
|
42
|
+
parser.advance();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
TokenKind::Dot => {
|
|
46
|
+
parser.advance(); // consume dot
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_ => {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let entity = parts.join(".");
|
|
56
|
+
|
|
57
|
+
if entity.is_empty() {
|
|
17
58
|
return Statement {
|
|
18
59
|
kind: StatementKind::Trigger {
|
|
19
60
|
entity: String::new(),
|
|
@@ -24,68 +65,93 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
24
65
|
line: dot_token.line,
|
|
25
66
|
column: dot_token.column,
|
|
26
67
|
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
parser.advance(); // consume entity
|
|
30
|
-
let entity = entity_token.lexeme.clone();
|
|
68
|
+
}
|
|
31
69
|
|
|
32
70
|
// Check if there's a duration
|
|
33
71
|
let next = parser.peek_clone();
|
|
34
72
|
|
|
35
73
|
let (duration, value) = match next {
|
|
36
|
-
// If no more tokens, it's just `.kick`
|
|
37
74
|
None => (Duration::Auto, Value::Null),
|
|
38
75
|
|
|
39
76
|
Some(token) =>
|
|
40
77
|
match token.kind {
|
|
41
|
-
TokenKind::Newline | TokenKind::EOF =>
|
|
78
|
+
TokenKind::Newline | TokenKind::EOF => (Duration::Auto, Value::Null),
|
|
42
79
|
|
|
43
80
|
TokenKind::Number => {
|
|
44
|
-
let
|
|
45
|
-
parser.advance(); // consume
|
|
81
|
+
let numerator = token.lexeme.clone();
|
|
82
|
+
parser.advance(); // consume numerator
|
|
83
|
+
|
|
84
|
+
if let Some(TokenKind::Slash) = parser.peek_kind() {
|
|
85
|
+
parser.advance(); // consume slash
|
|
86
|
+
|
|
87
|
+
if let Some(denominator_token) = parser.peek_clone() {
|
|
88
|
+
if denominator_token.kind == TokenKind::Number {
|
|
89
|
+
let denominator = denominator_token.lexeme.clone();
|
|
90
|
+
parser.advance(); // consume denominator
|
|
91
|
+
|
|
92
|
+
let beat_str = format!("{}/{}", numerator, denominator);
|
|
93
|
+
let beat_duration = Duration::Beat(beat_str);
|
|
94
|
+
|
|
95
|
+
let val = match parser.peek_clone() {
|
|
96
|
+
Some(param_token) if
|
|
97
|
+
param_token.kind == TokenKind::Identifier
|
|
98
|
+
=> {
|
|
99
|
+
parser.advance();
|
|
100
|
+
Value::Identifier(param_token.lexeme.clone())
|
|
101
|
+
}
|
|
102
|
+
Some(param_token) if param_token.kind == TokenKind::LBrace => {
|
|
103
|
+
parser.parse_map_value().unwrap_or(Value::Null)
|
|
104
|
+
}
|
|
105
|
+
_ => Value::Null,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return Statement {
|
|
109
|
+
kind: StatementKind::Trigger {
|
|
110
|
+
entity,
|
|
111
|
+
duration: beat_duration,
|
|
112
|
+
},
|
|
113
|
+
value: val,
|
|
114
|
+
indent: dot_token.indent,
|
|
115
|
+
line: dot_token.line,
|
|
116
|
+
column: dot_token.column,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
46
121
|
|
|
47
|
-
//
|
|
48
|
-
|
|
122
|
+
// fallback: simple numeric duration
|
|
123
|
+
let duration = parse_duration(numerator);
|
|
124
|
+
|
|
125
|
+
let val = match parser.peek_clone() {
|
|
49
126
|
Some(param_token) if param_token.kind == TokenKind::Identifier => {
|
|
50
127
|
parser.advance();
|
|
51
|
-
(
|
|
52
|
-
parse_duration(duration_lexeme),
|
|
53
|
-
Value::Identifier(param_token.lexeme.clone()),
|
|
54
|
-
)
|
|
128
|
+
Value::Identifier(param_token.lexeme.clone())
|
|
55
129
|
}
|
|
56
|
-
|
|
57
130
|
Some(param_token) if param_token.kind == TokenKind::LBrace => {
|
|
58
|
-
|
|
59
|
-
let map = parser.parse_map_value(); // Assumes you have a helper for map
|
|
60
|
-
(parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
|
|
131
|
+
parser.parse_map_value().unwrap_or(Value::Null)
|
|
61
132
|
}
|
|
133
|
+
_ => Value::Null,
|
|
134
|
+
};
|
|
62
135
|
|
|
63
|
-
|
|
64
|
-
}
|
|
136
|
+
(duration, val)
|
|
65
137
|
}
|
|
66
138
|
|
|
67
139
|
TokenKind::Identifier => {
|
|
68
140
|
let duration_lexeme = token.lexeme.clone();
|
|
69
141
|
parser.advance(); // consume duration
|
|
70
142
|
|
|
71
|
-
|
|
72
|
-
match parser.peek_clone() {
|
|
143
|
+
let val = match parser.peek_clone() {
|
|
73
144
|
Some(param_token) if param_token.kind == TokenKind::Identifier => {
|
|
74
145
|
parser.advance();
|
|
75
|
-
(
|
|
76
|
-
parse_duration(duration_lexeme),
|
|
77
|
-
Value::Identifier(param_token.lexeme.clone()),
|
|
78
|
-
)
|
|
146
|
+
Value::Identifier(param_token.lexeme.clone())
|
|
79
147
|
}
|
|
80
|
-
|
|
81
148
|
Some(param_token) if param_token.kind == TokenKind::LBrace => {
|
|
82
|
-
|
|
83
|
-
let map = parser.parse_map_value(); // Assumes you have a helper for map
|
|
84
|
-
(parse_duration(duration_lexeme), map.unwrap_or(Value::Null))
|
|
149
|
+
parser.parse_map_value().unwrap_or(Value::Null)
|
|
85
150
|
}
|
|
151
|
+
_ => Value::Null,
|
|
152
|
+
};
|
|
86
153
|
|
|
87
|
-
|
|
88
|
-
}
|
|
154
|
+
(parse_duration(duration_lexeme), val)
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
_ => (Duration::Auto, Value::Null),
|
|
@@ -104,8 +170,8 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
|
|
|
104
170
|
fn parse_duration(s: String) -> Duration {
|
|
105
171
|
if s == "auto" {
|
|
106
172
|
Duration::Auto
|
|
107
|
-
} else if s.parse::<f32>()
|
|
108
|
-
Duration::Number(
|
|
173
|
+
} else if let Ok(num) = s.parse::<f32>() {
|
|
174
|
+
Duration::Number(num)
|
|
109
175
|
} else {
|
|
110
176
|
Duration::Identifier(s)
|
|
111
177
|
}
|