Skip to main content

nash_parse/
type_.rs

1//! Type parsing for Nash.
2//!
3//! Ported from Elm's `Parse/Type.hs`.
4//!
5//! Provides:
6//! - `type_term` - atomic types (variables, named types, tuples, records)
7//! - `type_expr` - full types including function arrows and type application
8
9use bumpalo::collections::Vec as BumpVec;
10use nash_region::{Located, Position, Region};
11use nash_source::{FieldType, Type};
12
13use crate::Parser;
14use crate::error::{self, TRecord, TTuple};
15
16/// Qualified or unqualified uppercase name (for types).
17enum ForeignUpper<'a> {
18    Unqualified(&'a str),
19    Qualified(&'a str, &'a str), // (module, name)
20}
21
22impl<'a> Parser<'a> {
23    // -------------------------------------------------------------------------
24    // Type expressions (with arrows)
25    // -------------------------------------------------------------------------
26
27    /// Parse a type expression including function arrows.
28    ///
29    /// Returns `(type, end)` where `end` is the position at end of type (before any chomp).
30    ///
31    /// Mirrors Elm's `Type.expression`:
32    /// ```haskell
33    /// expression :: Space.Parser E.Type Src.Type
34    /// expression =
35    ///   do  start <- getPosition
36    ///       term1@(tipe1, end1) <- oneOf E.TStart [ app start, term... ]
37    ///       oneOfWithFallback [ arrow... ] term1
38    /// ```
39    pub fn type_expr(&mut self) -> Result<(&'a Located<Type<'a>>, Position), error::Type<'a>> {
40        let start = self.get_position();
41
42        // Parse first term - either type application or simple term
43        let (tipe1, end1) = self.one_of(
44            error::Type::Start,
45            vec![
46                // Type application: Maybe Int, Result String Int, etc.
47                Box::new(|p: &mut Parser<'a>| p.type_app(start)),
48                // Simple term
49                Box::new(|p: &mut Parser<'a>| {
50                    let term = p.type_term()?;
51                    let end = p.get_position();
52                    p.chomp(error::Type::Space)?;
53                    Ok((term, end))
54                }),
55            ],
56        )?;
57
58        // Try to parse function arrow
59        self.one_of_with_fallback(
60            vec![Box::new(|p: &mut Parser<'a>| {
61                p.check_indent(end1.line, end1.column, error::Type::IndentStart)?;
62                p.word2(0x2D, 0x3E, error::Type::Start)?; // ->
63                p.chomp_and_check_indent(error::Type::Space, error::Type::IndentStart)?;
64
65                let (tipe2, end2) = p.type_expr()?;
66                let tipe = p.alloc(Located::at(
67                    Region::new(start, end2),
68                    Type::Lambda {
69                        from: tipe1,
70                        to: tipe2,
71                    },
72                ));
73                Ok((tipe, end2))
74            })],
75            (tipe1, end1),
76        )
77    }
78
79    // -------------------------------------------------------------------------
80    // Type application
81    // -------------------------------------------------------------------------
82
83    /// Parse type application: `Maybe Int`, `Result String Int`, etc.
84    ///
85    /// Mirrors Elm's `Type.app`.
86    fn type_app(
87        &mut self,
88        start: Position,
89    ) -> Result<(&'a Located<Type<'a>>, Position), error::Type<'a>> {
90        let upper = self.foreign_upper(error::Type::Start)?;
91        let upper_end = self.get_position();
92        self.chomp(error::Type::Space)?;
93
94        let (args, end) = self.type_chomp_args(upper_end)?;
95
96        let region = Region::new(start, upper_end);
97        let tipe = match upper {
98            ForeignUpper::Unqualified(name) => Type::Type { region, name, args },
99            ForeignUpper::Qualified(module, name) => Type::TypeQual {
100                region,
101                module,
102                name,
103                args,
104            },
105        };
106
107        Ok((self.alloc(Located::at(Region::new(start, end), tipe)), end))
108    }
109
110    /// Chomp type arguments for application.
111    ///
112    /// Mirrors Elm's `Type.chompArgs`.
113    fn type_chomp_args(
114        &mut self,
115        mut end: Position,
116    ) -> Result<(&'a [&'a Located<Type<'a>>], Position), error::Type<'a>> {
117        let mut args: BumpVec<'a, &'a Located<Type<'a>>> = BumpVec::new_in(self.bump);
118
119        loop {
120            let result = self.one_of_with_fallback(
121                vec![Box::new(|p: &mut Parser<'a>| {
122                    // Check CURRENT position (after chomp), not the end of previous token
123                    let (row, col) = p.position();
124                    p.check_indent(row, col, error::Type::IndentStart)?;
125                    let arg = p.type_term()?;
126                    let new_end = p.get_position();
127                    p.chomp(error::Type::Space)?;
128                    Ok(Some((arg, new_end)))
129                })],
130                None,
131            )?;
132
133            match result {
134                Some((arg, new_end)) => {
135                    args.push(arg);
136                    end = new_end;
137                }
138                None => break,
139            }
140        }
141
142        Ok((args.into_bump_slice(), end))
143    }
144
145    // -------------------------------------------------------------------------
146    // Type terms (atomic)
147    // -------------------------------------------------------------------------
148
149    /// Parse an atomic type (no arrows, no application).
150    ///
151    /// Mirrors Elm's `Type.term`:
152    /// - Named types: `Int`, `Maybe`, `Module.Type`
153    /// - Type variables: `a`, `msg`
154    /// - Tuples: `()`, `(Int, String)`
155    /// - Records: `{}`, `{ name : String }`, `{ a | name : String }`
156    pub fn type_term(&mut self) -> Result<&'a Located<Type<'a>>, error::Type<'a>> {
157        let start = self.get_position();
158
159        self.one_of(
160            error::Type::Start,
161            vec![
162                // Named type (no args in term - args handled by app)
163                Box::new(|p: &mut Parser<'a>| {
164                    let upper = p.foreign_upper(error::Type::Start)?;
165                    let end = p.get_position();
166                    let region = Region::new(start, end);
167
168                    let tipe = match upper {
169                        ForeignUpper::Unqualified(name) => {
170                            let empty: &'a [&'a Located<Type<'a>>] = &[];
171                            Type::Type {
172                                region,
173                                name,
174                                args: empty,
175                            }
176                        }
177                        ForeignUpper::Qualified(module, name) => {
178                            let empty: &'a [&'a Located<Type<'a>>] = &[];
179                            Type::TypeQual {
180                                region,
181                                module,
182                                name,
183                                args: empty,
184                            }
185                        }
186                    };
187
188                    Ok(p.add_end(start, tipe))
189                }),
190                // Type variable
191                Box::new(|p: &mut Parser<'a>| {
192                    let var = p.lower_name(error::Type::Start)?;
193                    Ok(p.add_end(start, Type::Var(var)))
194                }),
195                // Tuple (or unit, or parenthesized)
196                Box::new(|p: &mut Parser<'a>| p.type_tuple(start)),
197                // Record
198                Box::new(|p: &mut Parser<'a>| p.type_record(start)),
199            ],
200        )
201    }
202
203    // -------------------------------------------------------------------------
204    // Tuples
205    // -------------------------------------------------------------------------
206
207    /// Parse a tuple type: `()`, `(a)`, `(a, b)`, `(a, b, c)`
208    fn type_tuple(&mut self, start: Position) -> Result<&'a Located<Type<'a>>, error::Type<'a>> {
209        self.in_context(
210            |bump, tuple_err, row, col| error::Type::Tuple(bump.alloc(tuple_err), row, col),
211            |p| p.word1(0x28, error::Type::Start), // (
212            |p| p.type_tuple_body(start),
213        )
214    }
215
216    /// Parse tuple type body after `(`.
217    fn type_tuple_body(&mut self, start: Position) -> Result<&'a Located<Type<'a>>, TTuple<'a>> {
218        self.chomp_and_check_indent(TTuple::Space, TTuple::IndentType1)?;
219
220        self.one_of(
221            TTuple::Open,
222            vec![
223                // Unit: `()`
224                Box::new(|p: &mut Parser<'a>| {
225                    p.word1(0x29, TTuple::Open)?; // )
226                    Ok(p.add_end(start, Type::Unit))
227                }),
228                // Type (might be parenthesized or tuple)
229                Box::new(|p: &mut Parser<'a>| {
230                    let (first, end) = p.type_tuple_entry()?;
231                    p.check_indent(end.line, end.column, TTuple::IndentEnd)?;
232                    p.type_tuple_help(start, first)
233                }),
234            ],
235        )
236    }
237
238    /// Parse a type inside a tuple.
239    fn type_tuple_entry(&mut self) -> Result<(&'a Located<Type<'a>>, Position), TTuple<'a>> {
240        self.specialize(
241            |bump, type_err, row, col| TTuple::Type(bump.alloc(type_err), row, col),
242            |p| p.type_expr(),
243        )
244    }
245
246    /// Parse remaining tuple elements.
247    fn type_tuple_help(
248        &mut self,
249        start: Position,
250        first: &'a Located<Type<'a>>,
251    ) -> Result<&'a Located<Type<'a>>, TTuple<'a>> {
252        let mut rest: BumpVec<'a, &'a Located<Type<'a>>> = BumpVec::new_in(self.bump);
253
254        loop {
255            self.chomp(TTuple::Space)?;
256
257            let done = self.one_of(
258                TTuple::End,
259                vec![
260                    // Comma - another type
261                    Box::new(|p: &mut Parser<'a>| {
262                        p.word1(0x2C, TTuple::End)?; // ,
263                        p.chomp_and_check_indent(TTuple::Space, TTuple::IndentTypeN)?;
264
265                        let (tipe, end) = p.type_tuple_entry()?;
266                        rest.push(tipe);
267
268                        p.check_indent(end.line, end.column, TTuple::IndentEnd)?;
269                        Ok(false)
270                    }),
271                    // Close paren
272                    Box::new(|p: &mut Parser<'a>| {
273                        p.word1(0x29, TTuple::End)?; // )
274                        Ok(true)
275                    }),
276                ],
277            )?;
278
279            if done {
280                break;
281            }
282        }
283
284        if rest.is_empty() {
285            // Just parenthesized type
286            Ok(first)
287        } else {
288            // Tuple
289            let second = rest.remove(0);
290            let others = rest.into_bump_slice();
291            Ok(self.add_end(
292                start,
293                Type::Tuple {
294                    first,
295                    second,
296                    rest: others,
297                },
298            ))
299        }
300    }
301
302    // -------------------------------------------------------------------------
303    // Records
304    // -------------------------------------------------------------------------
305
306    /// Parse a record type: `{}`, `{ name : String }`, `{ a | name : String }`
307    fn type_record(&mut self, start: Position) -> Result<&'a Located<Type<'a>>, error::Type<'a>> {
308        self.in_context(
309            |bump, record_err, row, col| error::Type::Record(bump.alloc(record_err), row, col),
310            |p| p.word1(0x7B, error::Type::Start), // {
311            |p| p.type_record_body(start),
312        )
313    }
314
315    /// Parse record type body after `{`.
316    fn type_record_body(&mut self, start: Position) -> Result<&'a Located<Type<'a>>, TRecord<'a>> {
317        self.chomp_and_check_indent(TRecord::Space, TRecord::IndentOpen)?;
318
319        self.one_of(
320            TRecord::Open,
321            vec![
322                // Empty record: `{}`
323                Box::new(|p: &mut Parser<'a>| {
324                    p.word1(0x7D, TRecord::Open)?; // }
325                    let empty: &'a [&'a FieldType<'a>] = &[];
326                    Ok(p.add_end(
327                        start,
328                        Type::Record {
329                            fields: empty,
330                            ext: None,
331                        },
332                    ))
333                }),
334                // Non-empty record
335                Box::new(|p: &mut Parser<'a>| {
336                    let name_start = p.get_position();
337                    let name = p.lower_name(TRecord::Field)?;
338                    let name_loc = p.add_end(name_start, name);
339
340                    p.chomp_and_check_indent(TRecord::Space, TRecord::IndentColon)?;
341
342                    p.one_of(
343                        TRecord::Colon,
344                        vec![
345                            // Extension: `{ a | field : Type }`
346                            Box::new(|p: &mut Parser<'a>| {
347                                p.word1(0x7C, TRecord::Colon)?; // |
348                                p.chomp_and_check_indent(TRecord::Space, TRecord::IndentField)?;
349
350                                let field = p.type_record_field()?;
351                                let fields = p.type_record_end(field)?;
352                                Ok(p.add_end(
353                                    start,
354                                    Type::Record {
355                                        fields,
356                                        ext: Some(name_loc),
357                                    },
358                                ))
359                            }),
360                            // Regular field: `{ name : Type }`
361                            Box::new(|p: &mut Parser<'a>| {
362                                p.word1(0x3A, TRecord::Colon)?; // :
363                                p.chomp_and_check_indent(TRecord::Space, TRecord::IndentType)?;
364
365                                let (tipe, end) = p.type_record_type_entry()?;
366                                p.check_indent(end.line, end.column, TRecord::IndentEnd)?;
367
368                                let field = p.alloc(FieldType {
369                                    field: name_loc,
370                                    typ: tipe,
371                                });
372                                let fields = p.type_record_end(field)?;
373                                Ok(p.add_end(start, Type::Record { fields, ext: None }))
374                            }),
375                        ],
376                    )
377                }),
378            ],
379        )
380    }
381
382    /// Parse a type inside a record field.
383    fn type_record_type_entry(&mut self) -> Result<(&'a Located<Type<'a>>, Position), TRecord<'a>> {
384        self.specialize(
385            |bump, type_err, row, col| TRecord::Type(bump.alloc(type_err), row, col),
386            |p| p.type_expr(),
387        )
388    }
389
390    /// Parse a record field: `name : Type`
391    fn type_record_field(&mut self) -> Result<&'a FieldType<'a>, TRecord<'a>> {
392        let name_start = self.get_position();
393        let name = self.lower_name(TRecord::Field)?;
394        let name_loc = self.add_end(name_start, name);
395
396        self.chomp_and_check_indent(TRecord::Space, TRecord::IndentColon)?;
397        self.word1(0x3A, TRecord::Colon)?; // :
398        self.chomp_and_check_indent(TRecord::Space, TRecord::IndentType)?;
399
400        let (tipe, end) = self.type_record_type_entry()?;
401        self.check_indent(end.line, end.column, TRecord::IndentEnd)?;
402
403        Ok(self.alloc(FieldType {
404            field: name_loc,
405            typ: tipe,
406        }))
407    }
408
409    /// Parse remaining record fields.
410    fn type_record_end(
411        &mut self,
412        first: &'a FieldType<'a>,
413    ) -> Result<&'a [&'a FieldType<'a>], TRecord<'a>> {
414        let mut fields: BumpVec<'a, &'a FieldType<'a>> = BumpVec::new_in(self.bump);
415        fields.push(first);
416
417        loop {
418            self.chomp(TRecord::Space)?;
419
420            let done = self.one_of(
421                TRecord::End,
422                vec![
423                    // Comma - another field
424                    Box::new(|p: &mut Parser<'a>| {
425                        p.word1(0x2C, TRecord::End)?; // ,
426                        p.chomp_and_check_indent(TRecord::Space, TRecord::IndentField)?;
427
428                        let field = p.type_record_field()?;
429                        fields.push(field);
430                        Ok(false)
431                    }),
432                    // Close brace
433                    Box::new(|p: &mut Parser<'a>| {
434                        p.word1(0x7D, TRecord::End)?; // }
435                        Ok(true)
436                    }),
437                ],
438            )?;
439
440            if done {
441                break;
442            }
443        }
444
445        Ok(fields.into_bump_slice())
446    }
447
448    // -------------------------------------------------------------------------
449    // Helpers
450    // -------------------------------------------------------------------------
451
452    /// Parse a possibly-qualified uppercase name (for types).
453    ///
454    /// Mirrors Elm's `Var.foreignUpper`.
455    fn foreign_upper<E>(
456        &mut self,
457        to_error: impl FnOnce(u16, u16) -> E,
458    ) -> Result<ForeignUpper<'a>, E> {
459        let (row, col) = self.position();
460        let start_pos = self.pos;
461
462        match self.peek() {
463            Some(b) if b.is_ascii_uppercase() => {
464                self.advance();
465                self.chomp_inner_chars();
466
467                // Check for qualification
468                if self.is_dot_upper() {
469                    self.chomp_qualified_upper_for_type(start_pos)
470                } else if self.is_dot_lower() {
471                    // Can't have lowercase after dot for types
472                    Err(to_error(row, col))
473                } else {
474                    let name = self.slice_from(start_pos);
475                    Ok(ForeignUpper::Unqualified(name))
476                }
477            }
478            _ => Err(to_error(row, col)),
479        }
480    }
481
482    /// Chomp through Module.Module... chain for type names.
483    fn chomp_qualified_upper_for_type<E>(
484        &mut self,
485        start_pos: usize,
486    ) -> Result<ForeignUpper<'a>, E> {
487        loop {
488            if self.is_dot_upper() {
489                self.advance(); // consume dot
490                self.advance(); // consume first uppercase char
491                self.chomp_inner_chars();
492            } else {
493                // No more dots - split into module and name
494                let full = self.slice_from(start_pos);
495                if let Some(last_dot) = full.rfind('.') {
496                    let module = &full[..last_dot];
497                    let name = &full[last_dot + 1..];
498                    return Ok(ForeignUpper::Qualified(module, name));
499                } else {
500                    return Ok(ForeignUpper::Unqualified(full));
501                }
502            }
503        }
504    }
505}
506
507// =============================================================================
508// Tests
509// =============================================================================
510
511#[cfg(test)]
512macro_rules! assert_type_snapshot {
513    ($code:expr) => {{
514        let bump = bumpalo::Bump::new();
515        let src = bump.alloc_str(indoc::indoc!($code));
516        let mut parser = $crate::Parser::new(&bump, src.as_bytes());
517        let (result, _end) = parser.type_expr().expect("expected successful parse");
518
519        insta::with_settings!({
520            description => format!("Code:\n\n{}", indoc::indoc!($code)),
521            omit_expression => true,
522        }, {
523            insta::assert_debug_snapshot!(result);
524        });
525    }};
526}
527
528#[cfg(test)]
529mod tests {
530    // Type variables
531    #[test]
532    fn type_var_simple() {
533        assert_type_snapshot!("a");
534    }
535
536    #[test]
537    fn type_var_msg() {
538        assert_type_snapshot!("msg");
539    }
540
541    // Named types (no args)
542    #[test]
543    fn named_type_int() {
544        assert_type_snapshot!("Int");
545    }
546
547    #[test]
548    fn named_type_string() {
549        assert_type_snapshot!("String");
550    }
551
552    #[test]
553    fn named_type_qualified() {
554        assert_type_snapshot!("Dict.Dict");
555    }
556
557    #[test]
558    fn named_type_multi_qualified() {
559        assert_type_snapshot!("Data.Map.Map");
560    }
561
562    // Type application
563    #[test]
564    fn type_app_maybe() {
565        assert_type_snapshot!("Maybe Int");
566    }
567
568    #[test]
569    fn type_app_result() {
570        assert_type_snapshot!("Result String Int");
571    }
572
573    #[test]
574    fn type_app_nested() {
575        assert_type_snapshot!("Maybe (List Int)");
576    }
577
578    #[test]
579    fn type_app_qualified() {
580        assert_type_snapshot!("Dict.Dict String Int");
581    }
582
583    // Function types
584    #[test]
585    fn function_simple() {
586        assert_type_snapshot!("a -> b");
587    }
588
589    #[test]
590    fn function_multi() {
591        assert_type_snapshot!("a -> b -> c");
592    }
593
594    #[test]
595    fn function_with_types() {
596        assert_type_snapshot!("Int -> String -> Bool");
597    }
598
599    #[test]
600    fn function_with_app() {
601        assert_type_snapshot!("Maybe a -> Result e a");
602    }
603
604    // Unit
605    #[test]
606    fn unit() {
607        assert_type_snapshot!("()");
608    }
609
610    // Tuple types
611    #[test]
612    fn tuple_pair() {
613        assert_type_snapshot!("(Int, String)");
614    }
615
616    #[test]
617    fn tuple_triple() {
618        assert_type_snapshot!("(Int, String, Bool)");
619    }
620
621    #[test]
622    fn tuple_nested() {
623        assert_type_snapshot!("((Int, String), Bool)");
624    }
625
626    #[test]
627    fn tuple_with_function() {
628        assert_type_snapshot!("(a -> b, c)");
629    }
630
631    #[test]
632    fn tuple_multiline() {
633        assert_type_snapshot!(
634            "(
635                Int,
636                String,
637                Bool
638            )"
639        );
640    }
641
642    // Record types
643    #[test]
644    fn record_empty() {
645        assert_type_snapshot!("{}");
646    }
647
648    #[test]
649    fn record_single() {
650        assert_type_snapshot!("{ name : String }");
651    }
652
653    #[test]
654    fn record_multiple() {
655        assert_type_snapshot!("{ name : String, age : Int }");
656    }
657
658    #[test]
659    fn record_with_function() {
660        assert_type_snapshot!("{ onClick : msg -> Cmd msg }");
661    }
662
663    #[test]
664    fn record_extension() {
665        assert_type_snapshot!("{ a | name : String }");
666    }
667
668    #[test]
669    fn record_extension_multiple() {
670        assert_type_snapshot!("{ a | name : String, age : Int }");
671    }
672
673    #[test]
674    fn record_multiline() {
675        assert_type_snapshot!(
676            "{
677                name : String,
678                age : Int,
679                active : Bool
680            }"
681        );
682    }
683
684    // Parenthesized
685    #[test]
686    fn parenthesized() {
687        assert_type_snapshot!("(Int)");
688    }
689
690    #[test]
691    fn parenthesized_function() {
692        assert_type_snapshot!("(a -> b) -> List a -> List b");
693    }
694
695    // Complex combinations
696    #[test]
697    fn complex_model_msg() {
698        assert_type_snapshot!("{ model : Model, update : Msg -> Model -> Model }");
699    }
700}