Skip to main content

haskelujah_parser/
lib.rs

1// For God so loved the world that he gave his only begotten Son, that whoever
2// believes in him should not perish but have eternal life. — John 3:16
3
4//! # haskelujah-parser-chirho
5//!
6//! Full recursive-descent parser for Haskell source files with layout rule support.
7//! The main entry point is [`cst_parser_chirho::ParserChirho`] which produces a
8//! lossless green CST. [`lower_chirho::lower_module_chirho`] converts the CST to
9//! an AST. A lightweight [`scan_module_header_chirho`] scanner is also provided
10//! for quick module-name extraction without full parsing.
11
12pub mod cst_parser_chirho;
13pub mod layout_chirho;
14pub mod lexer_chirho;
15pub mod lower_chirho;
16#[cfg(test)]
17mod proptest_chirho;
18
19use haskelujah_diagnostics_chirho::{DiagnosticBundleChirho, DiagnosticChirho, ErrorCodeChirho};
20use haskelujah_span_chirho::{ByteOffsetChirho, SpanChirho};
21use haskelujah_syntax_chirho::{ModuleHeaderChirho, SourceFileChirho};
22
23pub const DEFAULT_MODULE_NAME_CHIRHO: &str = "Main";
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ParsedModuleChirho {
27    pub module_header_chirho: ModuleHeaderChirho,
28    pub source_file_chirho: SourceFileChirho,
29}
30
31/// Scan a Haskell source file for its `module … where` header line.
32///
33/// This is a lightweight string-based scanner, NOT a full parser.  It only
34/// extracts the module name (defaulting to `"Main"` when no header is present).
35/// For real parsing, use the CST parser in [`cst_parser_chirho`].
36pub fn scan_module_header_chirho(
37    source_file_chirho: SourceFileChirho,
38) -> Result<ParsedModuleChirho, DiagnosticBundleChirho> {
39    let file_id_chirho = source_file_chirho.file_id_chirho();
40    let mut saw_non_comment_code_chirho = false;
41    let mut byte_offset_chirho: usize = 0;
42
43    for raw_line_chirho in source_file_chirho.contents_chirho().lines() {
44        let line_start_chirho = byte_offset_chirho;
45        let line_len_chirho = raw_line_chirho.len();
46        // Advance past this line + newline
47        byte_offset_chirho += line_len_chirho + 1; // +1 for \n
48
49        let trimmed_line_chirho = raw_line_chirho.trim();
50
51        if trimmed_line_chirho.is_empty() {
52            continue;
53        }
54
55        if trimmed_line_chirho.starts_with("--") {
56            continue;
57        }
58
59        if let Some(module_header_chirho) =
60            parse_module_header_line_chirho(trimmed_line_chirho, file_id_chirho, line_start_chirho)?
61        {
62            return Ok(ParsedModuleChirho {
63                module_header_chirho,
64                source_file_chirho,
65            });
66        }
67
68        saw_non_comment_code_chirho = true;
69        break;
70    }
71
72    let span_chirho = if saw_non_comment_code_chirho {
73        SpanChirho::new_chirho(
74            file_id_chirho,
75            ByteOffsetChirho::new_chirho(0),
76            ByteOffsetChirho::new_chirho(0),
77        )
78    } else {
79        SpanChirho::DUMMY_CHIRHO
80    };
81
82    Ok(ParsedModuleChirho {
83        module_header_chirho: ModuleHeaderChirho {
84            module_name_chirho: DEFAULT_MODULE_NAME_CHIRHO.to_owned(),
85            span_chirho,
86        },
87        source_file_chirho,
88    })
89}
90
91fn parse_module_header_line_chirho(
92    trimmed_line_chirho: &str,
93    file_id_chirho: haskelujah_span_chirho::FileIdChirho,
94    line_start_offset_chirho: usize,
95) -> Result<Option<ModuleHeaderChirho>, DiagnosticBundleChirho> {
96    if !trimmed_line_chirho.starts_with("module ") {
97        return Ok(None);
98    }
99
100    let remainder_chirho = trimmed_line_chirho
101        .strip_prefix("module ")
102        .expect("module prefix was checked");
103    let remainder_chirho = remainder_chirho.trim();
104    let module_name_chirho = remainder_chirho
105        .strip_suffix(" where")
106        .or_else(|| remainder_chirho.strip_suffix("where"))
107        .map(str::trim)
108        .filter(|module_name_chirho| !module_name_chirho.is_empty());
109
110    match module_name_chirho {
111        Some(module_name_chirho) => {
112            let span_chirho = SpanChirho::new_chirho(
113                file_id_chirho,
114                ByteOffsetChirho::from_usize_chirho(line_start_offset_chirho),
115                ByteOffsetChirho::from_usize_chirho(
116                    line_start_offset_chirho + trimmed_line_chirho.len(),
117                ),
118            );
119            Ok(Some(ModuleHeaderChirho {
120                module_name_chirho: module_name_chirho.to_owned(),
121                span_chirho,
122            }))
123        }
124        None => {
125            let span_chirho = SpanChirho::new_chirho(
126                file_id_chirho,
127                ByteOffsetChirho::from_usize_chirho(line_start_offset_chirho),
128                ByteOffsetChirho::from_usize_chirho(
129                    line_start_offset_chirho + trimmed_line_chirho.len(),
130                ),
131            );
132            Err(DiagnosticChirho::error_with_code_chirho(
133                ErrorCodeChirho::error_chirho(1),
134                "malformed module header; expected `module <Name> where`",
135                span_chirho,
136            )
137            .into())
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests_chirho {
144    use super::{DEFAULT_MODULE_NAME_CHIRHO, scan_module_header_chirho};
145    use haskelujah_span_chirho::SourceMapChirho;
146    use haskelujah_syntax_chirho::SourceFileChirho;
147
148    #[test]
149    fn parses_explicit_module_header_chirho() {
150        let mut source_map_chirho = SourceMapChirho::new_chirho();
151        let source_file_chirho = SourceFileChirho::from_source_map_chirho(
152            &mut source_map_chirho,
153            "SampleChirho.hs",
154            "module SampleChirho where\nvalueChirho = 1\n",
155        );
156
157        let parsed_module_chirho = scan_module_header_chirho(source_file_chirho)
158            .expect("parser should accept a valid module header");
159
160        assert_eq!(
161            parsed_module_chirho.module_header_chirho.module_name_chirho,
162            "SampleChirho"
163        );
164    }
165
166    #[test]
167    fn defaults_to_main_when_no_module_header_exists_chirho() {
168        let mut source_map_chirho = SourceMapChirho::new_chirho();
169        let source_file_chirho = SourceFileChirho::from_source_map_chirho(
170            &mut source_map_chirho,
171            "MainChirho.hs",
172            "mainChirho = putStrLn \"hi\"\n",
173        );
174
175        let parsed_module_chirho = scan_module_header_chirho(source_file_chirho)
176            .expect("parser should accept scripts without a module header");
177
178        assert_eq!(
179            parsed_module_chirho.module_header_chirho.module_name_chirho,
180            DEFAULT_MODULE_NAME_CHIRHO
181        );
182    }
183}