Skip to main content

aver/
source.rs

1use std::path::{Path, PathBuf};
2
3use crate::ast::TopLevel;
4use crate::lexer::Lexer;
5use crate::parser::Parser;
6
7pub fn parse_source(source: &str) -> Result<Vec<TopLevel>, String> {
8    let mut lexer = Lexer::new(source);
9    let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
10    let mut parser = Parser::new(tokens);
11    parser.parse().map_err(|e| e.to_string())
12}
13
14/// Enforce module contract for file-based programs:
15/// exactly one `module` declaration and it must be the first top-level item.
16pub fn require_module_declaration(items: &[TopLevel], file: &str) -> Result<(), String> {
17    let module_positions: Vec<usize> = items
18        .iter()
19        .enumerate()
20        .filter_map(|(idx, item)| matches!(item, TopLevel::Module(_)).then_some(idx))
21        .collect();
22
23    if module_positions.is_empty() {
24        return Err(format!(
25            "File '{}' must declare `module <Name>` as the first top-level item",
26            file
27        ));
28    }
29
30    if module_positions[0] != 0 {
31        return Err(format!(
32            "File '{}' must place `module <Name>` as the first top-level item",
33            file
34        ));
35    }
36
37    if module_positions.len() > 1 {
38        return Err(format!(
39            "File '{}' must contain exactly one module declaration (found {})",
40            file,
41            module_positions.len()
42        ));
43    }
44
45    Ok(())
46}
47
48pub fn find_module_file(name: &str, module_root: &str) -> Option<PathBuf> {
49    let root = Path::new(module_root);
50    let parts: Vec<&str> = name.split('.').filter(|s| !s.is_empty()).collect();
51    if parts.is_empty() {
52        return None;
53    }
54
55    let lower_rel = format!(
56        "{}.av",
57        parts
58            .iter()
59            .map(|p| p.to_lowercase())
60            .collect::<Vec<_>>()
61            .join("/")
62    );
63    let exact_rel = format!("{}.av", parts.join("/"));
64
65    let lower = root.join(&lower_rel);
66    if lower.exists() {
67        return Some(lower);
68    }
69
70    let exact = root.join(&exact_rel);
71    if exact.exists() {
72        return Some(exact);
73    }
74
75    None
76}
77
78pub fn canonicalize_path(path: &Path) -> PathBuf {
79    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::{parse_source, require_module_declaration};
85
86    #[test]
87    fn require_module_accepts_single_first_module() {
88        let src = "module Demo\n    intent = \"ok\"\nfn x() -> Int\n    = 1\n";
89        let items = parse_source(src).expect("parse");
90        require_module_declaration(&items, "demo.av").expect("module declaration should pass");
91    }
92
93    #[test]
94    fn require_module_rejects_missing_module() {
95        let src = "fn x() -> Int\n    = 1\n";
96        let items = parse_source(src).expect("parse");
97        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
98        assert!(err.contains("must declare `module <Name>`"));
99    }
100
101    #[test]
102    fn require_module_rejects_module_not_first() {
103        let src = "fn x() -> Int\n    = 1\nmodule Demo\n";
104        let items = parse_source(src).expect("parse");
105        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
106        assert!(err.contains("must place `module <Name>` as the first"));
107    }
108
109    #[test]
110    fn require_module_rejects_multiple_modules() {
111        let src = "module A\nmodule B\n";
112        let items = parse_source(src).expect("parse");
113        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
114        assert!(err.contains("exactly one module declaration"));
115    }
116}