@devaloop/devalang 0.0.1-alpha.14 → 0.0.1-alpha.15

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 (90) hide show
  1. package/.devalang +8 -8
  2. package/Cargo.toml +2 -2
  3. package/README.md +31 -14
  4. package/docs/CHANGELOG.md +59 -0
  5. package/docs/ROADMAP.md +1 -1
  6. package/examples/automation.deva +44 -0
  7. package/examples/index.deva +41 -25
  8. package/examples/plugin.deva +15 -0
  9. package/out-tsc/bin/devalang.exe +0 -0
  10. package/package.json +1 -1
  11. package/project-version.json +3 -3
  12. package/rust/cli/bank.rs +14 -15
  13. package/rust/cli/check.rs +1 -1
  14. package/rust/cli/play.rs +1 -1
  15. package/rust/cli/update.rs +1 -1
  16. package/rust/common/api.rs +3 -6
  17. package/rust/common/cdn.rs +3 -6
  18. package/rust/common/sso.rs +3 -6
  19. package/rust/core/audio/engine.rs +215 -74
  20. package/rust/core/audio/evaluator.rs +101 -0
  21. package/rust/core/audio/interpreter/arrow_call.rs +27 -1
  22. package/rust/core/audio/interpreter/automate.rs +18 -0
  23. package/rust/core/audio/interpreter/call.rs +2 -2
  24. package/rust/core/audio/interpreter/condition.rs +3 -3
  25. package/rust/core/audio/interpreter/driver.rs +49 -16
  26. package/rust/core/audio/interpreter/let_.rs +14 -7
  27. package/rust/core/audio/interpreter/loop_.rs +39 -6
  28. package/rust/core/audio/interpreter/mod.rs +2 -1
  29. package/rust/core/audio/interpreter/sleep.rs +2 -4
  30. package/rust/core/audio/interpreter/spawn.rs +2 -2
  31. package/rust/core/audio/loader/trigger.rs +2 -5
  32. package/rust/core/audio/mod.rs +2 -1
  33. package/rust/core/audio/renderer.rs +1 -1
  34. package/rust/core/audio/special/easing.rs +120 -0
  35. package/rust/core/audio/special/env.rs +41 -0
  36. package/rust/core/audio/special/math.rs +92 -0
  37. package/rust/core/audio/special/mod.rs +9 -0
  38. package/rust/core/audio/special/modulator.rs +120 -0
  39. package/rust/core/debugger/store.rs +1 -1
  40. package/rust/core/error/mod.rs +4 -1
  41. package/rust/core/lexer/handler/arrow.rs +60 -9
  42. package/rust/core/lexer/handler/at.rs +4 -4
  43. package/rust/core/lexer/handler/brace.rs +8 -8
  44. package/rust/core/lexer/handler/colon.rs +4 -4
  45. package/rust/core/lexer/handler/comment.rs +2 -2
  46. package/rust/core/lexer/handler/dot.rs +4 -4
  47. package/rust/core/lexer/handler/driver.rs +42 -13
  48. package/rust/core/lexer/handler/identifier.rs +5 -4
  49. package/rust/core/lexer/handler/newline.rs +1 -1
  50. package/rust/core/lexer/handler/number.rs +3 -3
  51. package/rust/core/lexer/handler/operator.rs +3 -1
  52. package/rust/core/lexer/handler/parenthesis.rs +8 -8
  53. package/rust/core/lexer/handler/slash.rs +5 -5
  54. package/rust/core/lexer/handler/string.rs +1 -1
  55. package/rust/core/lexer/mod.rs +1 -1
  56. package/rust/core/lexer/token.rs +3 -0
  57. package/rust/core/parser/driver.rs +94 -12
  58. package/rust/core/parser/handler/arrow_call.rs +105 -89
  59. package/rust/core/parser/handler/at.rs +1 -1
  60. package/rust/core/parser/handler/dot.rs +3 -3
  61. package/rust/core/parser/handler/identifier/automate.rs +194 -0
  62. package/rust/core/parser/handler/identifier/function.rs +2 -3
  63. package/rust/core/parser/handler/identifier/let_.rs +16 -0
  64. package/rust/core/parser/handler/identifier/mod.rs +14 -10
  65. package/rust/core/parser/handler/identifier/print.rs +29 -0
  66. package/rust/core/parser/handler/identifier/sleep.rs +1 -1
  67. package/rust/core/parser/handler/identifier/synth.rs +7 -9
  68. package/rust/core/parser/handler/loop_.rs +60 -43
  69. package/rust/core/parser/statement.rs +5 -0
  70. package/rust/core/preprocessor/loader.rs +1 -1
  71. package/rust/core/preprocessor/processor.rs +4 -4
  72. package/rust/core/preprocessor/resolver/bank.rs +1 -2
  73. package/rust/core/preprocessor/resolver/call.rs +19 -18
  74. package/rust/core/preprocessor/resolver/driver.rs +7 -5
  75. package/rust/core/preprocessor/resolver/function.rs +3 -13
  76. package/rust/core/preprocessor/resolver/loop_.rs +31 -1
  77. package/rust/core/preprocessor/resolver/spawn.rs +3 -22
  78. package/rust/core/preprocessor/resolver/tempo.rs +1 -1
  79. package/rust/core/preprocessor/resolver/trigger.rs +2 -3
  80. package/rust/core/preprocessor/resolver/value.rs +6 -12
  81. package/rust/core/shared/bank.rs +1 -1
  82. package/rust/core/utils/path.rs +1 -1
  83. package/rust/core/utils/validation.rs +0 -1
  84. package/rust/installer/bank.rs +1 -1
  85. package/rust/installer/plugin.rs +2 -2
  86. package/rust/main.rs +0 -1
  87. package/rust/utils/error.rs +51 -0
  88. package/rust/utils/logger.rs +4 -0
  89. package/rust/utils/mod.rs +1 -44
  90. package/rust/utils/spinner.rs +1 -1
