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

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 (177) hide show
  1. package/.devalang +10 -8
  2. package/.github/workflows/ci.yml +92 -0
  3. package/Cargo.toml +60 -58
  4. package/README.md +32 -15
  5. package/docs/CHANGELOG.md +93 -1
  6. package/docs/CONTRIBUTING.md +101 -1
  7. package/docs/ROADMAP.md +2 -2
  8. package/docs/TODO.md +1 -1
  9. package/examples/automation.deva +42 -0
  10. package/examples/bank.deva +4 -4
  11. package/examples/events.deva +12 -0
  12. package/examples/function.deva +4 -4
  13. package/examples/index.deva +39 -25
  14. package/examples/loop.deva +5 -11
  15. package/examples/pattern.deva +8 -0
  16. package/examples/plugin.deva +16 -0
  17. package/examples/variables.deva +1 -1
  18. package/out-tsc/bin/index.js +51 -7
  19. package/out-tsc/index.js +3 -1
  20. package/out-tsc/scripts/postbuild.js +9 -10
  21. package/out-tsc/scripts/postinstall.js +49 -0
  22. package/package.json +12 -4
  23. package/project-version.json +3 -3
  24. package/rust/cli/bank.rs +462 -456
  25. package/rust/cli/build.rs +252 -199
  26. package/rust/cli/check.rs +221 -180
  27. package/rust/cli/driver.rs +297 -292
  28. package/rust/cli/generator.rs +1 -0
  29. package/rust/cli/init.rs +87 -79
  30. package/rust/cli/install.rs +35 -32
  31. package/rust/cli/login.rs +127 -134
  32. package/rust/cli/mod.rs +13 -11
  33. package/rust/cli/play.rs +1123 -218
  34. package/rust/cli/telemetry.rs +19 -0
  35. package/rust/cli/template.rs +69 -57
  36. package/rust/cli/update.rs +6 -4
  37. package/rust/common/api.rs +5 -8
  38. package/rust/common/cdn.rs +3 -6
  39. package/rust/common/mod.rs +3 -3
  40. package/rust/common/sso.rs +3 -6
  41. package/rust/config/driver.rs +118 -94
  42. package/rust/config/loader.rs +165 -156
  43. package/rust/config/mod.rs +4 -2
  44. package/rust/config/settings.rs +91 -0
  45. package/rust/config/stats.rs +257 -0
  46. package/rust/core/audio/engine.rs +696 -518
  47. package/rust/core/audio/evaluator.rs +263 -31
  48. package/rust/core/audio/interpreter/arrow_call.rs +198 -161
  49. package/rust/core/audio/interpreter/automate.rs +18 -0
  50. package/rust/core/audio/interpreter/call.rs +98 -95
  51. package/rust/core/audio/interpreter/condition.rs +70 -71
  52. package/rust/core/audio/interpreter/driver.rs +487 -198
  53. package/rust/core/audio/interpreter/function.rs +26 -21
  54. package/rust/core/audio/interpreter/let_.rs +38 -19
  55. package/rust/core/audio/interpreter/load.rs +18 -18
  56. package/rust/core/audio/interpreter/loop_.rs +113 -73
  57. package/rust/core/audio/interpreter/mod.rs +14 -13
  58. package/rust/core/audio/interpreter/sleep.rs +27 -30
  59. package/rust/core/audio/interpreter/spawn.rs +105 -102
  60. package/rust/core/audio/interpreter/tempo.rs +19 -16
  61. package/rust/core/audio/interpreter/trigger.rs +239 -210
  62. package/rust/core/audio/loader/mod.rs +1 -1
  63. package/rust/core/audio/loader/trigger.rs +100 -97
  64. package/rust/core/audio/mod.rs +7 -6
  65. package/rust/core/audio/player.rs +64 -64
  66. package/rust/core/audio/renderer.rs +56 -53
  67. package/rust/core/audio/special/easing.rs +189 -0
  68. package/rust/core/audio/special/env.rs +43 -0
  69. package/rust/core/audio/special/math.rs +102 -0
  70. package/rust/core/audio/special/mod.rs +9 -0
  71. package/rust/core/audio/special/modulator.rs +143 -0
  72. package/rust/core/builder/mod.rs +80 -85
  73. package/rust/core/debugger/lexer.rs +27 -27
  74. package/rust/core/debugger/mod.rs +24 -23
  75. package/rust/core/debugger/module.rs +55 -47
  76. package/rust/core/debugger/preprocessor.rs +27 -27
  77. package/rust/core/debugger/store.rs +40 -39
  78. package/rust/core/error/mod.rs +80 -66
  79. package/rust/core/lexer/handler/arrow.rs +82 -31
  80. package/rust/core/lexer/handler/at.rs +21 -21
  81. package/rust/core/lexer/handler/brace.rs +41 -41
  82. package/rust/core/lexer/handler/colon.rs +21 -21
  83. package/rust/core/lexer/handler/comment.rs +30 -30
  84. package/rust/core/lexer/handler/dot.rs +21 -21
  85. package/rust/core/lexer/handler/driver.rs +337 -263
  86. package/rust/core/lexer/handler/identifier.rs +46 -42
  87. package/rust/core/lexer/handler/indent.rs +66 -66
  88. package/rust/core/lexer/handler/mod.rs +16 -16
  89. package/rust/core/lexer/handler/newline.rs +23 -23
  90. package/rust/core/lexer/handler/number.rs +31 -31
  91. package/rust/core/lexer/handler/operator.rs +46 -44
  92. package/rust/core/lexer/handler/parenthesis.rs +41 -41
  93. package/rust/core/lexer/handler/slash.rs +21 -21
  94. package/rust/core/lexer/handler/string.rs +63 -63
  95. package/rust/core/lexer/mod.rs +54 -51
  96. package/rust/core/lexer/token.rs +97 -91
  97. package/rust/core/mod.rs +11 -11
  98. package/rust/core/parser/driver.rs +513 -408
  99. package/rust/core/parser/handler/arrow_call.rs +233 -211
  100. package/rust/core/parser/handler/at.rs +245 -162
  101. package/rust/core/parser/handler/bank.rs +94 -69
  102. package/rust/core/parser/handler/condition.rs +80 -74
  103. package/rust/core/parser/handler/dot.rs +143 -135
  104. package/rust/core/parser/handler/identifier/automate.rs +257 -0
  105. package/rust/core/parser/handler/identifier/call.rs +91 -88
  106. package/rust/core/parser/handler/identifier/emit.rs +66 -0
  107. package/rust/core/parser/handler/identifier/function.rs +100 -92
  108. package/rust/core/parser/handler/identifier/group.rs +85 -75
  109. package/rust/core/parser/handler/identifier/let_.rs +158 -127
  110. package/rust/core/parser/handler/identifier/mod.rs +54 -52
  111. package/rust/core/parser/handler/identifier/on.rs +98 -0
  112. package/rust/core/parser/handler/identifier/print.rs +52 -0
  113. package/rust/core/parser/handler/identifier/sleep.rs +36 -33
  114. package/rust/core/parser/handler/identifier/spawn.rs +91 -88
  115. package/rust/core/parser/handler/identifier/synth.rs +65 -65
  116. package/rust/core/parser/handler/loop_.rs +170 -72
  117. package/rust/core/parser/handler/mod.rs +8 -8
  118. package/rust/core/parser/handler/tempo.rs +53 -47
  119. package/rust/core/parser/mod.rs +4 -4
  120. package/rust/core/parser/statement.rs +142 -108
  121. package/rust/core/plugin/loader.rs +123 -48
  122. package/rust/core/plugin/mod.rs +2 -1
  123. package/rust/core/plugin/runner.rs +296 -0
  124. package/rust/core/preprocessor/loader.rs +515 -326
  125. package/rust/core/preprocessor/mod.rs +4 -4
  126. package/rust/core/preprocessor/module.rs +60 -58
  127. package/rust/core/preprocessor/processor.rs +99 -101
  128. package/rust/core/preprocessor/resolver/bank.rs +51 -49
  129. package/rust/core/preprocessor/resolver/call.rs +100 -100
  130. package/rust/core/preprocessor/resolver/condition.rs +97 -97
  131. package/rust/core/preprocessor/resolver/driver.rs +310 -278
  132. package/rust/core/preprocessor/resolver/function.rs +69 -78
  133. package/rust/core/preprocessor/resolver/group.rs +96 -91
  134. package/rust/core/preprocessor/resolver/let_.rs +32 -28
  135. package/rust/core/preprocessor/resolver/loop_.rs +320 -91
  136. package/rust/core/preprocessor/resolver/mod.rs +15 -15
  137. package/rust/core/preprocessor/resolver/spawn.rs +76 -92
  138. package/rust/core/preprocessor/resolver/synth.rs +56 -50
  139. package/rust/core/preprocessor/resolver/tempo.rs +50 -49
  140. package/rust/core/preprocessor/resolver/trigger.rs +113 -116
  141. package/rust/core/preprocessor/resolver/value.rs +81 -87
  142. package/rust/core/shared/bank.rs +1 -1
  143. package/rust/core/shared/duration.rs +9 -9
  144. package/rust/core/shared/mod.rs +3 -3
  145. package/rust/core/shared/value.rs +35 -32
  146. package/rust/core/store/function.rs +34 -34
  147. package/rust/core/store/global.rs +55 -38
  148. package/rust/core/store/mod.rs +5 -5
  149. package/rust/core/store/variable.rs +37 -34
  150. package/rust/core/utils/mod.rs +2 -2
  151. package/rust/core/utils/path.rs +37 -31
  152. package/rust/core/utils/validation.rs +35 -37
  153. package/rust/installer/addon.rs +84 -80
  154. package/rust/installer/bank.rs +62 -65
  155. package/rust/installer/mod.rs +5 -5
  156. package/rust/installer/plugin.rs +54 -55
  157. package/rust/installer/utils.rs +56 -56
  158. package/rust/lib.rs +156 -164
  159. package/rust/main.rs +250 -145
  160. package/rust/utils/error.rs +200 -0
  161. package/rust/utils/file.rs +38 -35
  162. package/rust/utils/first_usage.rs +76 -0
  163. package/rust/utils/logger.rs +195 -139
  164. package/rust/utils/mod.rs +9 -50
  165. package/rust/utils/signature.rs +19 -17
  166. package/rust/utils/spinner.rs +22 -19
  167. package/rust/utils/telemetry.rs +292 -0
  168. package/rust/utils/watcher.rs +34 -33
  169. package/templates/minimal/README.md +97 -121
  170. package/templates/welcome/README.md +97 -121
  171. package/typescript/bin/index.ts +19 -5
  172. package/typescript/index.ts +3 -1
  173. package/typescript/scripts/postbuild.ts +10 -6
  174. package/typescript/scripts/postinstall.ts +56 -0
  175. package/typescript/scripts/version/bump.ts +0 -1
  176. package/typescript/scripts/version/index.ts +0 -1
  177. package/out-tsc/bin/devalang.exe +0 -0
