devalang_core/core/preprocessor/
loader.rs

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        // Insert a module stub containing the provided content into the
55        // global store. This is used by the WASM APIs and tests which
56        // operate on in-memory sources instead of files on disk.
57        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        // SECTION Lexing the module content
82        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        // SECTION Parsing tokens into statements
90        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        // SECTION Injecting bank triggers if any (legacy default for single-module run)
96        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        // SECTION Error handling
107        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        // Step one : Load the module from the global store
115        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        // Step two : lexing
124        let lexer = Lexer::new();
125        let tokens = lexer
126            .lex_from_source(&module.content)
127            .map_err(|e| format!("Lexer failed: {}", e))?;
128
129        // Step three : parsing
130        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        // Step four : Injecting bank triggers if any
140        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        // Insert the updated module into the global store before processing so
145        // process_modules can operate on it and populate variable_table, imports,
146        // and other derived structures.
147        global_store.modules.insert(self.entry.clone(), updated_module.clone());
148
149        // Process modules to populate module.variable_table, import/export tables,
150        // and other derived structures so runtime execution can resolve groups/synths.
151        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        // Step four : error handling
158        let mut error_handler = ErrorHandler::new();
159        error_handler.detect_from_statements(&mut parser, &updated_module.statements);
160
161        // Final step : also expose module-level variables and functions into the global store
162        // so runtime evaluation (render_audio) can find group/synth definitions.
163        // Use the module instance that was actually processed by `process_modules`
164        // (it lives in `global_store.modules`) because `updated_module` is a local
165        // clone and won't contain the mutations applied by `process_modules`.
166        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            // Fallback to the local updated_module if for any reason the module
171            // wasn't inserted into the store (defensive programming).
172            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        // SECTION Load the entry module and its dependencies
189        let tokens_by_module = self.load_module_recursively(&self.entry, global_store);
190
191        // SECTION Process and resolve modules
192        process_modules(self, global_store);
193        resolve_all_modules(self, global_store);
194
195        // SECTION Flatten all modules to get statements (+ injects)
196        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        // Check if already loaded
210        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        // Insert module into store
230        let mut module = Module::new(&path);
231        module.tokens = tokens.clone();
232        module.statements = statements.clone();
233
234        // Inject triggers for each bank used in module, respecting aliases
235        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        // Inject module variables and functions into global store
246        global_store.variables.variables.extend(module.variable_table.variables.clone());
247        global_store.functions.functions.extend(module.function_table.functions.clone());
248
249        // Inject the module into the global store
250        global_store.insert_module(path.clone(), module);
251
252        // Load dependencies
253        self.load_module_imports(&path, global_store);
254
255        // Error handling (use the module now in the store to include injected errors)
256        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, &current_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        // Return tokens per module
272        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            // Use the configured path from the bank file as the entity reference so
338            // that bank entries can point to files or nested paths. Clean common
339            // local prefixes like "./" to keep the URI tidy.
340            let entity_ref = bank_trigger.path.clone().replace("\\", "/").replace("./", "");
341            let bank_trigger_path = format!("devalang://bank/{}/{}", bank_name, entity_ref);
342
343            // Keep the trigger key as declared (bank_trigger.name) but expose its
344            // value as a devalang://bank URI pointing to the configured path.
345            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        // Inject the map under the bank name
363        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        // plugin_name expected format: "author.name"
412        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        // Enforce presence in .devalang config when plugin exists locally
433        // Build expected URI from author/name
434        let expected_uri = format!("devalang://plugin/{}.{}", author, name);
435
436        // Detect local presence (preferred and legacy layouts)
437        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            // Load config and verify plugin is declared
449            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                // Inject a single, clear error into the module so it is reported once by the error handler
458                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                // Set alias to URI, and inject exported variables
476                module.variable_table.set(alias.to_string(), Value::String(uri.clone()));
477                // Also expose alias at global level so runtime can resolve it
478                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                                // Provide a discoverable marker: alias.<synthName> resolves to alias.synthName waveform string
525                                module.variable_table.set(
526                                    format!("{}.{}", alias, exp.name),
527                                    Value::String(format!("{}.{}", alias, exp.name))
528                                );
529                            }
530                            _ => {
531                                // Fallback: if default is present, map it to a Value dynamically
532                                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}