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