1#[cfg(feature = "cli")]
2use crate::core::preprocessor::resolver::driver::{
3 resolve_all_modules,
4 resolve_and_flatten_all_modules,
5};
6#[cfg(feature = "cli")]
7use crate::core::utils::path::resolve_relative_path;
8#[cfg_attr(not(feature = "cli"), allow(unused_imports))]
9use crate::core::{
10 error::ErrorHandler,
11 lexer::{ token::Token, driver::Lexer },
12 parser::{ driver::Parser, statement::{ Statement, StatementKind } },
13 plugin::loader::load_plugin,
14 preprocessor::{ module::Module, processor::process_modules },
15 store::global::GlobalStore,
16 utils::path::normalize_path,
17};
18use devalang_types::{ BankFile, Value };
19use devalang_utils::logger::{ LogLevel, Logger };
20use std::{ collections::HashMap, path::Path };
21
22pub struct ModuleLoader {
23 pub entry: String,
24 pub output: String,
25 pub base_dir: String,
26}
27
28impl ModuleLoader {
29 pub fn new(entry: &str, output: &str) -> Self {
30 let base_dir = Path::new(entry)
31 .parent()
32 .unwrap_or(Path::new(""))
33 .to_string_lossy()
34 .replace('\\', "/");
35
36 Self {
37 entry: entry.to_string(),
38 output: output.to_string(),
39 base_dir,
40 }
41 }
42
43 pub fn from_raw_source(
44 entry_path: &str,
45 output_path: &str,
46 content: &str,
47 global_store: &mut GlobalStore
48 ) -> Self {
49 let normalized_entry_path = normalize_path(entry_path);
50
51 let mut module = Module::new(entry_path);
52 module.content = content.to_string();
53
54 global_store.insert_module(normalized_entry_path.to_string(), module);
58
59 Self {
60 entry: normalized_entry_path.to_string(),
61 output: output_path.to_string(),
62 base_dir: "".to_string(),
63 }
64 }
65
66 pub fn extract_statements_map(
67 &self,
68 global_store: &GlobalStore
69 ) -> HashMap<String, Vec<Statement>> {
70 global_store.modules
71 .iter()
72 .map(|(path, module)| (path.clone(), module.statements.clone()))
73 .collect()
74 }
75
76 pub fn load_single_module(&self, global_store: &mut GlobalStore) -> Result<Module, String> {
77 let mut module = global_store.modules
78 .remove(&self.entry)
79 .ok_or_else(|| format!("Module not found in store for path: {}", self.entry))?;
80
81 let lexer = Lexer::new();
83 let tokens = lexer
84 .lex_from_source(&module.content)
85 .map_err(|e| format!("Lexer failed: {}", e))?;
86
87 module.tokens = tokens.clone();
88
89 let mut parser = Parser::new();
91 parser.set_current_module(self.entry.clone());
92 let statements = parser.parse_tokens(tokens, global_store);
93 module.statements = statements;
94
95 if let Err(e) = self.inject_bank_triggers(&mut module, "808", None) {
97 return Err(format!("Failed to inject bank triggers: {}", e));
98 }
99
100 for (plugin_name, alias) in self.extract_plugin_uses(&module.statements) {
101 self.load_plugin_and_register(&mut module, &plugin_name, &alias, global_store);
102 }
103
104 global_store.modules.insert(self.entry.clone(), module.clone());
105
106 let mut error_handler = ErrorHandler::new();
108 error_handler.detect_from_statements(&mut parser, &module.statements);
109
110 Ok(module)
111 }
112
113 pub fn load_wasm_module(&self, global_store: &mut GlobalStore) -> Result<(), String> {
114 let module = {
116 let module_ref = global_store.modules
117 .get(&self.entry)
118 .ok_or_else(|| format!("❌ Module not found for path: {}", self.entry))?;
119
120 Module::from_existing(&self.entry, module_ref.content.clone())
121 };
122
123 let lexer = Lexer::new();
125 let tokens = lexer
126 .lex_from_source(&module.content)
127 .map_err(|e| format!("Lexer failed: {}", e))?;
128
129 let mut parser = Parser::new();
131 parser.set_current_module(self.entry.clone());
132
133 let statements = parser.parse_tokens(tokens.clone(), global_store);
134
135 let mut updated_module = module;
136 updated_module.tokens = tokens;
137 updated_module.statements = statements;
138
139 if let Err(e) = self.inject_bank_triggers(&mut updated_module, "808", None) {
141 return Err(format!("Failed to inject bank triggers: {}", e));
142 }
143
144 global_store.modules.insert(self.entry.clone(), updated_module.clone());
148
149 process_modules(self, global_store);
152
153 for (plugin_name, alias) in self.extract_plugin_uses(&updated_module.statements) {
154 self.load_plugin_and_register(&mut updated_module, &plugin_name, &alias, global_store);
155 }
156
157 let mut error_handler = ErrorHandler::new();
159 error_handler.detect_from_statements(&mut parser, &updated_module.statements);
160
161 if let Some(stored_module) = global_store.modules.get(&self.entry) {
167 global_store.variables.variables.extend(stored_module.variable_table.variables.clone());
168 global_store.functions.functions.extend(stored_module.function_table.functions.clone());
169 } else {
170 global_store.variables.variables.extend(
173 updated_module.variable_table.variables.clone()
174 );
175 global_store.functions.functions.extend(
176 updated_module.function_table.functions.clone()
177 );
178 }
179
180 Ok(())
181 }
182
183 #[cfg(feature = "cli")]
184 pub fn load_all_modules(
185 &self,
186 global_store: &mut GlobalStore
187 ) -> (HashMap<String, Vec<Token>>, HashMap<String, Vec<Statement>>) {
188 let tokens_by_module = self.load_module_recursively(&self.entry, global_store);
190
191 process_modules(self, global_store);
193 resolve_all_modules(self, global_store);
194
195 let statements_by_module = resolve_and_flatten_all_modules(global_store);
197
198 (tokens_by_module, statements_by_module)
199 }
200
201 #[cfg(feature = "cli")]
202 fn load_module_recursively(
203 &self,
204 raw_path: &str,
205 global_store: &mut GlobalStore
206 ) -> HashMap<String, Vec<Token>> {
207 let path = normalize_path(raw_path);
208
209 if global_store.modules.contains_key(&path) {
211 return HashMap::new();
212 }
213
214 let lexer = Lexer::new();
215 let tokens = match lexer.lex_tokens(&path) {
216 Ok(t) => t,
217 Err(e) => {
218 let logger = Logger::new();
219 logger.log_message(LogLevel::Error, &format!("Failed to lex '{}': {}", path, e));
220 return HashMap::new();
221 }
222 };
223
224 let mut parser = Parser::new();
225 parser.set_current_module(path.clone());
226
227 let statements = parser.parse_tokens(tokens.clone(), global_store);
228
229 let mut module = Module::new(&path);
231 module.tokens = tokens.clone();
232 module.statements = statements.clone();
233
234 for (bank_name, alias_opt) in self.extract_bank_decls(&statements) {
236 if let Err(e) = self.inject_bank_triggers(&mut module, &bank_name, alias_opt) {
237 eprintln!("Failed to inject bank triggers for '{}': {}", bank_name, e);
238 }
239 }
240
241 for (plugin_name, alias) in self.extract_plugin_uses(&statements) {
242 self.load_plugin_and_register(&mut module, &plugin_name, &alias, global_store);
243 }
244
245 global_store.variables.variables.extend(module.variable_table.variables.clone());
247 global_store.functions.functions.extend(module.function_table.functions.clone());
248
249 global_store.insert_module(path.clone(), module);
251
252 self.load_module_imports(&path, global_store);
254
255 let mut error_handler = ErrorHandler::new();
257 if let Some(current_module) = global_store.modules.get(&path) {
258 error_handler.detect_from_statements(&mut parser, ¤t_module.statements);
259 } else {
260 error_handler.detect_from_statements(&mut parser, &statements);
261 }
262
263 if error_handler.has_errors() {
264 let logger = Logger::new();
265 for error in error_handler.get_errors() {
266 let trace = format!("{}:{}:{}", path, error.line, error.column);
267 logger.log_error_with_stacktrace(&error.message, &trace);
268 }
269 }
270
271 global_store.modules
273 .iter()
274 .map(|(p, m)| (p.clone(), m.tokens.clone()))
275 .collect()
276 }
277
278 #[cfg(feature = "cli")]
279 fn load_module_imports(&self, path: &String, global_store: &mut GlobalStore) {
280 let import_paths: Vec<String> = {
281 let current_module = match global_store.modules.get(path) {
282 Some(module) => module,
283 None => {
284 eprintln!("[warn] Cannot resolve imports: module '{}' not found in store", path);
285 return;
286 }
287 };
288
289 current_module.statements
290 .iter()
291 .filter_map(|stmt| {
292 if let StatementKind::Import { source, .. } = &stmt.kind {
293 Some(source.clone())
294 } else {
295 None
296 }
297 })
298 .collect()
299 };
300
301 for import_path in import_paths {
302 let resolved = resolve_relative_path(path, &import_path);
303 self.load_module_recursively(&resolved, global_store);
304 }
305 }
306
307 pub fn inject_bank_triggers(
308 &self,
309 module: &mut Module,
310 bank_name: &str,
311 alias_override: Option<String>
312 ) -> Result<Module, String> {
313 let default_alias = bank_name.split('.').next_back().unwrap_or(bank_name).to_string();
314 let alias_ref = alias_override.as_deref().unwrap_or(&default_alias);
315
316 let bank_path = match devalang_utils::path::get_deva_dir() {
317 Ok(dir) => dir.join("banks").join(bank_name),
318 Err(_) => Path::new("./.deva").join("banks").join(bank_name),
319 };
320 let bank_toml_path = bank_path.join("bank.toml");
321
322 if !bank_toml_path.exists() {
323 return Ok(module.clone());
324 }
325
326 let content = std::fs
327 ::read_to_string(&bank_toml_path)
328 .map_err(|e| format!("Failed to read '{}': {}", bank_toml_path.display(), e))?;
329
330 let parsed_bankfile: BankFile = toml
331 ::from_str(&content)
332 .map_err(|e| format!("Failed to parse '{}': {}", bank_toml_path.display(), e))?;
333
334 let mut bank_map = HashMap::new();
335
336 for bank_trigger in parsed_bankfile.triggers.unwrap_or_default() {
337 let entity_ref = bank_trigger.path.clone().replace("\\", "/").replace("./", "");
341 let bank_trigger_path = format!("devalang://bank/{}/{}", bank_name, entity_ref);
342
343 bank_map.insert(bank_trigger.name.clone(), Value::String(bank_trigger_path.clone()));
346
347 if module.variable_table.variables.contains_key(alias_ref) {
348 eprintln!(
349 "⚠️ Trigger '{}' already defined in module '{}', skipping injection.",
350 alias_ref,
351 module.path
352 );
353 continue;
354 }
355
356 module.variable_table.set(
357 format!("{}.{}", alias_ref, bank_trigger.name),
358 Value::String(bank_trigger_path.clone())
359 );
360 }
361
362 module.variable_table.set(alias_ref.to_string(), Value::Map(bank_map));
364
365 Ok(module.clone())
366 }
367
368 #[cfg_attr(not(feature = "cli"), allow(dead_code))]
369 fn extract_bank_decls(&self, statements: &[Statement]) -> Vec<(String, Option<String>)> {
370 let mut banks = Vec::new();
371
372 for stmt in statements {
373 if let StatementKind::Bank { alias } = &stmt.kind {
374 let name_opt = match &stmt.value {
375 Value::String(s) => Some(s.clone()),
376 Value::Identifier(s) => Some(s.clone()),
377 Value::Number(n) => Some(n.to_string()),
378 _ => None,
379 };
380 if let Some(name) = name_opt {
381 banks.push((name, alias.clone()));
382 }
383 }
384 }
385
386 banks
387 }
388
389 fn extract_plugin_uses(&self, statements: &[Statement]) -> Vec<(String, String)> {
390 let mut plugins = Vec::new();
391
392 for stmt in statements {
393 if let StatementKind::Use { name, alias } = &stmt.kind {
394 let alias_name = alias
395 .clone()
396 .unwrap_or_else(|| name.split('.').next_back().unwrap_or(name).to_string());
397 plugins.push((name.clone(), alias_name));
398 }
399 }
400
401 plugins
402 }
403
404 fn load_plugin_and_register(
405 &self,
406 module: &mut Module,
407 plugin_name: &str,
408 alias: &str,
409 global_store: &mut GlobalStore
410 ) {
411 let mut parts = plugin_name.split('.');
413 let author = match parts.next() {
414 Some(a) if !a.is_empty() => a,
415 _ => {
416 eprintln!("Invalid plugin name '{}': missing author", plugin_name);
417 return;
418 }
419 };
420 let name = match parts.next() {
421 Some(n) if !n.is_empty() => n,
422 _ => {
423 eprintln!("Invalid plugin name '{}': missing name", plugin_name);
424 return;
425 }
426 };
427 if parts.next().is_some() {
428 eprintln!("Invalid plugin name '{}': expected <author>.<name>", plugin_name);
429 return;
430 }
431
432 let expected_uri = format!("devalang://plugin/{}.{}", author, name);
435
436 let root = match devalang_utils::path::get_deva_dir() {
438 Ok(dir) => dir,
439 Err(_) => Path::new("./.deva").to_path_buf(),
440 };
441 let plugin_dir_preferred = root.join("plugins").join(format!("{}.{}", author, name));
442 let toml_path_preferred = plugin_dir_preferred.join("plugin.toml");
443 let plugin_dir_fallback = root.join("plugins").join(author).join(name);
444 let toml_path_fallback = plugin_dir_fallback.join("plugin.toml");
445 let exists_locally = toml_path_preferred.exists() || toml_path_fallback.exists();
446
447 if exists_locally {
448 let cfg_opt = crate::config::ops::load_config(None);
450 let mut declared = false;
451 if let Some(cfg) = cfg_opt {
452 if let Some(list) = cfg.plugins {
453 declared = list.iter().any(|p| p.path == expected_uri);
454 }
455 }
456 if !declared {
457 module.statements.push(Statement {
459 kind: StatementKind::Error {
460 message: "plugin present in local files but missing in .devalang config".to_string(),
461 },
462 value: Value::Null,
463 indent: 0,
464 line: 0,
465 column: 0,
466 });
467 return;
468 }
469 }
470
471 match load_plugin(author, name) {
472 Ok((info, wasm)) => {
473 let uri = format!("devalang://plugin/{}.{}", author, name);
474 global_store.plugins.insert(format!("{}:{}", author, name), (info, wasm));
475 module.variable_table.set(alias.to_string(), Value::String(uri.clone()));
477 global_store.variables.set(alias.to_string(), Value::String(uri.clone()));
479
480 if
481 let Some((plugin_info, _)) = global_store.plugins.get(
482 &format!("{}:{}", author, name)
483 )
484 {
485 for exp in &plugin_info.exports {
486 match exp.kind.as_str() {
487 "number" => {
488 if let Some(toml::Value::String(s)) = &exp.default {
489 if let Ok(n) = s.parse::<f32>() {
490 module.variable_table.set(
491 format!("{}.{}", alias, exp.name),
492 Value::Number(n)
493 );
494 }
495 } else if let Some(toml::Value::Integer(i)) = &exp.default {
496 module.variable_table.set(
497 format!("{}.{}", alias, exp.name),
498 Value::Number(*i as f32)
499 );
500 } else if let Some(toml::Value::Float(f)) = &exp.default {
501 module.variable_table.set(
502 format!("{}.{}", alias, exp.name),
503 Value::Number(*f as f32)
504 );
505 }
506 }
507 "string" => {
508 if let Some(toml::Value::String(s)) = &exp.default {
509 module.variable_table.set(
510 format!("{}.{}", alias, exp.name),
511 Value::String(s.clone())
512 );
513 }
514 }
515 "bool" => {
516 if let Some(toml::Value::Boolean(b)) = &exp.default {
517 module.variable_table.set(
518 format!("{}.{}", alias, exp.name),
519 Value::Boolean(*b)
520 );
521 }
522 }
523 "synth" => {
524 module.variable_table.set(
526 format!("{}.{}", alias, exp.name),
527 Value::String(format!("{}.{}", alias, exp.name))
528 );
529 }
530 _ => {
531 if let Some(def) = &exp.default {
533 let val = match def {
534 toml::Value::String(s) => Value::String(s.clone()),
535 toml::Value::Integer(i) => Value::Number(*i as f32),
536 toml::Value::Float(f) => Value::Number(*f as f32),
537 toml::Value::Boolean(b) => Value::Boolean(*b),
538 toml::Value::Array(arr) =>
539 Value::Array(
540 arr
541 .iter()
542 .map(|v| {
543 match v {
544 toml::Value::String(s) => {
545 Value::String(s.clone())
546 }
547 toml::Value::Integer(i) => {
548 Value::Number(*i as f32)
549 }
550 toml::Value::Float(f) => {
551 Value::Number(*f as f32)
552 }
553 toml::Value::Boolean(b) =>
554 Value::Boolean(*b),
555 _ => Value::Null,
556 }
557 })
558 .collect()
559 ),
560 toml::Value::Table(t) => {
561 let mut m = std::collections::HashMap::new();
562 for (k, v) in t.iter() {
563 let vv = match v {
564 toml::Value::String(s) => {
565 Value::String(s.clone())
566 }
567 toml::Value::Integer(i) => {
568 Value::Number(*i as f32)
569 }
570 toml::Value::Float(f) => {
571 Value::Number(*f as f32)
572 }
573 toml::Value::Boolean(b) => Value::Boolean(*b),
574 _ => Value::Null,
575 };
576 m.insert(k.clone(), vv);
577 }
578 Value::Map(m)
579 }
580 _ => Value::Null,
581 };
582 if val != Value::Null {
583 module.variable_table.set(
584 format!("{}.{}", alias, exp.name),
585 val
586 );
587 }
588 }
589 }
590 }
591 }
592 }
593 }
594 Err(e) => eprintln!("Failed to load plugin {}: {}", plugin_name, e),
595 }
596 }
597}