Skip to main content

uni_locy/compiler/
modules.rs

1use std::collections::HashMap;
2
3use uni_cypher::locy_ast::{LocyProgram, UseDecl};
4
5use super::errors::LocyCompileError;
6
7/// Resolved module context: maps short rule names to qualified names.
8#[derive(Debug, Clone, Default)]
9pub struct ModuleContext {
10    /// The current module's qualified name (e.g., "acme.compliance").
11    pub module_name: Option<String>,
12    /// Imported rule names: short name → qualified module + rule name.
13    pub imports: HashMap<String, String>,
14}
15
16/// Build a module context from a program's MODULE and USE declarations.
17pub fn resolve_modules(
18    program: &LocyProgram,
19    available_modules: &HashMap<String, Vec<String>>,
20) -> Result<ModuleContext, LocyCompileError> {
21    let mut ctx = ModuleContext::default();
22
23    if let Some(module_decl) = &program.module {
24        ctx.module_name = Some(module_decl.name.to_string());
25    }
26
27    for use_decl in &program.uses {
28        resolve_use(&mut ctx, use_decl, available_modules)?;
29    }
30
31    Ok(ctx)
32}
33
34fn resolve_use(
35    ctx: &mut ModuleContext,
36    use_decl: &UseDecl,
37    available_modules: &HashMap<String, Vec<String>>,
38) -> Result<(), LocyCompileError> {
39    let module_name = use_decl.name.to_string();
40    if let Some(rules) = available_modules.get(&module_name) {
41        match &use_decl.imports {
42            None => {
43                // Glob import: import all rules from the module
44                for rule in rules {
45                    ctx.imports
46                        .insert(rule.clone(), format!("{}.{}", module_name, rule));
47                }
48            }
49            Some(selected) => {
50                // Selective import: only import listed rules
51                for name in selected {
52                    if rules.contains(name) {
53                        ctx.imports
54                            .insert(name.clone(), format!("{}.{}", module_name, name));
55                    } else {
56                        return Err(LocyCompileError::ImportNotFound {
57                            module: module_name,
58                            rule: name.clone(),
59                        });
60                    }
61                }
62            }
63        }
64        Ok(())
65    } else {
66        Err(LocyCompileError::ModuleNotFound { name: module_name })
67    }
68}
69
70/// Resolve a rule name using the module context.
71/// Returns the canonical name (qualified if from an import, otherwise as-is).
72pub fn resolve_rule_name(ctx: &ModuleContext, name: &str) -> String {
73    if let Some(qualified) = ctx.imports.get(name) {
74        qualified.clone()
75    } else if let Some(module) = &ctx.module_name {
76        // If no dot in name and we have a module, qualify it
77        if !name.contains('.') {
78            format!("{}.{}", module, name)
79        } else {
80            name.to_string()
81        }
82    } else {
83        name.to_string()
84    }
85}