rukalang/
source_index.rs

1use std::collections::BTreeMap;
2
3/// Build a best-effort map from function names to declaration lines.
4pub(crate) fn build_function_line_map(source_text: &str) -> BTreeMap<String, usize> {
5    let mut lines = BTreeMap::<String, usize>::new();
6    for (line_index, line) in source_text.lines().enumerate() {
7        if let Some(name) = parse_function_name(line) {
8            lines.entry(name).or_insert(line_index + 1);
9        }
10    }
11    lines
12}
13
14/// Parse one function declaration name from one source line.
15fn parse_function_name(line: &str) -> Option<String> {
16    let trimmed = line.trim_start();
17    let after_fn = if let Some(rest) = trimmed.strip_prefix("fn ") {
18        rest
19    } else if let Some(rest) = trimmed.strip_prefix("meta fn ") {
20        rest
21    } else {
22        return None;
23    };
24
25    let mut name = String::new();
26    for ch in after_fn.chars() {
27        if ch == '_' || ch.is_ascii_alphanumeric() {
28            name.push(ch);
29        } else {
30            break;
31        }
32    }
33
34    if name.is_empty() { None } else { Some(name) }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::build_function_line_map;
40
41    /// Map runtime and meta declarations to source lines.
42    #[test]
43    fn maps_runtime_and_meta_function_lines() {
44        let source = r#"
45fn main() -> Unit {}
46meta fn expand() -> Expr[i64] { expr { 0 } }
47"#;
48        let lines = build_function_line_map(source);
49        assert_eq!(lines.get("main"), Some(&2));
50        assert_eq!(lines.get("expand"), Some(&3));
51    }
52
53    /// Keep the first line for duplicate names.
54    #[test]
55    fn keeps_first_line_for_duplicate_names() {
56        let source = r#"
57fn helper() -> Unit {}
58fn helper() -> Unit {}
59"#;
60        let lines = build_function_line_map(source);
61        assert_eq!(lines.get("helper"), Some(&2));
62    }
63}