@@ -1,198 +1,487 @@
1
- use rayon::prelude::*;
2
- use std::sync::{ Arc, Mutex };
3
-
4
- use crate::core::{
5
- audio::{
6
- engine::AudioEngine,
7
- interpreter::{
8
- arrow_call::interprete_call_arrow_statement,
9
- call::interprete_call_statement,
10
- condition::interprete_condition_statement,
11
- function::interprete_function_statement,
12
- let_::interprete_let_statement,
13
- load::interprete_load_statement,
14
- loop_::interprete_loop_statement,
15
- sleep::interprete_sleep_statement,
16
- spawn::interprete_spawn_statement,
17
- tempo::interprete_tempo_statement,
18
- trigger::interprete_trigger_statement,
19
- },
20
- },
21
- parser::statement::{ Statement, StatementKind },
22
- store::{ function::FunctionTable, global::GlobalStore, variable::VariableTable },
23
- };
24
-
25
- pub fn run_audio_program(
26
- statements: &Vec<Statement>,
27
- audio_engine: &mut AudioEngine,
28
- entry: String,
29
- output: String,
30
- mut module_variables: VariableTable,
31
- mut module_functions: FunctionTable,
32
- global_store: &mut GlobalStore
33
- ) -> (f32, f32) {
34
- let mut base_bpm = 120.0;
35
- let mut base_duration = 60.0 / base_bpm;
36
-
37
- let (max_end_time, cursor_time) = execute_audio_block(
38
- audio_engine,
39
- global_store,
40
- global_store.variables.clone(),
41
- global_store.functions.clone(),
42
- statements.clone(),
43
- base_bpm,
44
- base_duration,
45
- 0.0,
46
- 0.0
47
- );
48
-
49
- (max_end_time, cursor_time)
50
- }
51
-
52
- pub fn execute_audio_block(
53
- audio_engine: &mut AudioEngine,
54
- global_store: &GlobalStore,
55
- mut variable_table: VariableTable,
56
- mut functions_table: FunctionTable,
57
- statements: Vec<Statement>,
58
- mut base_bpm: f32,
59
- mut base_duration: f32,
60
- mut max_end_time: f32,
61
- mut cursor_time: f32
62
- ) -> (f32, f32) {
63
- let (spawns, others): (Vec<_>, Vec<_>) = statements
64
- .into_iter()
65
- .partition(|stmt| matches!(stmt.kind, StatementKind::Spawn { .. }));
66
-
67
- // Execute sequential statements first
68
- for stmt in others {
69
- match &stmt.kind {
70
- StatementKind::Load { .. } => {
71
- if let Some(new_table) = interprete_load_statement(&stmt, &mut variable_table) {
72
- variable_table = new_table;
73
- }
74
- }
75
- StatementKind::Let { .. } => {
76
- if let Some(new_table) = interprete_let_statement(&stmt, &mut variable_table) {
77
- variable_table = new_table;
78
- }
79
- }
80
- StatementKind::Function { .. } => {
81
- if let Some(new_functions) =
82
- interprete_function_statement(&stmt, &mut functions_table)
83
- {
84
- functions_table = new_functions;
85
- }
86
- }
87
- StatementKind::Tempo => {
88
- if let Some((new_bpm, new_duration)) = interprete_tempo_statement(&stmt) {
89
- base_bpm = new_bpm;
90
- base_duration = new_duration;
91
- }
92
- }
93
- StatementKind::Trigger { .. } => {
94
- if
95
- let Some((new_cursor, new_max, _)) = interprete_trigger_statement(
96
- &stmt,
97
- audio_engine,
98
- &variable_table,
99
- base_duration,
100
- cursor_time,
101
- max_end_time
102
- )
103
- {
104
- cursor_time = new_cursor;
105
- max_end_time = new_max;
106
- }
107
- }
108
- StatementKind::Sleep => {
109
- let (new_cursor, new_max) = interprete_sleep_statement(
110
- &stmt,
111
- cursor_time,
112
- max_end_time
113
- );
114
- cursor_time = new_cursor;
115
- max_end_time = new_max;
116
- }
117
- StatementKind::Loop => {
118
- let (new_max, new_cursor) = interprete_loop_statement(
119
- &stmt,
120
- audio_engine,
121
- global_store,
122
- &variable_table,
123
- &functions_table,
124
- base_bpm,
125
- base_duration,
126
- max_end_time,
127
- cursor_time
128
- );
129
- cursor_time = new_cursor;
130
- max_end_time = new_max;
131
- }
132
- StatementKind::Call { .. } => {
133
- let (new_max, _) = interprete_call_statement(
134
- &stmt,
135
- audio_engine,
136
- &variable_table,
137
- &functions_table,
138
- global_store,
139
- base_bpm,
140
- base_duration,
141
- max_end_time,
142
- cursor_time,
143
- );
144
- cursor_time = new_max;
145
- max_end_time = new_max;
146
- }
147
- StatementKind::ArrowCall { .. } => {
148
- let (new_max, new_cursor) = interprete_call_arrow_statement(
149
- &stmt,
150
- audio_engine,
151
- &variable_table,
152
- base_bpm,
153
- base_duration,
154
- &mut max_end_time,
155
- Some(&mut cursor_time),
156
- true
157
- );
158
-
159
- cursor_time = new_cursor;
160
-
161
- if new_max > max_end_time {
162
- max_end_time = new_max;
163
- }
164
- }
165
- _ => {}
166
- }
167
- }
168
-
169
- // Execute parallel spawns (collect results)
170
- let spawn_results: Vec<(AudioEngine, f32)> = spawns
171
- .par_iter()
172
- .map(|stmt| {
173
- let mut local_engine = AudioEngine::new(audio_engine.module_name.clone());
174
- let (spawn_max, _) = interprete_spawn_statement(
175
- stmt,
176
- &mut local_engine,
177
- &variable_table,
178
- &functions_table,
179
- global_store,
180
- base_bpm,
181
- base_duration,
182
- 0.0,
183
- 0.0
184
- );
185
- (local_engine, spawn_max)
186
- })
187
- .collect();
188
-
189
- // Finally, merge results from all spawns
190
- for (local_engine, spawn_max) in spawn_results {
191
- audio_engine.merge_with(local_engine);
192
- if spawn_max > max_end_time {
193
- max_end_time = spawn_max;
194
- }
195
- }
196
-
197
- (max_end_time.max(cursor_time), cursor_time)
198
- }
1
+ use rayon::prelude::*;
2
+
3
+ use crate::{
4
+ core::{
5
+ audio::{
6
+ engine::AudioEngine,
7
+ interpreter::{
8
+ arrow_call::interprete_call_arrow_statement, call::interprete_call_statement,
9
+ function::interprete_function_statement, let_::interprete_let_statement,
10
+ load::interprete_load_statement, loop_::interprete_loop_statement,
11
+ sleep::interprete_sleep_statement, spawn::interprete_spawn_statement,
12
+ tempo::interprete_tempo_statement, trigger::interprete_trigger_statement,
13
+ },
14
+ },
15
+ parser::statement::{Statement, StatementKind},
16
+ shared::value::Value,
17
+ store::{function::FunctionTable, global::GlobalStore, variable::VariableTable},
18
+ },
19
+ utils::logger::{LogLevel, Logger},
20
+ };
21
+
22
+ pub fn run_audio_program(
23
+ statements: &Vec<Statement>,
24
+ audio_engine: &mut AudioEngine,
25
+ _entry: String,
26
+ _output: String,
27
+ _module_variables: VariableTable,
28
+ _module_functions: FunctionTable,
29
+ global_store: &mut GlobalStore,
30
+ ) -> (f32, f32) {
31
+ let base_bpm = 120.0;
32
+ let base_duration = 60.0 / base_bpm;
33
+
34
+ let (max_end_time, cursor_time) = execute_audio_block(
35
+ audio_engine,
36
+ global_store,
37
+ global_store.variables.clone(),
38
+ global_store.functions.clone(),
39
+ &statements,
40
+ base_bpm,
41
+ base_duration,
42
+ 0.0,
43
+ 0.0,
44
+ );
45
+
46
+ (max_end_time, cursor_time)
47
+ }
48
+
49
+ pub fn execute_audio_block(
50
+ audio_engine: &mut AudioEngine,
51
+ global_store: &GlobalStore,
52
+ mut variable_table: VariableTable,
53
+ mut functions_table: FunctionTable,
54
+ statements: &[Statement],
55
+ mut base_bpm: f32,
56
+ mut base_duration: f32,
57
+ mut max_end_time: f32,
58
+ mut cursor_time: f32,
59
+ ) -> (f32, f32) {
60
+ // Track nested depth of execute_audio_block to avoid scheduling periodic events multiple times
61
+ let current_depth = match variable_table.get("__depth") {
62
+ Some(Value::Number(n)) => *n,
63
+ _ => 0.0,
64
+ };
65
+ variable_table.set("__depth".to_string(), Value::Number(current_depth + 1.0));
66
+ let (spawns, others): (Vec<_>, Vec<_>) = statements
67
+ .iter()
68
+ .partition(|stmt| matches!(stmt.kind, StatementKind::Spawn { .. }));
69
+
70
+ // Execute sequential statements first
71
+ for stmt in others {
72
+ match &stmt.kind {
73
+ StatementKind::Load { .. } => {
74
+ if let Some(new_table) = interprete_load_statement(&stmt, &mut variable_table) {
75
+ variable_table = new_table;
76
+ }
77
+ }
78
+ StatementKind::On { .. } => {
79
+ // already registered in global store during parsing; nothing to do at runtime
80
+ }
81
+ StatementKind::Emit { event, payload: _ } => {
82
+ if let Some(handlers) = global_store.get_event_handlers(event) {
83
+ for h in handlers {
84
+ if let StatementKind::On {
85
+ event: _,
86
+ args,
87
+ body,
88
+ } = &h.kind
89
+ {
90
+ // Create a derived variable table with event context
91
+ let mut vt = variable_table.clone();
92
+ let mut ctx = std::collections::HashMap::new();
93
+ ctx.insert("name".to_string(), Value::String(event.clone()));
94
+ if let Some(arg_list) = args.clone() {
95
+ ctx.insert("args".to_string(), Value::Array(arg_list));
96
+ }
97
+ // Attach payload if any on the Emit statement value
98
+ ctx.insert("payload".to_string(), stmt.value.clone());
99
+ vt.set("event".to_string(), Value::Map(ctx));
100
+ // Mark we're inside an event handler to avoid re-scheduling periodic events recursively
101
+ vt.set("__in_event".to_string(), Value::Boolean(true));
102
+
103
+ let (_max, _cursor) = execute_audio_block(
104
+ audio_engine,
105
+ global_store,
106
+ vt,
107
+ functions_table.clone(),
108
+ body,
109
+ base_bpm,
110
+ base_duration,
111
+ max_end_time,
112
+ cursor_time,
113
+ );
114
+ }
115
+ }
116
+ }
117
+ }
118
+ StatementKind::Let { .. } => {
119
+ if let Some(new_table) = interprete_let_statement(&stmt, &mut variable_table) {
120
+ variable_table = new_table;
121
+ }
122
+ }
123
+ StatementKind::Function { .. } => {
124
+ if let Some(new_functions) =
125
+ interprete_function_statement(&stmt, &mut functions_table)
126
+ {
127
+ functions_table = new_functions;
128
+ }
129
+ }
130
+ StatementKind::Tempo => {
131
+ if let Some((new_bpm, new_duration)) = interprete_tempo_statement(&stmt) {
132
+ base_bpm = new_bpm;
133
+ base_duration = new_duration;
134
+ }
135
+ }
136
+ StatementKind::Trigger { .. } => {
137
+ if let Some((new_cursor, new_max, _)) = interprete_trigger_statement(
138
+ &stmt,
139
+ audio_engine,
140
+ &variable_table,
141
+ base_duration,
142
+ cursor_time,
143
+ max_end_time,
144
+ ) {
145
+ cursor_time = new_cursor;
146
+ max_end_time = new_max;
147
+ }
148
+ }
149
+ StatementKind::Sleep => {
150
+ let (new_cursor, new_max) =
151
+ interprete_sleep_statement(&stmt, cursor_time, max_end_time);
152
+ cursor_time = new_cursor;
153
+ max_end_time = new_max;
154
+ }
155
+ StatementKind::Loop => {
156
+ let (new_max, new_cursor) = interprete_loop_statement(
157
+ &stmt,
158
+ audio_engine,
159
+ global_store,
160
+ &variable_table,
161
+ &functions_table,
162
+ base_bpm,
163
+ base_duration,
164
+ max_end_time,
165
+ cursor_time,
166
+ );
167
+ cursor_time = new_cursor;
168
+ max_end_time = new_max;
169
+ }
170
+ StatementKind::Call { .. } => {
171
+ let (new_max, _) = interprete_call_statement(
172
+ &stmt,
173
+ audio_engine,
174
+ &variable_table,
175
+ &functions_table,
176
+ global_store,
177
+ base_bpm,
178
+ base_duration,
179
+ max_end_time,
180
+ cursor_time,
181
+ );
182
+ cursor_time = new_max;
183
+ max_end_time = new_max;
184
+ }
185
+ StatementKind::ArrowCall { .. } => {
186
+ let (new_max, new_cursor) = interprete_call_arrow_statement(
187
+ &stmt,
188
+ audio_engine,
189
+ &variable_table,
190
+ base_bpm,
191
+ base_duration,
192
+ &mut max_end_time,
193
+ Some(&mut cursor_time),
194
+ true,
195
+ );
196
+
197
+ cursor_time = new_cursor;
198
+
199
+ if new_max > max_end_time {
200
+ max_end_time = new_max;
201
+ }
202
+ }
203
+ StatementKind::Automate { .. } => {
204
+ if let Some(new_table) =
205
+ crate::core::audio::interpreter::automate::interprete_automate_statement(
206
+ &stmt,
207
+ &mut variable_table,
208
+ )
209
+ {
210
+ variable_table = new_table;
211
+ }
212
+ }
213
+ StatementKind::Print => {
214
+ // Only print in real-time mode (during playback), not during offline render.
215
+ let is_realtime = matches!(variable_table.get("__rt"), Some(Value::Boolean(true)));
216
+ if is_realtime {
217
+ let logger = Logger::new();
218
+ match &stmt.value {
219
+ Value::String(s) => {
220
+ let bpm = if let Some(Value::Number(n)) = variable_table.get("bpm") {
221
+ *n
222
+ } else {
223
+ 120.0
224
+ };
225
+ let beat = if let Some(Value::Number(n)) = variable_table.get("beat") {
226
+ *n
227
+ } else {
228
+ 0.0
229
+ };
230
+ // First try JS-like string concatenation: "str " + var + 1 + $env.*
231
+ if let Some(res) =
232
+ crate::core::audio::evaluator::evaluate_string_expression(
233
+ s,
234
+ &variable_table,
235
+ bpm,
236
+ beat,
237
+ )
238
+ {
239
+ logger.log_message(LogLevel::Print, &res);
240
+ } else if let Some(val) = variable_table.get(&s) {
241
+ logger.log_message(LogLevel::Print, &format!("{:?}", val));
242
+ } else if s.contains("$env")
243
+ || s.contains("$math")
244
+ || s.parse::<f32>().is_ok()
245
+ {
246
+ let v = crate::core::audio::evaluator::evaluate_rhs_into_value(
247
+ s,
248
+ &variable_table,
249
+ bpm,
250
+ beat,
251
+ );
252
+ match v {
253
+ Value::Number(n) => {
254
+ logger.log_message(LogLevel::Print, &format!("{}", n))
255
+ }
256
+ _ => logger.log_message(LogLevel::Print, s),
257
+ }
258
+ } else {
259
+ logger.log_message(LogLevel::Print, s)
260
+ }
261
+ }
262
+ Value::Number(n) => {
263
+ logger.log_message(LogLevel::Print, &format!("{}", n));
264
+ }
265
+ Value::Identifier(name) => {
266
+ if let Some(val) = variable_table.get(name) {
267
+ match val {
268
+ Value::Number(n) => {
269
+ logger.log_message(LogLevel::Print, &format!("{}", n))
270
+ }
271
+ Value::String(s) => logger.log_message(LogLevel::Print, s),
272
+ Value::Boolean(b) => {
273
+ logger.log_message(LogLevel::Print, &format!("{}", b))
274
+ }
275
+ other => {
276
+ logger.log_message(LogLevel::Print, &format!("{:?}", other))
277
+ }
278
+ }
279
+ } else {
280
+ logger.log_message(LogLevel::Print, name)
281
+ }
282
+ }
283
+ v => logger.log_message(LogLevel::Print, &format!("{:?}", v)),
284
+ }
285
+ }
286
+ }
287
+ _ => {}
288
+ }
289
+ }
290
+
291
+ // Execute parallel spawns (collect results)
292
+ let spawn_results: Vec<(AudioEngine, f32)> = spawns
293
+ .par_iter()
294
+ .map(|stmt| {
295
+ let mut local_engine = AudioEngine::new(audio_engine.module_name.clone());
296
+ let (spawn_max, _) = interprete_spawn_statement(
297
+ stmt,
298
+ &mut local_engine,
299
+ &variable_table,
300
+ &functions_table,
301
+ global_store,
302
+ base_bpm,
303
+ base_duration,
304
+ 0.0,
305
+ 0.0,
306
+ );
307
+ (local_engine, spawn_max)
308
+ })
309
+ .collect();
310
+
311
+ // Finally, merge results from all spawns
312
+ for (local_engine, spawn_max) in spawn_results {
313
+ audio_engine.merge_with(local_engine);
314
+ if spawn_max > max_end_time {
315
+ max_end_time = spawn_max;
316
+ }
317
+ }
318
+
319
+ // ─────────────────────────────────────────────────────────────
320
+ // Built-in periodic events (e.g., on beat(n), on bar(n))
321
+ // Emit handlers across the timeline up to max_end_time.
322
+ // If no audio was scheduled (max_end_time == 0.0), skip.
323
+ // Don't schedule periodic events if we're already inside an event handler
324
+ let in_event = matches!(variable_table.get("__in_event"), Some(Value::Boolean(true)));
325
+ let depth_is_root = matches!(variable_table.get("__depth"), Some(Value::Number(n)) if (*n - 1.0).abs() < f32::EPSILON);
326
+ if max_end_time > 0.0 && !in_event && depth_is_root {
327
+ if !global_store.events.is_empty() {
328
+ // Beat-based handlers (support "beat" and "$beat")
329
+ for ev_key in ["beat", "$beat"] {
330
+ if let Some(handlers) = global_store.get_event_handlers(ev_key) {
331
+ let mut seen: std::collections::HashSet<(usize, usize, usize)> =
332
+ std::collections::HashSet::new();
333
+ // Default every 1 beat if args missing
334
+ for h in handlers {
335
+ let key = (h.line, h.column, h.indent);
336
+ if !seen.insert(key) {
337
+ continue;
338
+ }
339
+ if let StatementKind::On { event, args, body } = &h.kind {
340
+ let every: f32 = args
341
+ .as_ref()
342
+ .and_then(|v| v.get(0))
343
+ .and_then(|x| match x {
344
+ Value::Number(n) => Some(*n),
345
+ Value::Identifier(s) => {
346
+ // Try to resolve from variables first, fallback to parsing the literal
347
+ match variable_table.get(s) {
348
+ Some(Value::Number(n)) => Some(*n),
349
+ _ => s.parse::<f32>().ok(),
350
+ }
351
+ }
352
+ _ => None,
353
+ })
354
+ .unwrap_or(1.0)
355
+ .max(0.0001);
356
+ let step = base_duration * every;
357
+ // Start from first full bar boundary after t=0
358
+ let mut t = step;
359
+ while t <= max_end_time {
360
+ // Prepare event context
361
+ let mut vt = variable_table.clone();
362
+ let mut ctx = std::collections::HashMap::new();
363
+ ctx.insert("name".to_string(), Value::String(event.clone()));
364
+ if let Some(a) = args.clone() {
365
+ ctx.insert("args".to_string(), Value::Array(a));
366
+ }
367
+ vt.set("event".to_string(), Value::Map(ctx));
368
+ vt.set("beat".to_string(), Value::Number(t / base_duration));
369
+ // Prevent nested scheduling
370
+ vt.set("__in_event".to_string(), Value::Boolean(true));
371
+
372
+ let (_m, _c) = execute_audio_block(
373
+ audio_engine,
374
+ global_store,
375
+ vt,
376
+ functions_table.clone(),
377
+ body,
378
+ base_bpm,
379
+ base_duration,
380
+ max_end_time,
381
+ t,
382
+ );
383
+
384
+ t += step;
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ // Bar-based handlers (default 4/4 time => 4 beats per bar); support "bar" and "$bar"
392
+ for ev_key in ["bar", "$bar"] {
393
+ if let Some(handlers) = global_store.get_event_handlers(ev_key) {
394
+ let mut seen: std::collections::HashSet<(usize, usize, usize)> =
395
+ std::collections::HashSet::new();
396
+ for h in handlers {
397
+ let key = (h.line, h.column, h.indent);
398
+ if !seen.insert(key) {
399
+ continue;
400
+ }
401
+ if let StatementKind::On { event, args, body } = &h.kind {
402
+ let bar_beats = 4.0f32; // TODO: time signature support
403
+ let first_only = args.as_ref().and_then(|v| v.get(0)).is_none();
404
+
405
+ let every_bar: f32 = if first_only {
406
+ 1.0
407
+ } else {
408
+ args.as_ref()
409
+ .and_then(|v| v.get(0))
410
+ .and_then(|x| match x {
411
+ Value::Number(n) => Some(*n),
412
+ Value::Identifier(s) => match variable_table.get(s) {
413
+ Some(Value::Number(n)) => Some(*n),
414
+ _ => s.parse::<f32>().ok(),
415
+ },
416
+ _ => None,
417
+ })
418
+ .unwrap_or(1.0)
419
+ .max(0.0001)
420
+ };
421
+
422
+ let step = base_duration * bar_beats * every_bar;
423
+
424
+ if first_only {
425
+ let t = step; // first full bar after t=0
426
+ if t <= max_end_time {
427
+ let mut vt = variable_table.clone();
428
+ let mut ctx = std::collections::HashMap::new();
429
+ ctx.insert("name".to_string(), Value::String(event.clone()));
430
+ if let Some(a) = args.clone() {
431
+ ctx.insert("args".to_string(), Value::Array(a));
432
+ }
433
+ vt.set("event".to_string(), Value::Map(ctx));
434
+ vt.set("beat".to_string(), Value::Number(t / base_duration));
435
+ // Prevent nested scheduling
436
+ vt.set("__in_event".to_string(), Value::Boolean(true));
437
+
438
+ let (_m, _c) = execute_audio_block(
439
+ audio_engine,
440
+ global_store,
441
+ vt,
442
+ functions_table.clone(),
443
+ body,
444
+ base_bpm,
445
+ base_duration,
446
+ max_end_time,
447
+ t,
448
+ );
449
+ }
450
+ } else {
451
+ let mut t = step; // start from first full bar after t=0
452
+ while t <= max_end_time {
453
+ let mut vt = variable_table.clone();
454
+ let mut ctx = std::collections::HashMap::new();
455
+ ctx.insert("name".to_string(), Value::String(event.clone()));
456
+ if let Some(a) = args.clone() {
457
+ ctx.insert("args".to_string(), Value::Array(a));
458
+ }
459
+ vt.set("event".to_string(), Value::Map(ctx));
460
+ vt.set("beat".to_string(), Value::Number(t / base_duration));
461
+ // Prevent nested scheduling
462
+ vt.set("__in_event".to_string(), Value::Boolean(true));
463
+
464
+ let (_m, _c) = execute_audio_block(
465
+ audio_engine,
466
+ global_store,
467
+ vt,
468
+ functions_table.clone(),
469
+ body,
470
+ base_bpm,
471
+ base_duration,
472
+ max_end_time,
473
+ t,
474
+ );
475
+
476
+ t += step;
477
+ }
478
+ }
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ (max_end_time.max(cursor_time), cursor_time)
487
+ }