Skip to main content

rlean_search/parser/
type_expr.rs

1//! Recursive-descent parser for a useful fragment of Lean 4 types and search patterns.
2
3use crate::ast::{Binder, BinderKind, TypeExpr};
4use crate::lexer::{Lexer, Token};
5use thiserror::Error;
6
7#[derive(Debug, Error, Clone)]
8pub enum ParseError {
9    #[error("unexpected token {found} (expected {expected})")]
10    Unexpected {
11        expected: String,
12        found: String,
13    },
14    #[error("unexpected end of input while parsing {context}")]
15    Eof { context: String },
16    #[error("parse error: {0}")]
17    Message(String),
18}
19
20pub type Result<T> = std::result::Result<T, ParseError>;
21
22/// A user search pattern, optionally restricted to the conclusion (`|-`).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SearchPattern {
25    /// When true, match only against `TypeExpr::conclusion()`.
26    pub conclusion_only: bool,
27    pub expr: TypeExpr,
28}
29
30pub fn parse_type(input: &str) -> Result<TypeExpr> {
31    let mut p = Parser::new(input);
32    let t = p.parse_type()?;
33    // Allow trailing junk lightly — but prefer clean parse
34    Ok(t)
35}
36
37pub fn parse_search_pattern(input: &str) -> Result<SearchPattern> {
38    let trimmed = input.trim();
39    let (conclusion_only, rest) = if let Some(r) = trimmed.strip_prefix("|-") {
40        (true, r.trim())
41    } else if let Some(r) = trimmed.strip_prefix("⊢") {
42        (true, r.trim())
43    } else {
44        (false, trimmed)
45    };
46    let expr = parse_type(rest)?;
47    Ok(SearchPattern {
48        conclusion_only,
49        expr,
50    })
51}
52
53struct Parser {
54    tokens: Vec<Token>,
55    pos: usize,
56}
57
58impl Parser {
59    fn new(input: &str) -> Self {
60        let tokens = Lexer::tokenize(input);
61        Self { tokens, pos: 0 }
62    }
63
64    fn peek(&self) -> &Token {
65        self.tokens.get(self.pos).unwrap_or(&Token::Eof)
66    }
67
68    fn bump(&mut self) -> Token {
69        let t = self.peek().clone();
70        if !matches!(t, Token::Eof) {
71            self.pos += 1;
72        }
73        t
74    }
75
76    fn expect(&mut self, expected: &Token) -> Result<()> {
77        let found = self.bump();
78        if &found == expected {
79            Ok(())
80        } else {
81            Err(ParseError::Unexpected {
82                expected: expected.to_string(),
83                found: found.to_string(),
84            })
85        }
86    }
87
88    fn parse_type(&mut self) -> Result<TypeExpr> {
89        self.parse_arrow()
90    }
91
92    /// Right-associative arrows: `A → B → C`
93    fn parse_arrow(&mut self) -> Result<TypeExpr> {
94        // Leading binder Pi: `(x : A) → B` or `{x : A} → B`
95        if matches!(
96            self.peek(),
97            Token::LParen | Token::LBrace | Token::LBracket | Token::LStrict
98        ) {
99            // Could be parenthesized type OR binder then arrow. Lookahead.
100            if let Some(binder) = self.try_parse_leading_pi_binder()? {
101                if matches!(self.peek(), Token::Arrow) {
102                    self.bump();
103                    let body = self.parse_arrow()?;
104                    return Ok(TypeExpr::Pi {
105                        binder,
106                        body: Box::new(body),
107                    });
108                }
109                // Not an arrow — treat the binder group as a parenthesized/typed term is wrong.
110                // Fall through: re-parse as atomic via backtrack-ish by reconstructing.
111                // We already consumed the binder; if no arrow, interpret as the type inside
112                // only when single default binder with type used as parenthesized expression —
113                // actually `(A)` is not a binder. try_parse only succeeds for binders with names.
114                // If we parsed `(x : A)` without arrow, it's still a term-ish raw.
115                return Ok(TypeExpr::Raw(format_binder_raw(&binder_fallback_name(
116                    &binder,
117                ))));
118            }
119        }
120
121        let left = self.parse_bin_expr(0)?;
122        if matches!(self.peek(), Token::Arrow) {
123            self.bump();
124            let right = self.parse_arrow()?;
125            Ok(TypeExpr::Arrow(Box::new(left), Box::new(right)))
126        } else {
127            Ok(left)
128        }
129    }
130
131    /// Operator precedence climbing for infix operators.
132    fn parse_bin_expr(&mut self, min_prec: u8) -> Result<TypeExpr> {
133        let mut left = self.parse_app()?;
134
135        loop {
136            let op = match self.peek() {
137                Token::Op(s) if is_infix_op(s) => s.clone(),
138                // `⁻¹` as postfix handled in parse_app
139                _ => break,
140            };
141            let prec = op_prec(&op);
142            if prec < min_prec {
143                break;
144            }
145            self.bump();
146            // right-assoc for `^` and `→` (arrow handled elsewhere), `∘` sometimes
147            let next_min = if is_right_assoc(&op) { prec } else { prec + 1 };
148            let right = self.parse_bin_expr(next_min)?;
149            left = TypeExpr::BinOp {
150                op,
151                left: Box::new(left),
152                right: Box::new(right),
153            };
154        }
155        Ok(left)
156    }
157
158    fn parse_app(&mut self) -> Result<TypeExpr> {
159        // Prefix unary
160        if let Token::Op(s) = self.peek() {
161            if s == "¬" || s == "-" {
162                let op = s.clone();
163                self.bump();
164                let arg = self.parse_app()?;
165                return Ok(TypeExpr::UnaryOp {
166                    op,
167                    arg: Box::new(arg),
168                });
169            }
170        }
171
172        let mut expr = self.parse_atomic()?;
173
174        // Postfix: ⁻¹ and projections .field
175        loop {
176            if let Token::Op(s) = self.peek() {
177                if s == "⁻¹" || s == "'" {
178                    let op = s.clone();
179                    self.bump();
180                    expr = TypeExpr::Postfix {
181                        arg: Box::new(expr),
182                        op,
183                    };
184                    continue;
185                }
186            }
187            if matches!(self.peek(), Token::Dot) {
188                self.bump();
189                match self.bump() {
190                    Token::Ident(f) => {
191                        expr = TypeExpr::Proj {
192                            base: Box::new(expr),
193                            field: f,
194                        };
195                    }
196                    Token::Nat(n) => {
197                        expr = TypeExpr::Proj {
198                            base: Box::new(expr),
199                            field: n,
200                        };
201                    }
202                    Token::Op(s) if s == ".." => {
203                        // `x..` range sugar → raw-ish
204                        expr = TypeExpr::Postfix {
205                            arg: Box::new(expr),
206                            op: "..".into(),
207                        };
208                    }
209                    other => {
210                        return Err(ParseError::Unexpected {
211                            expected: "field name".into(),
212                            found: other.to_string(),
213                        });
214                    }
215                }
216                continue;
217            }
218            break;
219        }
220
221        // Juxtaposition application
222        while is_atomic_start(self.peek()) {
223            // Don't consume infix ops as apps
224            if let Token::Op(s) = self.peek() {
225                if is_infix_op(s) {
226                    break;
227                }
228            }
229            if matches!(self.peek(), Token::Arrow | Token::Comma | Token::Assign) {
230                break;
231            }
232            let arg = self.parse_atomic()?;
233            // Allow postfix on arg already inside parse_atomic path — apply
234            let mut arg = arg;
235            loop {
236                if let Token::Op(s) = self.peek() {
237                    if s == "⁻¹" || s == "'" {
238                        let op = s.clone();
239                        self.bump();
240                        arg = TypeExpr::Postfix {
241                            arg: Box::new(arg),
242                            op,
243                        };
244                        continue;
245                    }
246                }
247                if matches!(self.peek(), Token::Dot) {
248                    self.bump();
249                    match self.bump() {
250                        Token::Ident(f) => {
251                            arg = TypeExpr::Proj {
252                                base: Box::new(arg),
253                                field: f,
254                            };
255                        }
256                        Token::Nat(n) => {
257                            arg = TypeExpr::Proj {
258                                base: Box::new(arg),
259                                field: n,
260                            };
261                        }
262                        _ => break,
263                    }
264                    continue;
265                }
266                break;
267            }
268            expr = TypeExpr::App(Box::new(expr), Box::new(arg));
269        }
270
271        Ok(expr)
272    }
273
274    fn parse_atomic(&mut self) -> Result<TypeExpr> {
275        match self.peek().clone() {
276            Token::Underscore => {
277                self.bump();
278                Ok(TypeExpr::Hole)
279            }
280            Token::NamedHole(n) => {
281                self.bump();
282                Ok(TypeExpr::NamedHole(n))
283            }
284            Token::Nat(n) => {
285                self.bump();
286                Ok(TypeExpr::NatLit(n))
287            }
288            Token::Literal(s) => {
289                self.bump();
290                Ok(TypeExpr::Literal(s))
291            }
292            Token::Ident(s) => {
293                self.bump();
294                // Sort / Type / Prop with optional level
295                if s == "Prop" {
296                    return Ok(TypeExpr::Sort {
297                        name: "Prop".into(),
298                        level: None,
299                    });
300                }
301                if s == "Type" || s == "Sort" {
302                    let level = if is_atomic_start(self.peek())
303                        && !matches!(self.peek(), Token::Op(_))
304                    {
305                        // Type u / Type _ / Type*
306                        if matches!(self.peek(), Token::Op(op) if op == "*") {
307                            self.bump();
308                            Some(Box::new(TypeExpr::Ident("*".into())))
309                        } else if matches!(
310                            self.peek(),
311                            Token::Ident(_)
312                                | Token::Underscore
313                                | Token::NamedHole(_)
314                                | Token::Nat(_)
315                                | Token::LParen
316                        ) {
317                            // Only take a simple level atom, not a full app chain of term
318                            match self.peek() {
319                                Token::Ident(_)
320                                | Token::Underscore
321                                | Token::NamedHole(_)
322                                | Token::Nat(_) => Some(Box::new(self.parse_level_atom()?)),
323                                Token::LParen => Some(Box::new(self.parse_atomic()?)),
324                                _ => None,
325                            }
326                        } else {
327                            None
328                        }
329                    } else {
330                        None
331                    };
332                    return Ok(TypeExpr::Sort { name: s, level });
333                }
334                Ok(TypeExpr::Ident(s))
335            }
336            Token::Forall => {
337                self.bump();
338                self.parse_quantifier(true)
339            }
340            Token::Exists => {
341                self.bump();
342                self.parse_quantifier(false)
343            }
344            Token::Fun => {
345                self.bump();
346                let binders = self.parse_binder_list_until_mapsto()?;
347                if matches!(self.peek(), Token::MapsTo) {
348                    self.bump();
349                } else if matches!(self.peek(), Token::Arrow) {
350                    // fun x → body sometimes
351                    self.bump();
352                } else {
353                    // fun x => may use comma? rare
354                }
355                let body = self.parse_type()?;
356                Ok(TypeExpr::Lambda {
357                    binders,
358                    body: Box::new(body),
359                })
360            }
361            Token::LParen => {
362                self.bump();
363                // Empty `()`
364                if matches!(self.peek(), Token::RParen) {
365                    self.bump();
366                    return Ok(TypeExpr::Raw("()".into()));
367                }
368                // Grouping / type ascription `(e)` or `(e : T)` or `(e : T) → ...` handled higher
369                let inner = self.parse_type()?;
370                // Type ascription: `(1 : M)`, `(Inv.inv : G → G)`
371                let inner = if matches!(self.peek(), Token::Colon) {
372                    self.bump();
373                    let ty = self.parse_type()?;
374                    TypeExpr::App(
375                        Box::new(TypeExpr::App(
376                            Box::new(TypeExpr::Ident("ascribe".into())),
377                            Box::new(inner),
378                        )),
379                        Box::new(ty),
380                    )
381                } else {
382                    inner
383                };
384                if matches!(self.peek(), Token::Comma) {
385                    let mut parts = vec![inner];
386                    while matches!(self.peek(), Token::Comma) {
387                        self.bump();
388                        parts.push(self.parse_type()?);
389                    }
390                    self.expect(&Token::RParen)?;
391                    let s = parts
392                        .iter()
393                        .map(|p| p.surface())
394                        .collect::<Vec<_>>()
395                        .join(", ");
396                    return Ok(TypeExpr::Raw(format!("({s})")));
397                }
398                self.expect(&Token::RParen)?;
399                Ok(inner)
400            }
401            Token::LBrace => {
402                // Implicit binder as term is rare; parse as binder group raw or type
403                let binders = self.parse_brace_binder_group(BinderKind::Implicit)?;
404                Ok(TypeExpr::Raw(format_binders_surface(&binders)))
405            }
406            Token::LBracket => {
407                // Empty list `[]` or instance-like `[Group α]`
408                self.bump();
409                if matches!(self.peek(), Token::RBracket) {
410                    self.bump();
411                    return Ok(TypeExpr::Ident("[]".into()));
412                }
413                let inner = self.parse_type()?;
414                // optional ascription inside brackets rare
415                let inner = if matches!(self.peek(), Token::Colon) {
416                    self.bump();
417                    let ty = self.parse_type()?;
418                    TypeExpr::App(
419                        Box::new(TypeExpr::App(
420                            Box::new(TypeExpr::Ident("ascribe".into())),
421                            Box::new(inner),
422                        )),
423                        Box::new(ty),
424                    )
425                } else {
426                    inner
427                };
428                self.expect(&Token::RBracket)?;
429                Ok(TypeExpr::App(
430                    Box::new(TypeExpr::Ident("inst".into())),
431                    Box::new(inner),
432                ))
433            }
434            Token::Pipe => {
435                // Absolute value `|e|`
436                self.bump();
437                let inner = self.parse_bin_expr(0)?;
438                if matches!(self.peek(), Token::Pipe) {
439                    self.bump();
440                    Ok(TypeExpr::App(
441                        Box::new(TypeExpr::Ident("abs".into())),
442                        Box::new(inner),
443                    ))
444                } else {
445                    Ok(TypeExpr::UnaryOp {
446                        op: "|".into(),
447                        arg: Box::new(inner),
448                    })
449                }
450            }
451            Token::LStrict => {
452                let binders = self.parse_brace_binder_group(BinderKind::StrictImplicit)?;
453                Ok(TypeExpr::Raw(format_binders_surface(&binders)))
454            }
455            Token::Op(s) if s == "∑" || s == "∏" || s == "∫" => {
456                // Big operators: keep as unary-ish application chain
457                let op = s.clone();
458                self.bump();
459                // optional binder `(i ∈ s)` etc.
460                let arg = if is_atomic_start(self.peek()) {
461                    self.parse_app()?
462                } else {
463                    TypeExpr::Hole
464                };
465                Ok(TypeExpr::UnaryOp {
466                    op,
467                    arg: Box::new(arg),
468                })
469            }
470            other => Err(ParseError::Unexpected {
471                expected: "type expression".into(),
472                found: other.to_string(),
473            }),
474        }
475    }
476
477    fn parse_level_atom(&mut self) -> Result<TypeExpr> {
478        match self.bump() {
479            Token::Ident(s) => Ok(TypeExpr::Ident(s)),
480            Token::Nat(n) => Ok(TypeExpr::NatLit(n)),
481            Token::Underscore => Ok(TypeExpr::Hole),
482            Token::NamedHole(n) => Ok(TypeExpr::NamedHole(n)),
483            other => Err(ParseError::Unexpected {
484                expected: "universe level".into(),
485                found: other.to_string(),
486            }),
487        }
488    }
489
490    fn parse_quantifier(&mut self, is_forall: bool) -> Result<TypeExpr> {
491        let binders = self.parse_quantifier_binders()?;
492        // optional comma
493        if matches!(self.peek(), Token::Comma) {
494            self.bump();
495        }
496        let body = self.parse_type()?;
497        if is_forall {
498            Ok(TypeExpr::Forall {
499                binders,
500                body: Box::new(body),
501            })
502        } else {
503            Ok(TypeExpr::Exists {
504                binders,
505                body: Box::new(body),
506            })
507        }
508    }
509
510    fn parse_quantifier_binders(&mut self) -> Result<Vec<Binder>> {
511        let mut binders = Vec::new();
512        // ∀ x y : Nat, ...  or ∀ (x : Nat) (y : Nat), ... or ∀ x, ...
513        loop {
514            match self.peek() {
515                Token::LParen | Token::LBrace | Token::LBracket | Token::LStrict => {
516                    binders.push(self.parse_one_binder_group()?);
517                }
518                Token::Ident(_) | Token::Underscore => {
519                    // bare names until `:` or `,`
520                    let mut names = Vec::new();
521                    while matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
522                        match self.bump() {
523                            Token::Ident(n) => names.push(n),
524                            Token::Underscore => names.push("_".into()),
525                            _ => unreachable!(),
526                        }
527                        // stop if next would start body wrongly — if next is Op or known end
528                        if matches!(
529                            self.peek(),
530                            Token::Colon
531                                | Token::Comma
532                                | Token::LParen
533                                | Token::LBrace
534                                | Token::LBracket
535                                | Token::LStrict
536                        ) {
537                            break;
538                        }
539                        // also stop before another quantifier body keyword-less: if Op infix, it's body start without comma — rare
540                        if matches!(self.peek(), Token::Op(_) | Token::Arrow) {
541                            break;
542                        }
543                    }
544                    let ty = if matches!(self.peek(), Token::Colon) {
545                        self.bump();
546                        Some(Box::new(self.parse_bin_expr(0)?))
547                    } else {
548                        None
549                    };
550                    binders.push(Binder {
551                        kind: BinderKind::Default,
552                        names,
553                        ty,
554                    });
555                    // if next is another binder group continue; if comma break to body
556                    if matches!(self.peek(), Token::Comma) {
557                        break;
558                    }
559                    if !matches!(
560                        self.peek(),
561                        Token::LParen
562                            | Token::LBrace
563                            | Token::LBracket
564                            | Token::LStrict
565                            | Token::Ident(_)
566                            | Token::Underscore
567                    ) {
568                        break;
569                    }
570                }
571                Token::Comma => break,
572                _ => break,
573            }
574            if matches!(self.peek(), Token::Comma) {
575                break;
576            }
577        }
578        if binders.is_empty() {
579            return Err(ParseError::Message(
580                "expected binders after quantifier".into(),
581            ));
582        }
583        Ok(binders)
584    }
585
586    fn parse_binder_list_until_mapsto(&mut self) -> Result<Vec<Binder>> {
587        let mut binders = Vec::new();
588        while !matches!(
589            self.peek(),
590            Token::MapsTo | Token::Arrow | Token::Eof | Token::Comma
591        ) {
592            if matches!(
593                self.peek(),
594                Token::LParen | Token::LBrace | Token::LBracket | Token::LStrict
595            ) {
596                binders.push(self.parse_one_binder_group()?);
597            } else if matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
598                let mut names = Vec::new();
599                while matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
600                    match self.bump() {
601                        Token::Ident(n) => names.push(n),
602                        Token::Underscore => names.push("_".into()),
603                        _ => unreachable!(),
604                    }
605                    if matches!(self.peek(), Token::Colon) {
606                        break;
607                    }
608                    if matches!(self.peek(), Token::MapsTo | Token::Arrow) {
609                        break;
610                    }
611                    if matches!(
612                        self.peek(),
613                        Token::LParen | Token::LBrace | Token::LBracket | Token::LStrict
614                    ) {
615                        break;
616                    }
617                }
618                let ty = if matches!(self.peek(), Token::Colon) {
619                    self.bump();
620                    Some(Box::new(self.parse_bin_expr(0)?))
621                } else {
622                    None
623                };
624                binders.push(Binder {
625                    kind: BinderKind::Default,
626                    names,
627                    ty,
628                });
629            } else {
630                break;
631            }
632        }
633        Ok(binders)
634    }
635
636    fn parse_one_binder_group(&mut self) -> Result<Binder> {
637        match self.peek() {
638            Token::LParen => self.parse_paren_binder(BinderKind::Default),
639            Token::LBrace => self.parse_brace_binder_group(BinderKind::Implicit).map(|mut v| {
640                v.pop().unwrap_or(Binder {
641                    kind: BinderKind::Implicit,
642                    names: vec!["_".into()],
643                    ty: None,
644                })
645            }),
646            Token::LBracket => self.parse_brace_binder_group(BinderKind::Instance).map(|mut v| {
647                v.pop().unwrap_or(Binder {
648                    kind: BinderKind::Instance,
649                    names: vec!["_".into()],
650                    ty: None,
651                })
652            }),
653            Token::LStrict => self
654                .parse_brace_binder_group(BinderKind::StrictImplicit)
655                .map(|mut v| {
656                    v.pop().unwrap_or(Binder {
657                        kind: BinderKind::StrictImplicit,
658                        names: vec!["_".into()],
659                        ty: None,
660                    })
661                }),
662            _ => Err(ParseError::Message("expected binder group".into())),
663        }
664    }
665
666    fn parse_paren_binder(&mut self, kind: BinderKind) -> Result<Binder> {
667        self.expect(&Token::LParen)?;
668        // `(x y : T)` or `(x)` or `(_ : T)`
669        let mut names = Vec::new();
670        while matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
671            match self.bump() {
672                Token::Ident(n) => names.push(n),
673                Token::Underscore => names.push("_".into()),
674                _ => unreachable!(),
675            }
676            if matches!(self.peek(), Token::Colon | Token::RParen) {
677                break;
678            }
679        }
680        let ty = if matches!(self.peek(), Token::Colon) {
681            self.bump();
682            Some(Box::new(self.parse_type()?))
683        } else {
684            None
685        };
686        self.expect(&Token::RParen)?;
687        if names.is_empty() {
688            // `(T)` was not a binder — but we already consumed. Represent type-only.
689            if let Some(t) = ty {
690                return Ok(Binder {
691                    kind,
692                    names: vec!["_".into()],
693                    ty: Some(t),
694                });
695            }
696        }
697        Ok(Binder { kind, names, ty })
698    }
699
700    fn parse_brace_binder_group(&mut self, kind: BinderKind) -> Result<Vec<Binder>> {
701        let (open, close) = match kind {
702            BinderKind::Implicit => (Token::LBrace, Token::RBrace),
703            BinderKind::Instance => (Token::LBracket, Token::RBracket),
704            BinderKind::StrictImplicit => (Token::LStrict, Token::RStrict),
705            BinderKind::Default => (Token::LParen, Token::RParen),
706        };
707        self.expect(&open)?;
708        let mut binders = Vec::new();
709        // instance `[Group G]` may have no names with colon: Ident+ as type
710        // Try names : type, or just type
711        let mut names = Vec::new();
712        let start_pos = self.pos;
713        while matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
714            match self.bump() {
715                Token::Ident(n) => names.push(n),
716                Token::Underscore => names.push("_".into()),
717                _ => unreachable!(),
718            }
719            if matches!(self.peek(), Token::Colon) {
720                break;
721            }
722            // could be `Group G` type application without names
723            if matches!(
724                self.peek(),
725                Token::RBrace | Token::RBracket | Token::RStrict | Token::RParen
726            ) {
727                break;
728            }
729            // continue collecting — ambiguous
730        }
731        if matches!(self.peek(), Token::Colon) {
732            self.bump();
733            let ty = self.parse_type()?;
734            binders.push(Binder {
735                kind,
736                names,
737                ty: Some(Box::new(ty)),
738            });
739        } else {
740            // Rewind-ish: treat everything as type expression
741            // If we collected names without colon, it's `Group α` type
742            self.pos = start_pos;
743            let ty = self.parse_type()?;
744            binders.push(Binder {
745                kind,
746                names: vec![],
747                ty: Some(Box::new(ty)),
748            });
749        }
750        // optional more binder groups inside same braces rare
751        self.expect(&close)?;
752        Ok(binders)
753    }
754
755    /// Try to parse `(x : T)` as a Pi binder; returns None if it looks like grouping.
756    fn try_parse_leading_pi_binder(&mut self) -> Result<Option<Binder>> {
757        let save = self.pos;
758        let kind = match self.peek() {
759            Token::LParen => BinderKind::Default,
760            Token::LBrace => BinderKind::Implicit,
761            Token::LBracket => BinderKind::Instance,
762            Token::LStrict => BinderKind::StrictImplicit,
763            _ => return Ok(None),
764        };
765        // Need: open, name+, colon, type, close, arrow
766        let open_tok = self.bump();
767        let mut names = Vec::new();
768        while matches!(self.peek(), Token::Ident(_) | Token::Underscore) {
769            match self.bump() {
770                Token::Ident(n) => names.push(n),
771                Token::Underscore => names.push("_".into()),
772                _ => unreachable!(),
773            }
774            if matches!(self.peek(), Token::Colon) {
775                break;
776            }
777            // if we see something else before colon and only one "name", might be `(Nat)` grouping
778            if !matches!(self.peek(), Token::Ident(_) | Token::Underscore | Token::Colon)
779            {
780                break;
781            }
782        }
783        if names.is_empty() || !matches!(self.peek(), Token::Colon) {
784            self.pos = save;
785            return Ok(None);
786        }
787        self.bump(); // colon
788        let ty = match self.parse_type() {
789            Ok(t) => t,
790            Err(_) => {
791                self.pos = save;
792                return Ok(None);
793            }
794        };
795        let close_ok = match kind {
796            BinderKind::Default => matches!(self.peek(), Token::RParen),
797            BinderKind::Implicit => matches!(self.peek(), Token::RBrace),
798            BinderKind::Instance => matches!(self.peek(), Token::RBracket),
799            BinderKind::StrictImplicit => matches!(self.peek(), Token::RStrict),
800        };
801        if !close_ok {
802            self.pos = save;
803            return Ok(None);
804        }
805        self.bump();
806        // Only treat as binder if arrow follows OR we already know it's binder form
807        if matches!(self.peek(), Token::Arrow) {
808            Ok(Some(Binder {
809                kind,
810                names,
811                ty: Some(Box::new(ty)),
812            }))
813        } else {
814            // `(x : T)` alone isn't a type; restore
815            let _ = open_tok;
816            self.pos = save;
817            Ok(None)
818        }
819    }
820}
821
822fn is_atomic_start(tok: &Token) -> bool {
823    match tok {
824        Token::Ident(_)
825        | Token::Nat(_)
826        | Token::Literal(_)
827        | Token::Underscore
828        | Token::NamedHole(_)
829        | Token::LParen
830        | Token::LBrace
831        | Token::LBracket
832        | Token::LStrict
833        | Token::Forall
834        | Token::Exists
835        | Token::Fun
836        | Token::Pipe => true,
837        Token::Op(s) => s == "∑" || s == "∏" || s == "∫" || s == "¬" || s == "-",
838        _ => false,
839    }
840}
841
842fn is_infix_op(s: &str) -> bool {
843    matches!(
844        s,
845        "=" | "≠"
846            | "<"
847            | ">"
848            | "≤"
849            | "≥"
850            | "+"
851            | "-"
852            | "*"
853            | "/"
854            | "%"
855            | "^"
856            | "∧"
857            | "∨"
858            | "↔"
859            | "∘"
860            | "∈"
861            | "∉"
862            | "⊆"
863            | "⊂"
864            | "∪"
865            | "∩"
866            | "++"
867            | "::"
868            | "|>"
869            | "<|"
870            | "|>."
871            | "$"
872            | "≈"
873            | "≃"
874            | "≅"
875            | "≡"
876            | "⋅"
877            | "•"
878            | "⋆"
879            | "▸"
880            | "∥"
881            | "∣"
882            | "→"
883    ) || s == "→"
884}
885
886fn op_prec(op: &str) -> u8 {
887    match op {
888        "$" | "<|" | "|>" | "|>." => 1,
889        "↔" => 2,
890        "∨" => 3,
891        "∧" => 4,
892        "=" | "≠" | "<" | ">" | "≤" | "≥" | "∈" | "∉" | "⊆" | "⊂" | "≈" | "≃" | "≅" | "≡"
893        | "∥" | "∣" => 5,
894        "∪" | "++" => 6,
895        "∩" => 7,
896        "::" => 8,
897        "+" | "-" => 9,
898        "*" | "/" | "%" | "⋅" | "•" | "⋆" => 10,
899        "∘" => 11,
900        "^" => 12,
901        "▸" => 13,
902        _ => 5,
903    }
904}
905
906fn is_right_assoc(op: &str) -> bool {
907    matches!(op, "^" | "↔" | "::" | "$" | "∘")
908}
909
910fn format_binders_surface(binders: &[Binder]) -> String {
911    binders
912        .iter()
913        .map(|b| {
914            let names = b.names.join(" ");
915            let core = match &b.ty {
916                Some(t) if names.is_empty() => t.surface(),
917                Some(t) => format!("{names} : {}", t.surface()),
918                None => names,
919            };
920            match b.kind {
921                BinderKind::Default => format!("({core})"),
922                BinderKind::Implicit => format!("{{{core}}}"),
923                BinderKind::Instance => format!("[{core}]"),
924                BinderKind::StrictImplicit => format!("⦃{core}⦄"),
925            }
926        })
927        .collect::<Vec<_>>()
928        .join(" ")
929}
930
931fn format_binder_raw(s: &str) -> String {
932    s.to_string()
933}
934
935fn binder_fallback_name(b: &Binder) -> String {
936    format_binders_surface(std::slice::from_ref(b))
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942
943    #[test]
944    fn parse_eq_add() {
945        let t = parse_type("n + m = m + n").unwrap();
946        assert!(matches!(&t, TypeExpr::BinOp { op, .. } if op == "="));
947        assert_eq!(t.conclusion().head_key(), "op:=");
948    }
949
950    #[test]
951    fn parse_forall() {
952        let t = parse_type("∀ (n m : Nat), n + m = m + n").unwrap();
953        match t {
954            TypeExpr::Forall { binders, body } => {
955                assert_eq!(binders[0].names, vec!["n", "m"]);
956                assert!(matches!(body.as_ref(), TypeExpr::BinOp { op, .. } if op == "="));
957            }
958            _ => panic!("expected forall"),
959        }
960    }
961
962    #[test]
963    fn parse_arrow_chain() {
964        let t = parse_type("Nat → Nat → Prop").unwrap();
965        match t {
966            TypeExpr::Arrow(a, b) => {
967                assert!(matches!(a.as_ref(), TypeExpr::Ident(s) if s == "Nat"));
968                assert!(matches!(b.as_ref(), TypeExpr::Arrow(_, _)));
969            }
970            _ => panic!("expected arrow"),
971        }
972    }
973
974    #[test]
975    fn parse_pi_binder() {
976        let t = parse_type("(n : Nat) → n + 0 = n").unwrap();
977        assert!(matches!(t, TypeExpr::Pi { .. }));
978    }
979
980    #[test]
981    fn parse_holes_and_named() {
982        let t = parse_type("?a - ?a = 0").unwrap();
983        match t {
984            TypeExpr::BinOp { op, left, right } if op == "=" => {
985                assert!(matches!(right.as_ref(), TypeExpr::NatLit(n) if n == "0"));
986                match left.as_ref() {
987                    TypeExpr::BinOp { op, left, right } if op == "-" => {
988                        assert!(matches!(left.as_ref(), TypeExpr::NamedHole(a) if a == "a"));
989                        assert!(matches!(right.as_ref(), TypeExpr::NamedHole(a) if a == "a"));
990                    }
991                    _ => panic!("expected subtraction"),
992                }
993            }
994            _ => panic!("expected eq"),
995        }
996    }
997
998    #[test]
999    fn parse_search_turnstile() {
1000        let p = parse_search_pattern("|- tsum _ = _ * tsum _").unwrap();
1001        assert!(p.conclusion_only);
1002        assert!(matches!(p.expr, TypeExpr::BinOp { op, .. } if op == "="));
1003    }
1004
1005    #[test]
1006    fn parse_iff_and() {
1007        let t = parse_type("p ∧ q ↔ q ∧ p").unwrap();
1008        assert!(matches!(t, TypeExpr::BinOp { op, .. } if op == "↔"));
1009    }
1010}