@@ -107,10 +107,11 @@ impl Parser {
107
107
  tokens: Vec<Token>,
108
108
  global_store: &mut GlobalStore
109
109
  ) -> Vec<Statement> {
110
- // Filtrer les tokens Whitespace et Newline avant parsing
110
+ // Filtrer uniquement les espaces, mais conserver les Newline car
111
+ // certaines constructions (ex: print ...) s'appuient sur la fin de ligne.
111
112
  self.tokens = tokens
112
113
  .into_iter()
113
- .filter(|t| t.kind != TokenKind::Whitespace && t.kind != TokenKind::Newline)
114
+ .filter(|t| t.kind != TokenKind::Whitespace)
114
115
  .collect();
115
116
  self.token_index = 0;
116
117
 
@@ -197,12 +198,13 @@ impl Parser {
197
198
  let mut map = std::collections::HashMap::new();
198
199
 
199
200
  while !self.check_token(TokenKind::RBrace) && !self.is_eof() {
200
- // Skip newlines, whitespace, indent, dedent before the key
201
+ // Skip separators and formatting before the key
201
202
  while
202
203
  self.check_token(TokenKind::Newline) ||
203
204
  self.check_token(TokenKind::Whitespace) ||
204
205
  self.check_token(TokenKind::Indent) ||
205
- self.check_token(TokenKind::Dedent)
206
+ self.check_token(TokenKind::Dedent) ||
207
+ self.check_token(TokenKind::Comma)
206
208
  {
207
209
  self.advance();
208
210
  }
@@ -236,8 +238,14 @@ impl Parser {
236
238
  break;
237
239
  }
238
240
 
239
- // Skip newlines and whitespace before value
240
- while self.check_token(TokenKind::Newline) || self.check_token(TokenKind::Whitespace) {
241
+ // Skip separators and formatting before value
242
+ while
243
+ self.check_token(TokenKind::Newline) ||
244
+ self.check_token(TokenKind::Whitespace) ||
245
+ self.check_token(TokenKind::Indent) ||
246
+ self.check_token(TokenKind::Dedent) ||
247
+ self.check_token(TokenKind::Comma)
248
+ {
241
249
  self.advance();
242
250
  }
243
251
 
@@ -291,6 +299,11 @@ impl Parser {
291
299
  };
292
300
 
293
301
  map.insert(key, value);
302
+
303
+ // Optionally skip a trailing comma after the value
304
+ while self.check_token(TokenKind::Comma) || self.check_token(TokenKind::Whitespace) || self.check_token(TokenKind::Newline) {
305
+ self.advance();
306
+ }
294
307
  }
295
308
 
296
309
  if !self.match_token(TokenKind::RBrace) {
@@ -300,6 +313,79 @@ impl Parser {
300
313
  Some(Value::Map(map))
301
314
  }
302
315
 
316
+ // Parse an array value like [1, 2, 3] or ["a", b]
317
+ pub fn parse_array_value(&mut self) -> Option<Value> {
318
+ if !self.match_token(TokenKind::LBracket) {
319
+ return None;
320
+ }
321
+
322
+ let mut arr: Vec<Value> = Vec::new();
323
+
324
+ while !self.check_token(TokenKind::RBracket) && !self.is_eof() {
325
+ // Skip formatting tokens
326
+ while
327
+ self.check_token(TokenKind::Newline) ||
328
+ self.check_token(TokenKind::Whitespace) ||
329
+ self.check_token(TokenKind::Indent) ||
330
+ self.check_token(TokenKind::Dedent) ||
331
+ self.check_token(TokenKind::Comma)
332
+ {
333
+ self.advance();
334
+ }
335
+
336
+ if self.check_token(TokenKind::RBracket) {
337
+ break;
338
+ }
339
+
340
+ if let Some(token) = self.peek_clone() {
341
+ let value = match token.kind {
342
+ TokenKind::String => { self.advance(); Value::String(token.lexeme.clone()) }
343
+ TokenKind::Number => {
344
+ // Support simple decimals split as number '.' number
345
+ let mut number_str = token.lexeme.clone();
346
+ self.advance();
347
+ if let Some(dot) = self.peek_clone() {
348
+ if dot.kind == TokenKind::Dot {
349
+ if let Some(next) = self.peek_nth(1).cloned() {
350
+ if next.kind == TokenKind::Number {
351
+ self.advance(); // consume dot
352
+ self.advance(); // consume next number
353
+ number_str.push('.');
354
+ number_str.push_str(&next.lexeme);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ Value::Number(number_str.parse::<f32>().unwrap_or(0.0))
360
+ }
361
+ TokenKind::Identifier => { self.advance(); Value::Identifier(token.lexeme.clone()) }
362
+ TokenKind::LBrace => {
363
+ // Allow inline maps inside arrays
364
+ if let Some(v) = self.parse_map_value() { v } else { Value::Null }
365
+ }
366
+ TokenKind::LBracket => {
367
+ // Nested arrays
368
+ if let Some(v) = self.parse_array_value() { v } else { Value::Null }
369
+ }
370
+ _ => { self.advance(); Value::Null }
371
+ };
372
+
373
+ // Only push non-null (retain alignment with permissive parsing)
374
+ if value != Value::Null { arr.push(value); }
375
+
376
+ // Optional trailing comma handled by the skipper at loop start
377
+ } else {
378
+ break;
379
+ }
380
+ }
381
+
382
+ if !self.match_token(TokenKind::RBracket) {
383
+ println!("Expected ']' at end of array");
384
+ }
385
+
386
+ Some(Value::Array(arr))
387
+ }
388
+
303
389
  pub fn peek(&self) -> Option<&Token> {
304
390
  self.tokens.get(self.token_index)
305
391
  }
@@ -333,14 +419,10 @@ impl Parser {
333
419
  pub fn collect_until<F>(&mut self, condition: F) -> Vec<Token> where F: Fn(&Token) -> bool {
334
420
  let mut collected = Vec::new();
335
421
  while let Some(token) = self.peek() {
336
- if token.kind == TokenKind::Newline || token.kind == TokenKind::Indent {
337
- self.advance(); // Skip newlines and indents
338
- continue;
339
- }
340
- if token.kind == TokenKind::EOF {
422
+ if condition(token) {
341
423
  break;
342
424
  }
343
- if condition(token) {
425
+ if token.kind == TokenKind::EOF {
344
426
  break;
345
427
  }
346
428
  collected.push(self.advance().unwrap().clone());
@@ -5,7 +5,104 @@ use crate::core::{
5
5
  store::global::GlobalStore,
6
6
  };
7
7
 
8
- pub fn parse_arrow_call(parser: &mut Parser, global_store: &mut GlobalStore) -> Statement {
8
+ fn parse_map_literal(parser: &mut Parser) -> Value {
9
+ // Assumes '{' has already been consumed by caller
10
+ let mut map = std::collections::HashMap::new();
11
+ loop {
12
+ let Some(inner_token) = parser.peek_clone() else { break; };
13
+
14
+ match inner_token.kind {
15
+ TokenKind::RBrace => {
16
+ parser.advance(); // consume '}'
17
+ break;
18
+ }
19
+ TokenKind::Newline | TokenKind::Comma => {
20
+ parser.advance();
21
+ continue;
22
+ }
23
+ _ => {}
24
+ }
25
+
26
+ // Key
27
+ parser.advance();
28
+ let key = inner_token.lexeme.clone();
29
+
30
+ // Expect ':'
31
+ if let Some(colon_token) = parser.peek_clone() {
32
+ if colon_token.kind == TokenKind::Colon {
33
+ parser.advance(); // consume ':'
34
+
35
+ // Value
36
+ if let Some(value_token) = parser.peek_clone() {
37
+ match value_token.kind {
38
+ TokenKind::LBrace => {
39
+ parser.advance(); // consume '{'
40
+ let nested = parse_map_literal(parser);
41
+ map.insert(key, nested);
42
+ }
43
+ TokenKind::Identifier => {
44
+ parser.advance();
45
+ let v = if value_token.lexeme == "true" {
46
+ Value::Boolean(true)
47
+ } else if value_token.lexeme == "false" {
48
+ Value::Boolean(false)
49
+ } else {
50
+ Value::Identifier(value_token.lexeme.clone())
51
+ };
52
+ map.insert(key, v);
53
+ }
54
+ TokenKind::String => {
55
+ parser.advance();
56
+ map.insert(key, Value::String(value_token.lexeme.clone()));
57
+ }
58
+ TokenKind::Number => {
59
+ parser.advance();
60
+ // Beat fraction support: NUMBER '/' NUMBER
61
+ if let Some(TokenKind::Slash) = parser.peek_kind() {
62
+ parser.advance(); // '/'
63
+ if let Some(den) = parser.peek_clone() {
64
+ if den.kind == TokenKind::Number {
65
+ parser.advance();
66
+ let beat = format!("{}/{}", value_token.lexeme, den.lexeme);
67
+ map.insert(key, Value::Beat(beat));
68
+ continue;
69
+ }
70
+ }
71
+ }
72
+ // Decimal support NUMBER '.' NUMBER
73
+ if let Some(next) = parser.peek_clone() {
74
+ if next.kind == TokenKind::Dot {
75
+ parser.advance(); // '.'
76
+ if let Some(after) = parser.peek_clone() {
77
+ if after.kind == TokenKind::Number {
78
+ parser.advance();
79
+ let combined = format!("{}.{}", value_token.lexeme, after.lexeme);
80
+ map.insert(key, Value::Number(combined.parse::<f32>().unwrap_or(0.0)));
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ map.insert(key, Value::Number(value_token.lexeme.parse::<f32>().unwrap_or(0.0)));
87
+ }
88
+ TokenKind::Boolean => {
89
+ parser.advance();
90
+ map.insert(key, Value::Boolean(value_token.lexeme.parse::<bool>().unwrap_or(false)));
91
+ }
92
+ _ => {
93
+ // Unknown value type, consume and store Unknown
94
+ parser.advance();
95
+ map.insert(key, Value::Unknown);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ Value::Map(map)
103
+ }
104
+
105
+ pub fn parse_arrow_call(parser: &mut Parser, _global_store: &mut GlobalStore) -> Statement {
9
106
  let Some(target_token) = parser.peek_clone() else {
10
107
  return Statement::unknown();
11
108
  };
@@ -94,93 +191,16 @@ pub fn parse_arrow_call(parser: &mut Parser, global_store: &mut GlobalStore) ->
94
191
 
95
192
  parser.advance();
96
193
 
97
- let value = match token.kind {
194
+ let value = match token.kind {
98
195
  TokenKind::Identifier => Value::Identifier(token.lexeme.clone()),
99
196
  TokenKind::String => Value::String(token.lexeme.clone()),
100
197
  TokenKind::Number => Value::Number(token.lexeme.parse::<f32>().unwrap_or(0.0)),
101
198
  TokenKind::LBrace => {
102
- // Handle map literal
103
- let mut map = std::collections::HashMap::new();
104
- while let Some(inner_token) = parser.peek_clone() {
105
- if inner_token.kind == TokenKind::RBrace {
106
- parser.advance(); // consume RBrace
107
- break;
108
- }
109
- if inner_token.kind == TokenKind::Newline || inner_token.kind == TokenKind::EOF {
110
- break;
111
- }
112
- parser.advance(); // consume key token
113
- let key = inner_token.lexeme.clone();
114
- if let Some(colon_token) = parser.peek_clone() {
115
- if colon_token.kind == TokenKind::Colon {
116
- parser.advance(); // consume colon
117
- if let Some(value_token) = parser.peek_clone() {
118
- parser.advance(); // consume value token
119
- let value = match value_token.kind {
120
- TokenKind::Identifier => {
121
- // Interpret bare true/false as booleans
122
- if value_token.lexeme == "true" {
123
- Value::Boolean(true)
124
- } else if value_token.lexeme == "false" {
125
- Value::Boolean(false)
126
- } else {
127
- Value::Identifier(value_token.lexeme.clone())
128
- }
129
- },
130
- TokenKind::String => Value::String(value_token.lexeme.clone()),
131
- TokenKind::Number => {
132
- // Support decimals (e.g., 0.8) and beats (e.g., 1/4)
133
- if let Some(TokenKind::Slash) = parser.peek_kind() {
134
- // Beat fraction
135
- parser.advance(); // consume '/'
136
- if let Some(denominator_token) = parser.peek_clone() {
137
- if denominator_token.kind == TokenKind::Number {
138
- parser.advance(); // consume denominator
139
- let denominator = denominator_token.lexeme.clone();
140
- Value::Beat(format!("{}/{}", value_token.lexeme, denominator))
141
- } else {
142
- Value::Unknown
143
- }
144
- } else {
145
- Value::Unknown
146
- }
147
- } else if let Some(next) = parser.peek_clone() {
148
- // Decimal number handling: NUMBER '.' NUMBER -> f32
149
- if next.kind == TokenKind::Dot {
150
- // consume '.'
151
- parser.advance();
152
- if let Some(after_dot) = parser.peek_clone() {
153
- if after_dot.kind == TokenKind::Number {
154
- parser.advance(); // consume fractional digits
155
- let combined = format!("{}.{}", value_token.lexeme, after_dot.lexeme);
156
- Value::Number(combined.parse::<f32>().unwrap_or(0.0))
157
- } else {
158
- // Lone dot without number, fallback to integer part
159
- Value::Number(value_token.lexeme.parse::<f32>().unwrap_or(0.0))
160
- }
161
- } else {
162
- Value::Number(value_token.lexeme.parse::<f32>().unwrap_or(0.0))
163
- }
164
- } else {
165
- // Regular integer number
166
- Value::Number(value_token.lexeme.parse::<f32>().unwrap_or(0.0))
167
- }
168
- } else {
169
- Value::Number(value_token.lexeme.parse::<f32>().unwrap_or(0.0))
170
- }
171
- }
172
- TokenKind::Boolean =>
173
- Value::Boolean(
174
- value_token.lexeme.parse::<bool>().unwrap_or(false)
175
- ),
176
- _ => Value::Unknown,
177
- };
178
- map.insert(key, value);
179
- }
180
- }
181
- }
182
- }
183
- Value::Map(map)
199
+ // Handle map literal (supports nested maps)
200
+ let map_val = parse_map_literal(parser);
201
+ // We consumed the matching '}', so outer map_depth should be decremented
202
+ // if the caller tracks it.
203
+ map_val
184
204
  }
185
205
  _ => Value::Unknown,
186
206
  };
@@ -188,11 +208,7 @@ pub fn parse_arrow_call(parser: &mut Parser, global_store: &mut GlobalStore) ->
188
208
  args.push(value);
189
209
 
190
210
  // Stop if we reach the end of the statement
191
- if
192
- paren_depth == 0 &&
193
- map_depth == 0 &&
194
- (token.kind == TokenKind::RParen || token.kind == TokenKind::RBrace)
195
- {
211
+ if paren_depth == 0 && (token.kind == TokenKind::RParen || token.kind == TokenKind::RBrace) {
196
212
  break;
197
213
  }
198
214
  }
@@ -4,7 +4,7 @@ use crate::core::{
4
4
  shared::value::Value,
5
5
  store::global::GlobalStore,
6
6
  };
7
- pub fn parse_at_token(parser: &mut Parser, global_store: &mut GlobalStore) -> Statement {
7
+ pub fn parse_at_token(parser: &mut Parser, _global_store: &mut GlobalStore) -> Statement {
8
8
  parser.advance(); // consume '@'
9
9
 
10
10
  let Some(token) = parser.peek_clone() else {
@@ -19,7 +19,7 @@ pub fn parse_dot_token(
19
19
  let current_line = dot_token.line;
20
20
 
21
21
  while let Some(token) = parser.peek_clone() {
22
- // Ne jamais traverser une nouvelle ligne
22
+ // Never cross a newline
23
23
  if token.line != current_line {
24
24
  break;
25
25
  }
@@ -27,7 +27,7 @@ pub fn parse_dot_token(
27
27
  TokenKind::Identifier | TokenKind::Number => {
28
28
  parts.push(token.lexeme.clone());
29
29
  parser.advance();
30
- // Le séparateur doit être un '.' sur la même ligne, sinon on s'arrête
30
+ // The separator must be a '.' on the same line, otherwise stop
31
31
  if let Some(next) = parser.peek_clone() {
32
32
  if next.line != current_line || next.kind != TokenKind::Dot {
33
33
  break;
@@ -61,7 +61,7 @@ pub fn parse_dot_token(
61
61
  let mut value = Value::Null;
62
62
 
63
63
  if let Some(token) = parser.peek_clone() {
64
- // La durée et la map d'effets ne sont valides que sur la même ligne
64
+ // Duration and effects map are only valid on the same line
65
65
  if token.line == current_line {
66
66
  match token.kind {
67
67
  TokenKind::Number => {
@@ -0,0 +1,194 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::core::{
4
+ lexer::token::{Token, TokenKind},
5
+ parser::{driver::Parser, statement::{Statement, StatementKind}},
6
+ shared::value::Value,
7
+ store::global::GlobalStore,
8
+ };
9
+
10
+ // Grammar:
11
+ // automate <identifier>:
12
+ // param <name> { <percent>% = <number> ... }
13
+ // Produces StatementKind::Automate with value map:
14
+ // { target: Identifier, params: Map<paramName, Map<percent, Number>> }
15
+ pub fn parse_automate_token(
16
+ parser: &mut Parser,
17
+ current_token: Token,
18
+ _global_store: &mut GlobalStore,
19
+ ) -> Statement {
20
+ parser.advance(); // consume 'automate'
21
+
22
+ // Expect target identifier
23
+ let Some(target_token) = parser.peek_clone() else {
24
+ return Statement::error(current_token, "Expected target after 'automate'".to_string());
25
+ };
26
+
27
+ if target_token.kind != TokenKind::Identifier && target_token.kind != TokenKind::String {
28
+ return Statement::error(target_token, "Expected valid target after 'automate'".to_string());
29
+ }
30
+ parser.advance(); // consume target
31
+
32
+ // Expect ':'
33
+ let Some(colon_token) = parser.peek_clone() else {
34
+ return Statement::error(target_token, "Expected ':' after automate target".to_string());
35
+ };
36
+ if colon_token.kind != TokenKind::Colon {
37
+ return Statement::error(colon_token, "Expected ':' after automate target".to_string());
38
+ }
39
+ parser.advance(); // consume ':'
40
+
41
+ let base_indent = current_token.indent;
42
+
43
+ // Collect tokens inside block (indented > base_indent)
44
+ let mut index = parser.token_index;
45
+ let mut tokens_inside = Vec::new();
46
+ while index < parser.tokens.len() {
47
+ let tok = parser.tokens[index].clone();
48
+ if tok.indent <= base_indent && tok.kind != TokenKind::Newline {
49
+ break;
50
+ }
51
+ tokens_inside.push(tok);
52
+ index += 1;
53
+ }
54
+ parser.token_index = index;
55
+
56
+ // Now parse block manually to capture 'param' entries without reusing general parser kinds
57
+ let mut local = Parser {
58
+ resolve_modules: parser.resolve_modules,
59
+ tokens: tokens_inside,
60
+ token_index: 0,
61
+ current_module: parser.current_module.clone(),
62
+ previous: None,
63
+ };
64
+
65
+ let mut params: HashMap<String, Value> = HashMap::new();
66
+
67
+ while let Some(tok) = local.peek_clone() {
68
+ match tok.kind {
69
+ TokenKind::Identifier if tok.lexeme == "param" => {
70
+ local.advance(); // consume 'param'
71
+ // param name
72
+ let Some(name_tok) = local.peek_clone() else {
73
+ return Statement::error(tok, "Expected parameter name after 'param'".to_string());
74
+ };
75
+ if name_tok.kind != TokenKind::Identifier && name_tok.kind != TokenKind::String {
76
+ return Statement::error(name_tok, "Expected valid parameter name".to_string());
77
+ }
78
+ local.advance(); // consume name
79
+
80
+ // Expect '{'
81
+ if !local.match_token(TokenKind::LBrace) {
82
+ return Statement::error(name_tok, "Expected '{' to start parameter block".to_string());
83
+ }
84
+
85
+ // Collect entries like: 0% = 0.0
86
+ let mut envelope: HashMap<String, Value> = HashMap::new();
87
+ while let Some(inner) = local.peek_clone() {
88
+ if inner.kind == TokenKind::RBrace { local.advance(); break; }
89
+ // Skip formatting tokens inside the param block
90
+ if matches!(inner.kind, TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent | TokenKind::Comma) {
91
+ local.advance();
92
+ continue;
93
+ }
94
+
95
+ // Read percentage token: could be number followed by '%' as Dot or Identifier? '%' not defined.
96
+ // Our lexer has no Percent token, so accept either Number or Identifier containing e.g. '0%'.
97
+ let percent_token = inner.clone();
98
+ local.advance();
99
+
100
+ let percent_key = percent_token.lexeme.clone();
101
+
102
+ // Expect '='
103
+ // Skip any stray formatting between key and '='
104
+ while let Some(t) = local.peek_kind() {
105
+ if matches!(t, TokenKind::Indent | TokenKind::Dedent | TokenKind::Newline) { local.advance(); continue; }
106
+ break;
107
+ }
108
+ if !local.match_token(TokenKind::Equals) {
109
+ return Statement::error(percent_token, "Expected '=' in param entry".to_string());
110
+ }
111
+
112
+ // Read value (number or identifier)
113
+ // Skip formatting before value
114
+ while let Some(t) = local.peek_kind() {
115
+ if matches!(t, TokenKind::Indent | TokenKind::Dedent | TokenKind::Newline) { local.advance(); continue; }
116
+ break;
117
+ }
118
+
119
+ let value = if let Some(vtok) = local.peek_clone() {
120
+ match vtok.kind {
121
+ // Handle negative numbers where '-' is lexed as Arrow
122
+ TokenKind::Arrow => {
123
+ // Check if next token is a number
124
+ let mut num_str = String::from("-");
125
+ local.advance(); // consume '-'
126
+ if let Some(ntok) = local.peek_clone() {
127
+ if ntok.kind == TokenKind::Number {
128
+ num_str.push_str(&ntok.lexeme);
129
+ local.advance(); // consume number
130
+ if let Some(dot) = local.peek_clone() {
131
+ if dot.kind == TokenKind::Dot {
132
+ local.advance();
133
+ if let Some(frac) = local.peek_clone() {
134
+ if frac.kind == TokenKind::Number {
135
+ num_str.push('.');
136
+ num_str.push_str(&frac.lexeme);
137
+ local.advance();
138
+ }
139
+ }
140
+ }
141
+ }
142
+ Value::Number(num_str.parse::<f32>().unwrap_or(0.0))
143
+ } else {
144
+ Value::Unknown
145
+ }
146
+ } else {
147
+ Value::Unknown
148
+ }
149
+ }
150
+ TokenKind::Number => {
151
+ // Possibly a float with dot
152
+ let mut number_str = vtok.lexeme.clone();
153
+ local.advance();
154
+ if let Some(dot) = local.peek_clone() {
155
+ if dot.kind == TokenKind::Dot {
156
+ local.advance();
157
+ if let Some(frac) = local.peek_clone() {
158
+ if frac.kind == TokenKind::Number {
159
+ number_str.push('.');
160
+ number_str.push_str(&frac.lexeme);
161
+ local.advance();
162
+ }
163
+ }
164
+ }
165
+ }
166
+ Value::Number(number_str.parse::<f32>().unwrap_or(0.0))
167
+ }
168
+ TokenKind::Identifier => { local.advance(); Value::Identifier(vtok.lexeme.clone()) }
169
+ TokenKind::String => { local.advance(); Value::String(vtok.lexeme.clone()) }
170
+ _ => { local.advance(); Value::Unknown }
171
+ }
172
+ } else { Value::Null };
173
+
174
+ envelope.insert(percent_key, value);
175
+ }
176
+
177
+ params.insert(name_tok.lexeme.clone(), Value::Map(envelope));
178
+ }
179
+ _ => { local.advance(); }
180
+ }
181
+ }
182
+
183
+ let mut value_map = HashMap::new();
184
+ value_map.insert("target".to_string(), Value::String(target_token.lexeme.clone()));
185
+ value_map.insert("params".to_string(), Value::Map(params));
186
+
187
+ Statement {
188
+ kind: StatementKind::Automate { target: target_token.lexeme.clone() },
189
+ value: Value::Map(value_map),
190
+ indent: current_token.indent,
191
+ line: current_token.line,
192
+ column: current_token.column,
193
+ }
194
+ }
@@ -29,7 +29,6 @@ pub fn parse_function_token(parser: &mut Parser, global_store: &mut GlobalStore)
29
29
  parser.advance(); // consume function name
30
30
 
31
31
  let mut parameters = Vec::new();
32
- let mut body = Vec::new();
33
32
 
34
33
  // Expect '('
35
34
  if parser.peek_kind() != Some(TokenKind::LParen) {
@@ -68,8 +67,8 @@ pub fn parse_function_token(parser: &mut Parser, global_store: &mut GlobalStore)
68
67
  body_tokens.push(parser.advance().unwrap().clone());
69
68
  }
70
69
 
71
- // arse those tokens into block statements
72
- body = parser.parse_block(body_tokens.clone(), global_store);
70
+ // Parse those tokens into block statements
71
+ let body = parser.parse_block(body_tokens.clone(), global_store);
73
72
 
74
73
  // Skip Dedent if present
75
74
  if let Some(tok) = parser.peek() {
@@ -33,6 +33,22 @@ pub fn parse_let_token(
33
33
  return Statement::error(current_token, "Expected '=' after identifier".to_string());
34
34
  }
35
35
 
36
+ // If RHS begins with '$' or contains expression tokens ('+', '-', '*', '/', '(', '['),
37
+ // collect the rest of the line as a raw expression string.
38
+ if let Some(tok) = parser.peek_clone() {
39
+ let line = tok.line;
40
+ if tok.lexeme.starts_with('$') || matches!(tok.kind, TokenKind::Identifier | TokenKind::Number | TokenKind::LParen | TokenKind::LBracket) {
41
+ // Collect tokens until end of the current line
42
+ let collected = parser.collect_until(|t| t.line != line || matches!(t.kind, TokenKind::Newline | TokenKind::EOF));
43
+ let mut text = String::new();
44
+ for t in collected.iter() {
45
+ if matches!(t.kind, TokenKind::Newline | TokenKind::EOF) { break; }
46
+ text.push_str(&t.lexeme);
47
+ }
48
+ return Statement { kind: StatementKind::Let { name: identifier }, value: Value::String(text.trim().to_string()), indent: current_token.indent, line: current_token.line, column: current_token.column };
49
+ }
50
+ }
51
+
36
52
  let value = match parser.peek_clone() {
37
53
  Some(token) if token.kind == TokenKind::Dot => {
38
54
  let dot_stmt = parse_dot_token(parser, global_store);