devalang_core/core/preprocessor/
loader.rs

1use crate::core::preprocessor::resolver::driver::{
2    resolve_all_modules, resolve_and_flatten_all_modules,
3};
4use crate::core::utils::path::resolve_relative_path;
5use crate::{
6    config::loader::load_config,
7    core::{
8        error::ErrorHandler,
9        lexer::{Lexer, token::Token},
10        parser::{
11            driver::Parser,
12            statement::{Statement, StatementKind},
13        },
14        plugin::loader::load_plugin,
15        preprocessor::{module::Module, processor::process_modules},
16        shared::{bank::BankFile, value::Value},
17        store::global::GlobalStore,
18        utils::path::normalize_path,
19    },
20    utils::logger::Logger,
21};
22use std::{collections::HashMap, path::Path};
23
24pub struct ModuleLoader {
25    pub entry: String,
26    pub output: String,
27    pub base_dir: String,
28}
29
30impl ModuleLoader {
31    pub fn new(entry: &str, output: &str) -> Self {
32        let base_dir = Path::new(entry)
33            .parent()
34            .unwrap_or(Path::new(""))
35            .to_string_lossy()
36            .replace('\\', "/");
37
38        Self {
39            entry: entry.to_string(),
40            output: output.to_string(),
41            base_dir: base_dir,
42        }
43    }
44
45    pub fn from_raw_source(
46        entry_path: &str,
47        output_path: &str,
48        content: &str,
49        global_store: &mut GlobalStore,
50    ) -> Self {
51        let normalized_entry_path = normalize_path(entry_path);
52
53        let mut module = Module::new(&entry_path);
54        module.content = content.to_string();
55
56        global_store.insert_module(normalized_entry_path.to_string(), module);
57
58        Self {
59            entry: normalized_entry_path.to_string(),
60            output: output_path.to_string(),
61            base_dir: "".to_string(),
62        }
63    }
64
65    pub fn extract_statements_map(
66        &self,
67        global_store: &GlobalStore,
68    ) -> HashMap<String, Vec<Statement>> {
69        global_store
70            .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
78            .modules
79            .remove(&self.entry)
80            .ok_or_else(|| format!("Module not found in store for path: {}", self.entry))?;
81
82        // SECTION Lexing the module content
83        let lexer = Lexer::new();
84        let tokens = lexer
85            .lex_from_source(&module.content)
86            .map_err(|e| format!("Lexer failed: {}", e))?;
87
88        module.tokens = tokens.clone();
89
90        // SECTION Parsing tokens into statements
91        let mut parser = Parser::new();
92        parser.set_current_module(self.entry.clone());
93        let statements = parser.parse_tokens(tokens, global_store);
94        module.statements = statements;
95
96        // SECTION Injecting bank triggers if any (legacy default for single-module run)
97        if let Err(e) = self.inject_bank_triggers(&mut module, "808", None) {
98            return Err(format!("Failed to inject bank triggers: {}", e));
99        }
100
101        for (plugin_name, alias) in self.extract_plugin_uses(&module.statements) {
102            self.load_plugin_and_register(&mut module, &plugin_name, &alias, global_store);
103        }
104
105        global_store
106            .modules
107            .insert(self.entry.clone(), module.clone());
108
109        // SECTION Error handling
110        let mut error_handler = ErrorHandler::new();
111        error_handler.detect_from_statements(&mut parser, &module.statements);
112
113        Ok(module)
114    }
115
116    pub fn load_wasm_module(&self, global_store: &mut GlobalStore) -> Result<(), String> {
117        // Step one : Load the module from the global store
118        let module = {
119            let module_ref = global_store
120                .modules
121                .get(&self.entry)
122                .ok_or_else(|| format!("❌ Module not found for path: {}", self.entry))?;
123
124            Module::from_existing(&self.entry, module_ref.content.clone())
125        };
126
127        // Step two : lexing
128        let lexer = Lexer::new();
129        let tokens = lexer
130            .lex_from_source(&module.content)
131            .map_err(|e| format!("Lexer failed: {}", e))?;
132
133        // Step three : parsing
134        let mut parser = Parser::new();
135        parser.set_current_module(self.entry.clone());
136
137        let statements = parser.parse_tokens(tokens.clone(), global_store);
138
139        let mut updated_module = module;
140        updated_module.tokens = tokens;
141        updated_module.statements = statements;
142
143        // Step four : Injecting bank triggers if any
144        if let Err(e) = self.inject_bank_triggers(&mut updated_module, "808", None) {
145            return Err(format!("Failed to inject bank triggers: {}", e));
146        }
147
148        for (plugin_name, alias) in self.extract_plugin_uses(&updated_module.statements) {
149            self.load_plugin_and_register(&mut updated_module, &plugin_name, &alias, global_store);
150        }
151
152        // Step four : error handling
153        let mut error_handler = ErrorHandler::new();
154        error_handler.detect_from_statements(&mut parser, &updated_module.statements);
155
156        // Final step : insert the updated module back into the global store
157        global_store
158            .modules
159            .insert(self.entry.clone(), updated_module);
160
161        Ok(())
162    }
163
164    #[cfg(feature = "cli")]
165    pub fn load_all_modules(
166        &self,
167        global_store: &mut GlobalStore,
168    ) -> (HashMap<String, Vec<Token>>, HashMap<String, Vec<Statement>>) {
169        // SECTION Load the entry module and its dependencies
170        let tokens_by_module = self.load_module_recursively(&self.entry, global_store);
171
172        // SECTION Process and resolve modules
173        process_modules(self, global_store);
174        resolve_all_modules(self, global_store);
175
176        // SECTION Flatten all modules to get statements (+ injects)
177        let statements_by_module = resolve_and_flatten_all_modules(global_store);
178
179        (tokens_by_module, statements_by_module)
180    }
181
182    #[cfg(feature = "cli")]
183    fn load_module_recursively(
184        &self,
185        raw_path: &str,
186        global_store: &mut GlobalStore,
187    ) -> HashMap<String, Vec<Token>> {
188        let path = normalize_path(raw_path);
189
190        // Check if already loaded
191        if global_store.modules.contains_key(&path) {
192            return HashMap::new();
193        }
194
195        let lexer = Lexer::new();
196        let tokens = lexer.lex_tokens(&path);
197
198        let mut parser = Parser::new();
199        parser.set_current_module(path.clone());
200
201        let statements = parser.parse_tokens(tokens.clone(), global_store);
202
203        // Insert module into store
204        let mut module = Module::new(&path);
205        module.tokens = tokens.clone();
206        module.statements = statements.clone();
207
208        // Inject triggers for each bank used in module, respecting aliases
209        for (bank_name, alias_opt) in self.extract_bank_decls(&statements) {
210            if let Err(e) = self.inject_bank_triggers(&mut module, &bank_name, alias_opt) {
211                eprintln!("Failed to inject bank triggers for '{}': {}", bank_name, e);
212            }
213        }
214
215        for (plugin_name, alias) in self.extract_plugin_uses(&statements) {
216            self.load_plugin_and_register(&mut module, &plugin_name, &alias, global_store);
217        }
218
219        // Inject module variables and functions into global store
220        global_store
221            .variables
222            .variables
223            .extend(module.variable_table.variables.clone());
224        global_store
225            .functions
226            .functions
227            .extend(module.function_table.functions.clone());
228
229        // Inject the module into the global store
230        global_store.insert_module(path.clone(), module);
231
232        // Load dependencies
233        self.load_module_imports(&path, global_store);
234
235        // Error handling (use the module now in the store to include injected errors)
236        let mut error_handler = ErrorHandler::new();
237        if let Some(current_module) = global_store.modules.get(&path) {
238            error_handler.detect_from_statements(&mut parser, &current_module.statements);
239        } else {
240            error_handler.detect_from_statements(&mut parser, &statements);
241        }
242
243        if error_handler.has_errors() {
244            let logger = Logger::new();
245            for error in error_handler.get_errors() {
246                let trace = format!("{}:{}:{}", path, error.line, error.column);
247                logger.log_error_with_stacktrace(&error.message, &trace);
248            }
249        }
250
251        // Return tokens per module
252        global_store
253            .modules
254            .iter()
255            .map(|(p, m)| (p.clone(), m.tokens.clone()))
256            .collect()
257    }
258
259    #[cfg(feature = "cli")]
260    fn load_module_imports(&self, path: &String, global_store: &mut GlobalStore) {
261        let import_paths: Vec<String> = {
262            let current_module = match global_store.modules.get(path) {
263                Some(module) => module,
264                None => {
265                    eprintln!(
266                        "[warn] Cannot resolve imports: module '{}' not found in store",
267                        path
268                    );
269                    return;
270                }
271            };
272
273            current_module
274                .statements
275                .iter()
276                .filter_map(|stmt| {
277                    if let StatementKind::Import { source, .. } = &stmt.kind {
278                        Some(source.clone())
279                    } else {
280                        None
281                    }
282                })
283                .collect()
284        };
285
286        for import_path in import_paths {
287            let resolved = resolve_relative_path(path, &import_path);
288            self.load_module_recursively(&resolved, global_store);
289        }
290    }
291
292    pub fn inject_bank_triggers(
293        &self,
294        module: &mut Module,
295        bank_name: &str,
296        alias_override: Option<String>,
297    ) -> Result<Module, String> {
298        let default_alias = bank_name.split('.').last().unwrap_or(bank_name).to_string();
299        let alias_ref = alias_override.as_deref().unwrap_or(&default_alias);
300
301        let bank_path = Path::new("./.deva/bank").join(bank_name);
302        let bank_toml_path = bank_path.join("bank.toml");
303
304        if !bank_toml_path.exists() {
305            return Ok(module.clone());
306        }
307
308        let content = std::fs::read_to_string(&bank_toml_path)
309            .map_err(|e| format!("Failed to read '{}': {}", bank_toml_path.display(), e))?;
310
311        let parsed_bankfile: BankFile = toml::from_str(&content)
312            .map_err(|e| format!("Failed to parse '{}': {}", bank_toml_path.display(), e))?;
313
314        let mut bank_map = HashMap::new();
315
316        for bank_trigger in parsed_bankfile.triggers.unwrap_or_default() {
317            let trigger_name = bank_trigger.name.clone().replace("./", "");
318            let bank_trigger_path = format!("devalang://bank/{}/{}", bank_name, trigger_name);
319
320            bank_map.insert(
321                bank_trigger.name.clone(),
322                Value::String(bank_trigger_path.clone()),
323            );
324
325            if module.variable_table.variables.contains_key(alias_ref) {
326                eprintln!(
327                    "⚠️ Trigger '{}' already defined in module '{}', skipping injection.",
328                    alias_ref, module.path
329                );
330                continue;
331            }
332
333            module.variable_table.set(
334                format!("{}.{}", alias_ref, bank_trigger.name),
335                Value::String(bank_trigger_path.clone()),
336            );
337        }
338
339        // Inject the map under the bank name
340        module
341            .variable_table
342            .set(alias_ref.to_string(), Value::Map(bank_map));
343
344        Ok(module.clone())
345    }
346
347    fn extract_bank_decls(&self, statements: &[Statement]) -> Vec<(String, Option<String>)> {
348        let mut banks = Vec::new();
349
350        for stmt in statements {
351            if let StatementKind::Bank { alias } = &stmt.kind {
352                let name_opt = match &stmt.value {
353                    Value::String(s) => Some(s.clone()),
354                    Value::Identifier(s) => Some(s.clone()),
355                    Value::Number(n) => Some(n.to_string()),
356                    _ => None,
357                };
358                if let Some(name) = name_opt {
359                    banks.push((name, alias.clone()));
360                }
361            }
362        }
363
364        banks
365    }
366
367    fn extract_plugin_uses(&self, statements: &[Statement]) -> Vec<(String, String)> {
368        let mut plugins = Vec::new();
369
370        for stmt in statements {
371            if let StatementKind::Use { name, alias } = &stmt.kind {
372                let alias_name = alias
373                    .clone()
374                    .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
375                plugins.push((name.clone(), alias_name));
376            }
377        }
378
379        plugins
380    }
381
382    fn load_plugin_and_register(
383        &self,
384        module: &mut Module,
385        plugin_name: &str,
386        alias: &str,
387        global_store: &mut GlobalStore,
388    ) {
389        // plugin_name expected format: "author.name"
390        let mut parts = plugin_name.split('.');
391        let author = match parts.next() {
392            Some(a) if !a.is_empty() => a,
393            _ => {
394                eprintln!("Invalid plugin name '{}': missing author", plugin_name);
395                return;
396            }
397        };
398        let name = match parts.next() {
399            Some(n) if !n.is_empty() => n,
400            _ => {
401                eprintln!("Invalid plugin name '{}': missing name", plugin_name);
402                return;
403            }
404        };
405        if parts.next().is_some() {
406            eprintln!(
407                "Invalid plugin name '{}': expected <author>.<name>",
408                plugin_name
409            );
410            return;
411        }
412
413        // Enforce presence in .devalang config when plugin exists locally
414        // Build expected URI from author/name
415        let expected_uri = format!("devalang://plugin/{}.{}", author, name);
416
417        // Detect local presence (preferred and legacy layouts)
418        let root = Path::new("./.deva");
419        let plugin_dir_preferred = root.join("plugin").join(format!("{}.{}", author, name));
420        let toml_path_preferred = plugin_dir_preferred.join("plugin.toml");
421        let plugin_dir_fallback = root.join("plugin").join(author).join(name);
422        let toml_path_fallback = plugin_dir_fallback.join("plugin.toml");
423        let exists_locally = toml_path_preferred.exists() || toml_path_fallback.exists();
424
425        if exists_locally {
426            // Load config and verify plugin is declared
427            let cfg_opt = load_config(None);
428            let mut declared = false;
429            if let Some(cfg) = cfg_opt {
430                if let Some(list) = cfg.plugins {
431                    declared = list.iter().any(|p| p.path == expected_uri);
432                }
433            }
434            if !declared {
435                // Inject a single, clear error into the module so it is reported once by the error handler
436                module.statements.push(Statement {
437                    kind: StatementKind::Error {
438                        message: "plugin present in local files but missing in .devalang config"
439                            .to_string(),
440                    },
441                    value: Value::Null,
442                    indent: 0,
443                    line: 0,
444                    column: 0,
445                });
446                return;
447            }
448        }
449
450        match load_plugin(author, name) {
451            Ok((info, wasm)) => {
452                let uri = format!("devalang://plugin/{}.{}", author, name);
453                global_store
454                    .plugins
455                    .insert(format!("{}:{}", author, name), (info, wasm));
456                // Set alias to URI, and inject exported variables
457                module
458                    .variable_table
459                    .set(alias.to_string(), Value::String(uri.clone()));
460                if let Some((plugin_info, _)) =
461                    global_store.plugins.get(&format!("{}:{}", author, name))
462                {
463                    for exp in &plugin_info.exports {
464                        match exp.kind.as_str() {
465                            "number" => {
466                                if let Some(toml::Value::String(s)) = &exp.default {
467                                    if let Ok(n) = s.parse::<f32>() {
468                                        module.variable_table.set(
469                                            format!("{}.{}", alias, exp.name),
470                                            Value::Number(n),
471                                        );
472                                    }
473                                } else if let Some(toml::Value::Integer(i)) = &exp.default {
474                                    module.variable_table.set(
475                                        format!("{}.{}", alias, exp.name),
476                                        Value::Number(*i as f32),
477                                    );
478                                } else if let Some(toml::Value::Float(f)) = &exp.default {
479                                    module.variable_table.set(
480                                        format!("{}.{}", alias, exp.name),
481                                        Value::Number(*f as f32),
482                                    );
483                                }
484                            }
485                            "string" => {
486                                if let Some(toml::Value::String(s)) = &exp.default {
487                                    module.variable_table.set(
488                                        format!("{}.{}", alias, exp.name),
489                                        Value::String(s.clone()),
490                                    );
491                                }
492                            }
493                            "bool" => {
494                                if let Some(toml::Value::Boolean(b)) = &exp.default {
495                                    module
496                                        .variable_table
497                                        .set(format!("{}.{}", alias, exp.name), Value::Boolean(*b));
498                                }
499                            }
500                            "synth" => {
501                                // Provide a discoverable marker: alias.<synthName> resolves to alias.synthName waveform string
502                                module.variable_table.set(
503                                    format!("{}.{}", alias, exp.name),
504                                    Value::String(format!("{}.{}", alias, exp.name)),
505                                );
506                            }
507                            _ => {}
508                        }
509                    }
510                }
511            }
512            Err(e) => eprintln!("Failed to load plugin {}: {}", plugin_name, e),
513        }
514    }
515}