Skip to main content

nash_parse/
module.rs

1//! Module parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Module.hs`.
4//!
5//! Parses full modules including:
6//! - Module header: `module Main exposing (main)`
7//! - Imports: `import List exposing (map)`
8//! - Declarations: values, types, aliases
9
10use nash_region::{Located, Region};
11use nash_source::{Alias, Docs, Exposing, Import, Infix, Module, Union, Value};
12
13use crate::Parser;
14use crate::declaration::Decl;
15use crate::error;
16
17impl<'a> Parser<'a> {
18    /// Parse a module header.
19    ///
20    /// Mirrors Elm's `chompHeader` (simplified - no port/effect modules):
21    /// ```text
22    /// module_header = 'module' module_name 'exposing' exposing_list
23    /// ```
24    ///
25    /// Returns the module name and exports as a tuple.
26    pub fn module_header(
27        &mut self,
28    ) -> Result<(&'a Located<&'a str>, &'a Located<Exposing<'a>>), error::Module<'a>> {
29        // Match 'module' keyword
30        self.keyword_module(error::Module::Problem)?;
31
32        self.chomp_and_check_indent(error::Module::Space, error::Module::Name)?;
33
34        // Parse module name (e.g., "Json.Decode")
35        let start = self.get_position();
36        let name = self.module_name(error::Module::Name)?;
37        let module_name = self.add_end(start, name);
38
39        // Consume whitespace
40        self.chomp(error::Module::Space)?;
41
42        // Must have 'exposing' keyword
43        self.keyword_exposing(|row, col| {
44            error::Module::Exposing(self.bump.alloc(error::Exposing::Start(row, col)), row, col)
45        })?;
46
47        self.chomp_and_check_indent(error::Module::Space, |row, col| {
48            error::Module::Exposing(
49                self.bump.alloc(error::Exposing::IndentValue(row, col)),
50                row,
51                col,
52            )
53        })?;
54
55        // Parse the exposing list, wrapping errors
56        let exposing_start = self.get_position();
57        let exposing = self.specialize(
58            |bump, err, row, col| error::Module::Exposing(bump.alloc(err), row, col),
59            |p| p.exposing(),
60        )?;
61        let exposing_located = self.add_end(exposing_start, exposing);
62
63        Ok((module_name, exposing_located))
64    }
65
66    /// Parse zero or more import statements.
67    ///
68    /// Mirrors Elm's `chompImports`:
69    /// ```haskell
70    /// chompImports :: [Src.Import] -> Parser E.Module [Src.Import]
71    /// chompImports imports =
72    ///   oneOfWithFallback
73    ///     [ do  i <- chompImport
74    ///           chompImports (i:imports)
75    ///     ]
76    ///     (reverse imports)
77    /// ```
78    fn imports(&mut self) -> Result<&'a [&'a Import<'a>], error::Module<'a>> {
79        let mut imports = Vec::new();
80
81        loop {
82            // Save state in case import keyword doesn't match
83            let state = self.save_state();
84
85            match self.import() {
86                Ok(import) => {
87                    imports.push(import);
88                    // import() already ensures fresh line at the end
89                }
90                Err(_) => {
91                    // If we didn't consume input, we're done with imports
92                    if self.pos == state.pos {
93                        self.restore_state(state);
94                        break;
95                    }
96                    // Otherwise propagate the error
97                    return Err(error::Module::ImportStart(self.row, self.col));
98                }
99            }
100        }
101
102        Ok(self.alloc_slice_copy(&imports))
103    }
104
105    /// Parse zero or more declarations.
106    ///
107    /// Mirrors Elm's `chompDecls`:
108    /// ```haskell
109    /// chompDecls :: [Decl.Decl] -> Parser E.Decl [Decl.Decl]
110    /// chompDecls decls =
111    ///   do  (decl, _) <- Decl.declaration
112    ///       oneOfWithFallback
113    ///         [ do  Space.checkFreshLine E.DeclStart
114    ///               chompDecls (decl:decls)
115    ///         ]
116    ///         (reverse (decl:decls))
117    /// ```
118    fn declarations(&mut self) -> Result<Vec<Decl<'a>>, error::Module<'a>> {
119        let mut decls = Vec::new();
120
121        loop {
122            // Save state in case no declaration starts
123            let state = self.save_state();
124
125            // Try to parse a declaration
126            match self.specialize(
127                |bump, err, row, col| error::Module::Declarations(bump.alloc(err), row, col),
128                |p| p.declaration(),
129            ) {
130                Ok((decl, _end)) => {
131                    decls.push(decl);
132
133                    // Chomp any trailing whitespace
134                    self.chomp(error::Module::Space)?;
135
136                    // Check for fresh line (another declaration might follow)
137                    if self.is_eof() {
138                        break;
139                    }
140
141                    // If not at fresh line, we're done
142                    if self.col != 1 {
143                        break;
144                    }
145                }
146                Err(_) => {
147                    // If we didn't consume input, we're done with declarations
148                    if self.pos == state.pos {
149                        self.restore_state(state);
150                        break;
151                    }
152                    // Otherwise propagate the error
153                    return Err(error::Module::Declarations(
154                        self.bump.alloc(error::Decl::Start(self.row, self.col)),
155                        self.row,
156                        self.col,
157                    ));
158                }
159            }
160        }
161
162        Ok(decls)
163    }
164
165    /// Parse zero or more infix declarations.
166    ///
167    /// Infixes are parsed at module level (before regular declarations).
168    fn infixes(&mut self) -> Result<Vec<&'a Located<Infix<'a>>>, error::Module<'a>> {
169        let mut infixes = Vec::new();
170
171        loop {
172            let state = self.save_state();
173
174            match self.infix_decl() {
175                Ok(infix) => {
176                    infixes.push(infix);
177                    // infix_decl already ensures fresh line
178                }
179                Err(_) => {
180                    // If we didn't consume input, we're done
181                    if self.pos == state.pos {
182                        self.restore_state(state);
183                        break;
184                    }
185                    return Err(error::Module::Infix(self.row, self.col));
186                }
187            }
188        }
189
190        Ok(infixes)
191    }
192
193    /// Parse a complete module.
194    ///
195    /// Mirrors Elm's `chompModule`:
196    /// ```text
197    /// module = [ module_header ] { import } { infix } { declaration }
198    /// ```
199    pub fn module(&mut self) -> Result<Module<'a>, error::Module<'a>> {
200        // Consume initial whitespace
201        self.chomp(error::Module::Space)?;
202
203        let start_pos = self.get_position();
204
205        // Try to parse module header (optional)
206        let (name, exports) = {
207            let state = self.save_state();
208            match self.module_header() {
209                Ok((n, e)) => {
210                    // Consume whitespace after header and check fresh line
211                    self.chomp(error::Module::Space)?;
212                    self.check_fresh_line(error::Module::FreshLine)?;
213                    (Some(n), e)
214                }
215                Err(_) => {
216                    // No header - restore and use defaults
217                    if self.pos == state.pos {
218                        self.restore_state(state);
219                    }
220                    // Default: name = None, exports = Open
221                    let default_exports = self.alloc(Located::at(Region::one(), Exposing::Open));
222                    (None, default_exports)
223                }
224            }
225        };
226
227        // Parse imports
228        let imports = self.imports()?;
229
230        // Parse infixes
231        let infix_vec = self.infixes()?;
232        let binops = self.alloc_slice_copy(&infix_vec);
233
234        // Parse declarations
235        let decls = self.declarations()?;
236
237        // Categorize declarations into values, unions, aliases
238        let (values, unions, aliases) = self.categorize_decls(decls);
239
240        // Build docs (simplified: no module-level docs for now)
241        let docs = self.alloc(Docs::NoDocs(Region::new(start_pos, self.get_position())));
242
243        Ok(Module {
244            name,
245            exports,
246            docs,
247            imports,
248            values,
249            unions,
250            aliases,
251            binops,
252        })
253    }
254
255    /// Categorize declarations into separate slices by type.
256    #[allow(clippy::type_complexity)]
257    fn categorize_decls(
258        &self,
259        decls: Vec<Decl<'a>>,
260    ) -> (
261        &'a [&'a Located<Value<'a>>],
262        &'a [&'a Located<Union<'a>>],
263        &'a [&'a Located<Alias<'a>>],
264    ) {
265        let mut values = Vec::new();
266        let mut unions = Vec::new();
267        let mut aliases = Vec::new();
268
269        for decl in decls {
270            match decl {
271                Decl::Value(_doc, value) => values.push(value),
272                Decl::Union(_doc, union) => unions.push(union),
273                Decl::Alias(_doc, alias) => aliases.push(alias),
274            }
275        }
276
277        (
278            self.alloc_slice_copy(&values),
279            self.alloc_slice_copy(&unions),
280            self.alloc_slice_copy(&aliases),
281        )
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use bumpalo::Bump;
289    use indoc::indoc;
290
291    macro_rules! assert_module_header_snapshot {
292        ($input:expr) => {{
293            let input = indoc!($input);
294            let bump = Bump::new();
295            let src = bump.alloc_str(input);
296            let mut parser = Parser::new(&bump, src.as_bytes());
297            let result = parser.module_header();
298            match result {
299                Ok((name, exposing)) => {
300                    insta::with_settings!({
301                        description => format!("Code:\n\n{}", input),
302                        omit_expression => true,
303                    }, {
304                        insta::assert_debug_snapshot!((name, exposing));
305                    });
306                }
307                Err(e) => {
308                    panic!("Expected successful parse, got error: {:?}", e);
309                }
310            }
311        }};
312    }
313
314    #[test]
315    fn module_header_simple_open() {
316        assert_module_header_snapshot!("module Foo exposing (..)");
317    }
318
319    #[test]
320    fn module_header_dotted() {
321        assert_module_header_snapshot!("module Foo.Bar exposing (baz)");
322    }
323
324    #[test]
325    fn module_header_mixed_exposing() {
326        assert_module_header_snapshot!("module Main exposing (main, Msg(..))");
327    }
328
329    #[test]
330    fn module_header_deeply_nested() {
331        assert_module_header_snapshot!("module Platform.Cmd.Extra exposing (batch, none)");
332    }
333
334    // =========================================================================
335    // Full module parsing tests
336    // =========================================================================
337
338    macro_rules! assert_module_snapshot {
339        ($input:expr) => {{
340            let input = indoc!($input);
341            let bump = Bump::new();
342            let src = bump.alloc_str(input);
343            let mut parser = Parser::new(&bump, src.as_bytes());
344            let result = parser.module();
345            match result {
346                Ok(module) => {
347                    insta::with_settings!({
348                        description => format!("Code:\n\n{}", input),
349                        omit_expression => true,
350                    }, {
351                        insta::assert_debug_snapshot!(module);
352                    });
353                }
354                Err(e) => {
355                    panic!("Expected successful parse, got error: {:?}", e);
356                }
357            }
358        }};
359    }
360
361    #[test]
362    fn module_header_only() {
363        assert_module_snapshot!("module Main exposing (..)\n");
364    }
365
366    #[test]
367    fn module_with_imports() {
368        assert_module_snapshot!(
369            r#"
370            module Main exposing (..)
371
372            import List
373            import Maybe exposing (Maybe(..))
374        "#
375        );
376    }
377
378    #[test]
379    fn module_with_value() {
380        assert_module_snapshot!(
381            r#"
382            module Main exposing (..)
383
384            main = 42
385        "#
386        );
387    }
388
389    #[test]
390    fn module_with_type() {
391        assert_module_snapshot!(
392            r#"
393            module Main exposing (..)
394
395            type Maybe a
396                = Just a
397                | Nothing
398        "#
399        );
400    }
401
402    #[test]
403    fn module_with_alias() {
404        assert_module_snapshot!(
405            r#"
406            module Main exposing (..)
407
408            type alias Point = { x : Int, y : Int }
409        "#
410        );
411    }
412
413    #[test]
414    fn module_full() {
415        assert_module_snapshot!(
416            r#"
417            module Main exposing (main, Model, Msg(..))
418
419            import Html exposing (div)
420            import Platform.Cmd as Cmd
421
422            type alias Model = { count : Int }
423
424            type Msg
425                = Increment
426                | Decrement
427
428            main = 0
429        "#
430        );
431    }
432
433    #[test]
434    fn module_no_header() {
435        // Without header, defaults to name=None, exports=Open
436        assert_module_snapshot!("x = 1\n");
437    }
438
439    #[test]
440    fn module_with_infix() {
441        assert_module_snapshot!(
442            r#"
443            module Main exposing (..)
444
445            infix left 6 (|>) = apR
446
447            apR f x = f x
448        "#
449        );
450    }
451}