devalang_core/core/preprocessor/
loader.rs

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        // Insert a module stub containing the provided content into the
58        // global store. This is used by the WASM APIs and tests which
59        // operate on in-memory sources instead of files on disk.
60        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        // SECTION Lexing the module content
87        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        // SECTION Parsing tokens into statements
95        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        // SECTION Injecting bank triggers if any (legacy default for single-module run)
101        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        // SECTION Error handling
114        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        // Step one : Load the module from the global store
122        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        // Step two : lexing
132        let lexer = Lexer::new();
133        let tokens = lexer
134            .lex_from_source(&module.content)
135            .map_err(|e| format!("Lexer failed: {}", e))?;
136
137        // Step three : parsing
138        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        // Step four : Injecting bank triggers if any
148        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        // Insert the updated module into the global store before processing so
153        // process_modules can operate on it and populate variable_table, imports,
154        // and other derived structures.
155        global_store
156            .modules
157            .insert(self.entry.clone(), updated_module.clone());
158
159        // Process modules to populate module.variable_table, import/export tables,
160        // and other derived structures so runtime execution can resolve groups/synths.
161        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        // Step four : error handling
168        let mut error_handler = ErrorHandler::new();
169        error_handler.detect_from_statements(&mut parser, &updated_module.statements);
170
171        // Final step : also expose module-level variables and functions into the global store
172        // so runtime evaluation (render_audio) can find group/synth definitions.
173        // Use the module instance that was actually processed by `process_modules`
174        // (it lives in `global_store.modules`) because `updated_module` is a local
175        // clone and won't contain the mutations applied by `process_modules`.
176        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            // Fallback to the local updated_module if for any reason the module
187            // wasn't inserted into the store (defensive programming).
188            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        // SECTION Load the entry module and its dependencies
207        let tokens_by_module = self.load_module_recursively(&self.entry, global_store);
208
209        // SECTION Process and resolve modules
210        process_modules(self, global_store);
211        resolve_all_modules(self, global_store);
212
213        // SECTION Flatten all modules to get statements (+ injects)
214        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        // Check if already loaded
228        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        // Insert module into store
248        let mut module = Module::new(&path);
249        module.tokens = tokens.clone();
250        module.statements = statements.clone();
251
252        // Inject triggers for each bank used in module, respecting aliases
253        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        // Inject module variables and functions into global store
264        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        // Inject the module into the global store
274        global_store.insert_module(path.clone(), module);
275
276        // Load dependencies
277        self.load_module_imports(&path, global_store);
278
279        // Error handling (use the module now in the store to include injected errors)
280        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, &current_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        // Return tokens per module
296        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            // Use the configured path from the bank file as the entity reference so
369            // that bank entries can point to files or nested paths. Clean common
370            // local prefixes like "./" to keep the URI tidy.
371            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            // Keep the trigger key as declared (bank_trigger.name) but expose its
379            // value as a devalang://bank URI pointing to the configured path.
380            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        // Inject the map under the bank name
400        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        // plugin_name expected format: "author.name"
451        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        // Enforce presence in .devalang config when plugin exists locally
475        // Build expected URI from author/name
476        let expected_uri = format!("devalang://plugin/{}.{}", author, name);
477
478        // Detect local presence (preferred and legacy layouts)
479        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            // Load config and verify plugin is declared
491            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                // Inject a single, clear error into the module so it is reported once by the error handler
500                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                // Set alias to URI, and inject exported variables
521                module
522                    .variable_table
523                    .set(alias.to_string(), Value::String(uri.clone()));
524                // Also expose alias at global level so runtime can resolve it
525                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                                // Provide a discoverable marker: alias.<synthName> resolves to alias.synthName waveform string
571                                module.variable_table.set(
572                                    format!("{}.{}", alias, exp.name),
573                                    Value::String(format!("{}.{}", alias, exp.name)),
574                                );
575                            }
576                            _ => {
577                                // Fallback: if default is present, map it to a Value dynamically
578                                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}