Skip to main content

logicaffeine_kernel/interface/
term_parser.rs

1//! Parser for Kernel Term syntax.
2//!
3//! Parses the following grammar:
4//! ```text
5//! term   ::= lambda | forall | fix | match | arrow
6//! arrow  ::= app ("->" arrow)?
7//! app    ::= atom+
8//! atom   ::= "(" term ")" | sort | ident
9//! sort   ::= "Prop" | "Type" | "Type" digit+
10//! lambda ::= "fun" ident ":" term "=>" term
11//! forall ::= "forall" ident ":" term "," term
12//! fix    ::= "fix" ident "=>" term
13//! match  ::= "match" term "return" term "with" ("|" ident+ "=>" term)+
14//! ```
15
16use crate::{Literal, Term, Universe};
17use super::error::ParseError;
18use std::collections::HashSet;
19
20/// Recursive descent parser for Term syntax.
21pub struct TermParser<'a> {
22    input: &'a str,
23    pos: usize,
24    /// Variables currently in scope (bound by lambda, forall, fix, or match case)
25    bound_vars: HashSet<String>,
26    /// How many leading implicit binders `{x:T}` were parsed (for the elaborator).
27    implicit_count: usize,
28}
29
30impl<'a> TermParser<'a> {
31    /// Parse a term from input string.
32    pub fn parse(input: &'a str) -> Result<Term, ParseError> {
33        Self::parse_with_implicits(input).map(|(t, _)| t)
34    }
35
36    /// Parse a term, also returning how many leading implicit binders `{x:T}` it had —
37    /// the implicit-argument count the elaborator uses at application sites.
38    pub fn parse_with_implicits(input: &'a str) -> Result<(Term, usize), ParseError> {
39        let mut parser = Self {
40            input,
41            pos: 0,
42            bound_vars: HashSet::new(),
43            implicit_count: 0,
44        };
45        let term = parser.parse_term()?;
46        parser.skip_whitespace();
47        Ok((term, parser.implicit_count))
48    }
49
50    /// Parse a term (top-level).
51    fn parse_term(&mut self) -> Result<Term, ParseError> {
52        self.skip_whitespace();
53
54        if self.peek_keyword("fun") {
55            self.parse_lambda()
56        } else if self.peek_keyword("forall") {
57            self.parse_forall()
58        } else if self.peek_keyword("fix") {
59            self.parse_fix()
60        } else if self.peek_keyword("match") {
61            self.parse_match()
62        } else if self.peek_keyword("let") {
63            self.parse_let()
64        } else if self.peek_char('{') {
65            self.parse_implicit_binder()
66        } else {
67            self.parse_arrow()
68        }
69    }
70
71    /// Parse a let-expression `let x := e in body` (an optional `: T` annotation is
72    /// accepted and ignored), desugaring to `body[x := e]` — a local binding as
73    /// capture-avoiding substitution, so no new kernel term is needed.
74    fn parse_let(&mut self) -> Result<Term, ParseError> {
75        self.consume_keyword("let")?;
76        self.skip_whitespace();
77        let name = self.parse_ident()?;
78        self.skip_whitespace();
79        // Optional `: T` ascription (parsed and discarded — the value carries its type).
80        if self.peek_char(':') && !self.peek_str(":=") {
81            self.expect_char(':')?;
82            let _ty = self.parse_term()?;
83            self.skip_whitespace();
84        }
85        self.expect_str(":=")?;
86        self.skip_whitespace();
87        let value = self.parse_term()?;
88        self.skip_whitespace();
89        self.consume_keyword("in")?;
90
91        let was_bound = self.bound_vars.contains(&name);
92        self.bound_vars.insert(name.clone());
93        let body = self.parse_term()?;
94        if !was_bound {
95            self.bound_vars.remove(&name);
96        }
97
98        Ok(crate::type_checker::substitute(&body, &name, &value))
99    }
100
101    /// Parse an implicit binder `{x : T}` (optionally followed by `->`) and the body —
102    /// `Π{x:T}. body`, sugar marking `x` as an implicit argument the elaborator infers.
103    /// The binder is an ordinary `Pi` in the kernel; the implicit-ness is tracked by
104    /// `implicit_count` (the kernel `Term` has no implicit flag).
105    fn parse_implicit_binder(&mut self) -> Result<Term, ParseError> {
106        self.expect_char('{')?;
107        self.skip_whitespace();
108        let param = self.parse_ident()?;
109        self.skip_whitespace();
110        self.expect_char(':')?;
111        let param_type = self.parse_term()?;
112        self.skip_whitespace();
113        self.expect_char('}')?;
114        self.implicit_count += 1;
115
116        self.skip_whitespace();
117        self.try_consume("->"); // optional arrow between the binder and the body
118
119        let was_bound = self.bound_vars.contains(&param);
120        self.bound_vars.insert(param.clone());
121        let body_type = self.parse_term()?;
122        if !was_bound {
123            self.bound_vars.remove(&param);
124        }
125
126        Ok(Term::Pi {
127            param,
128            param_type: Box::new(param_type),
129            body_type: Box::new(body_type),
130        })
131    }
132
133    /// Parse arrow type: A -> B -> C (right associative)
134    fn parse_arrow(&mut self) -> Result<Term, ParseError> {
135        let left = self.parse_app()?;
136
137        self.skip_whitespace();
138        if self.try_consume("->") {
139            let right = self.parse_arrow()?;
140            // A -> B is sugar for Π(_:A). B
141            Ok(Term::Pi {
142                param: "_".to_string(),
143                param_type: Box::new(left),
144                body_type: Box::new(right),
145            })
146        } else {
147            Ok(left)
148        }
149    }
150
151    /// Parse application: f x y z (left associative)
152    fn parse_app(&mut self) -> Result<Term, ParseError> {
153        let mut func = self.parse_atom()?;
154
155        loop {
156            self.skip_whitespace();
157            // Check if we can parse another atom
158            // Stop at: ), ->, =>, ,, |, with, return, end, ., EOF, or keywords
159            if self.at_end()
160                || self.peek_char(')')
161                || self.peek_char('}')
162                || self.peek_char('{')
163                || self.peek_char('.')
164                || self.peek_char(',')
165                || self.peek_char('|')
166                || self.peek_char('⟩')
167                || self.peek_str("->")
168                || self.peek_str("=>")
169                || self.peek_str(":=")
170                || self.peek_keyword("with")
171                || self.peek_keyword("return")
172                || self.peek_keyword("end")
173                || self.peek_keyword("in")
174            {
175                break;
176            }
177
178            let arg = self.parse_atom()?;
179            func = Term::App(Box::new(func), Box::new(arg));
180        }
181
182        Ok(func)
183    }
184
185    /// Parse an atom: (term), number, string, sort, or identifier
186    fn parse_atom(&mut self) -> Result<Term, ParseError> {
187        let base = self.parse_atom_base()?;
188        self.parse_dot_postfix(base)
189    }
190
191    /// Parse the base of an atom, before any postfix projection.
192    fn parse_atom_base(&mut self) -> Result<Term, ParseError> {
193        self.skip_whitespace();
194
195        // Anonymous constructor ⟨e₁, …, eₙ⟩ (E3)
196        if self.peek_char('⟨') {
197            return self.parse_anon_ctor();
198        }
199
200        // Check for negative number
201        if self.peek_char('-') {
202            if let Some(lit) = self.try_parse_negative_number() {
203                return Ok(lit);
204            }
205        }
206
207        // Check for positive number
208        if let Some(c) = self.peek() {
209            if c.is_ascii_digit() {
210                return self.parse_number_literal();
211            }
212        }
213
214        // Check for string literal
215        if self.peek_char('"') {
216            return self.parse_string_literal();
217        }
218
219        if self.peek_char('(') {
220            self.parse_parens()
221        } else {
222            self.parse_ident_or_sort()
223        }
224    }
225
226    /// Apply zero or more TIGHT postfix projections `.field` to `base` (E4). Tight means no
227    /// intervening whitespace: `p.fst` projects, while `p .` / `p.` at a clause end stays a
228    /// terminator, and only a `.` immediately followed by an identifier start is a field.
229    fn parse_dot_postfix(&mut self, mut base: Term) -> Result<Term, ParseError> {
230        while self.peek_char('.') {
231            let after = self.pos + '.'.len_utf8();
232            let is_field = self.input[after..]
233                .chars()
234                .next()
235                .map(|c| c.is_alphabetic() || c == '_')
236                .unwrap_or(false);
237            if !is_field {
238                break;
239            }
240            self.advance(); // consume '.'
241            let field = self.parse_ident()?;
242            base = Term::App(
243                Box::new(Term::App(
244                    Box::new(Term::Global(crate::elaborate::DOT_MARKER.to_string())),
245                    Box::new(base),
246                )),
247                Box::new(Term::Global(field)),
248            );
249        }
250        Ok(base)
251    }
252
253    /// Parse an anonymous constructor `⟨e₁, …, eₙ⟩` into the reserved marker application
254    /// `⟨anon⟩ e₁ … eₙ` (E3). The elaborator resolves the marker against the expected type.
255    fn parse_anon_ctor(&mut self) -> Result<Term, ParseError> {
256        self.expect_char('⟨')?;
257        let mut comps = Vec::new();
258        self.skip_whitespace();
259        if !self.peek_char('⟩') {
260            loop {
261                comps.push(self.parse_term()?);
262                self.skip_whitespace();
263                if self.try_consume(",") {
264                    self.skip_whitespace();
265                    continue;
266                }
267                break;
268            }
269        }
270        self.expect_char('⟩')?;
271        let mut t = Term::Global(crate::elaborate::ANON_CTOR_MARKER.to_string());
272        for c in comps {
273            t = Term::App(Box::new(t), Box::new(c));
274        }
275        Ok(t)
276    }
277
278    /// Parse a positive integer literal
279    fn parse_number_literal(&mut self) -> Result<Term, ParseError> {
280        let start = self.pos;
281
282        while let Some(c) = self.peek() {
283            if c.is_ascii_digit() {
284                self.advance();
285            } else {
286                break;
287            }
288        }
289
290        let num_str = &self.input[start..self.pos];
291        let value: i64 = num_str
292            .parse()
293            .map_err(|_| ParseError::InvalidNumber(num_str.to_string()))?;
294
295        Ok(Term::Lit(Literal::Int(value)))
296    }
297
298    /// Parse a string literal: "contents"
299    fn parse_string_literal(&mut self) -> Result<Term, ParseError> {
300        self.expect_char('"')?;
301
302        // Collect characters until closing quote
303        let mut content = String::new();
304        loop {
305            match self.peek() {
306                Some('"') => {
307                    self.advance();
308                    break;
309                }
310                Some('\\') => {
311                    // Handle escape sequences
312                    self.advance();
313                    match self.peek() {
314                        Some('n') => {
315                            content.push('\n');
316                            self.advance();
317                        }
318                        Some('t') => {
319                            content.push('\t');
320                            self.advance();
321                        }
322                        Some('\\') => {
323                            content.push('\\');
324                            self.advance();
325                        }
326                        Some('"') => {
327                            content.push('"');
328                            self.advance();
329                        }
330                        Some(c) => {
331                            content.push(c);
332                            self.advance();
333                        }
334                        None => return Err(ParseError::UnexpectedEof),
335                    }
336                }
337                Some(c) => {
338                    content.push(c);
339                    self.advance();
340                }
341                None => return Err(ParseError::UnexpectedEof),
342            }
343        }
344
345        Ok(Term::Lit(Literal::Text(content)))
346    }
347
348    /// Try to parse a negative number, returning None if not a number
349    fn try_parse_negative_number(&mut self) -> Option<Term> {
350        // Look ahead: - followed by digit
351        if !self.peek_char('-') {
352            return None;
353        }
354        let after_dash = self.pos + 1;
355        if after_dash >= self.input.len() {
356            return None;
357        }
358        let next = self.input[after_dash..].chars().next()?;
359        if !next.is_ascii_digit() {
360            return None;
361        }
362
363        // Consume the dash
364        self.advance();
365
366        // Parse the number
367        let start = self.pos;
368        while let Some(c) = self.peek() {
369            if c.is_ascii_digit() {
370                self.advance();
371            } else {
372                break;
373            }
374        }
375
376        let num_str = &self.input[start..self.pos];
377        let value: i64 = num_str.parse().ok()?;
378
379        Some(Term::Lit(Literal::Int(-value)))
380    }
381
382    /// Parse parenthesized term
383    fn parse_parens(&mut self) -> Result<Term, ParseError> {
384        self.expect_char('(')?;
385        let term = self.parse_term()?;
386        self.skip_whitespace();
387        self.expect_char(')')?;
388        Ok(term)
389    }
390
391    /// Parse identifier or sort (Prop, Type, Type0, etc.)
392    fn parse_ident_or_sort(&mut self) -> Result<Term, ParseError> {
393        let ident = self.parse_ident()?;
394
395        match ident.as_str() {
396            "Prop" => Ok(Term::Sort(Universe::Prop)),
397            "Type" => {
398                // Check for Type followed by a number
399                self.skip_whitespace();
400                if let Some(n) = self.try_parse_number() {
401                    Ok(Term::Sort(Universe::Type(n)))
402                } else {
403                    Ok(Term::Sort(Universe::Type(0)))
404                }
405            }
406            _ => {
407                // Check for TypeN (e.g., Type0, Type1)
408                if ident.starts_with("Type") {
409                    if let Ok(n) = ident[4..].parse::<u32>() {
410                        return Ok(Term::Sort(Universe::Type(n)));
411                    }
412                }
413                // Check if this is a bound variable
414                if self.bound_vars.contains(&ident) {
415                    Ok(Term::Var(ident))
416                } else {
417                    Ok(Term::Global(ident))
418                }
419            }
420        }
421    }
422
423    /// Parse lambda: fun x : T => body
424    fn parse_lambda(&mut self) -> Result<Term, ParseError> {
425        self.consume_keyword("fun")?;
426        self.skip_whitespace();
427        let param = self.parse_ident()?;
428        self.skip_whitespace();
429        self.expect_char(':')?;
430        let param_type = self.parse_term()?;
431        self.skip_whitespace();
432        self.expect_str("=>")?;
433
434        // Add param to bound variables, parse body, then remove
435        let was_bound = self.bound_vars.contains(&param);
436        self.bound_vars.insert(param.clone());
437        let body = self.parse_term()?;
438        if !was_bound {
439            self.bound_vars.remove(&param);
440        }
441
442        Ok(Term::Lambda {
443            param,
444            param_type: Box::new(param_type),
445            body: Box::new(body),
446        })
447    }
448
449    /// Parse forall: forall x : T, body
450    fn parse_forall(&mut self) -> Result<Term, ParseError> {
451        self.consume_keyword("forall")?;
452        self.skip_whitespace();
453        let param = self.parse_ident()?;
454        self.skip_whitespace();
455        self.expect_char(':')?;
456        let param_type = self.parse_term()?;
457        self.skip_whitespace();
458        self.expect_char(',')?;
459
460        // Add param to bound variables, parse body, then remove
461        let was_bound = self.bound_vars.contains(&param);
462        self.bound_vars.insert(param.clone());
463        let body_type = self.parse_term()?;
464        if !was_bound {
465            self.bound_vars.remove(&param);
466        }
467
468        Ok(Term::Pi {
469            param,
470            param_type: Box::new(param_type),
471            body_type: Box::new(body_type),
472        })
473    }
474
475    /// Parse fix: fix f => body
476    fn parse_fix(&mut self) -> Result<Term, ParseError> {
477        self.consume_keyword("fix")?;
478        self.skip_whitespace();
479        let name = self.parse_ident()?;
480        self.skip_whitespace();
481        self.expect_str("=>")?;
482
483        // Add name to bound variables, parse body, then remove
484        let was_bound = self.bound_vars.contains(&name);
485        self.bound_vars.insert(name.clone());
486        let body = self.parse_term()?;
487        if !was_bound {
488            self.bound_vars.remove(&name);
489        }
490
491        Ok(Term::Fix {
492            name,
493            body: Box::new(body),
494        })
495    }
496
497    /// Parse match: match e return M with | C1 x => t1 | C2 y z => t2 end
498    fn parse_match(&mut self) -> Result<Term, ParseError> {
499        self.consume_keyword("match")?;
500        self.skip_whitespace();
501        let discriminant = self.parse_app()?;
502        self.skip_whitespace();
503        // The `return <motive>` clause is OPTIONAL: when omitted, the motive is left as a
504        // `Hole` for the elaborator to infer (`match e with …`).
505        let motive = if self.try_consume_keyword("return") {
506            self.skip_whitespace();
507            let m = self.parse_app()?;
508            self.skip_whitespace();
509            m
510        } else {
511            Term::Hole
512        };
513        self.consume_keyword("with")?;
514
515        let mut cases = Vec::new();
516        loop {
517            self.skip_whitespace();
518            if !self.try_consume("|") {
519                break;
520            }
521            self.skip_whitespace();
522
523            // Parse pattern: C x y z (constructor with binders)
524            let case_term = self.parse_case_body()?;
525            cases.push(case_term);
526        }
527
528        // Consume optional 'end' keyword
529        self.skip_whitespace();
530        let _ = self.try_consume_keyword("end");
531
532        Ok(Term::Match {
533            discriminant: Box::new(discriminant),
534            motive: Box::new(motive),
535            cases,
536        })
537    }
538
539    /// Parse a match case body: C x y => term becomes λx. λy. term
540    ///
541    /// The first identifier is the constructor name (skipped), the rest are binders.
542    fn parse_case_body(&mut self) -> Result<Term, ParseError> {
543        // First identifier is the constructor name (skip it)
544        self.skip_whitespace();
545        let _ctor_name = self.parse_ident()?;
546
547        // Collect binders until =>
548        let mut binders = Vec::new();
549        loop {
550            self.skip_whitespace();
551            if self.peek_str("=>") {
552                break;
553            }
554            let ident = self.parse_ident()?;
555            binders.push(ident);
556        }
557        self.expect_str("=>")?;
558
559        // Add all binders to bound vars
560        let mut previously_bound = Vec::new();
561        for binder in &binders {
562            previously_bound.push(self.bound_vars.contains(binder));
563            self.bound_vars.insert(binder.clone());
564        }
565
566        let mut body = self.parse_term()?;
567
568        // Remove binders that weren't previously bound
569        for (i, binder) in binders.iter().enumerate() {
570            if !previously_bound[i] {
571                self.bound_vars.remove(binder);
572            }
573        }
574
575        // Wrap in lambdas from right to left
576        // We don't know the types, so we use a placeholder
577        for binder in binders.into_iter().rev() {
578            body = Term::Lambda {
579                param: binder,
580                param_type: Box::new(Term::Global("_".to_string())), // Placeholder type
581                body: Box::new(body),
582            };
583        }
584
585        Ok(body)
586    }
587
588    // =========================================================================
589    // Low-level parsing utilities
590    // =========================================================================
591
592    /// Parse an identifier (alphanumeric + underscore, starting with letter/underscore)
593    fn parse_ident(&mut self) -> Result<String, ParseError> {
594        self.skip_whitespace();
595        let start = self.pos;
596
597        // First character must be letter or underscore
598        if let Some(c) = self.peek() {
599            if !c.is_alphabetic() && c != '_' {
600                return Err(ParseError::Expected {
601                    expected: "identifier".to_string(),
602                    found: format!("{}", c),
603                });
604            }
605        } else {
606            return Err(ParseError::UnexpectedEof);
607        }
608
609        // Consume alphanumeric and underscore
610        while let Some(c) = self.peek() {
611            if c.is_alphanumeric() || c == '_' {
612                self.advance();
613            } else {
614                break;
615            }
616        }
617
618        let ident = self.input[start..self.pos].to_string();
619        if ident.is_empty() {
620            Err(ParseError::InvalidIdent("empty".to_string()))
621        } else {
622            Ok(ident)
623        }
624    }
625
626    /// Try to parse a number, returning None if not present
627    fn try_parse_number(&mut self) -> Option<u32> {
628        self.skip_whitespace();
629        let start = self.pos;
630
631        while let Some(c) = self.peek() {
632            if c.is_ascii_digit() {
633                self.advance();
634            } else {
635                break;
636            }
637        }
638
639        if self.pos > start {
640            self.input[start..self.pos].parse().ok()
641        } else {
642            None
643        }
644    }
645
646    /// Skip whitespace
647    fn skip_whitespace(&mut self) {
648        while let Some(c) = self.peek() {
649            if c.is_whitespace() {
650                self.advance();
651            } else {
652                break;
653            }
654        }
655    }
656
657    /// Peek at current character
658    fn peek(&self) -> Option<char> {
659        self.input[self.pos..].chars().next()
660    }
661
662    /// Check if we're at a specific character
663    fn peek_char(&self, c: char) -> bool {
664        self.peek() == Some(c)
665    }
666
667    /// Check if input starts with a string at current position
668    fn peek_str(&self, s: &str) -> bool {
669        self.input[self.pos..].starts_with(s)
670    }
671
672    /// Check if input starts with a keyword (followed by non-alphanumeric)
673    fn peek_keyword(&self, keyword: &str) -> bool {
674        if !self.peek_str(keyword) {
675            return false;
676        }
677        // Check that keyword is not part of a longer identifier
678        let after = self.pos + keyword.len();
679        if after >= self.input.len() {
680            return true;
681        }
682        let next_char = self.input[after..].chars().next();
683        !next_char.map(|c| c.is_alphanumeric() || c == '_').unwrap_or(false)
684    }
685
686    /// Advance position by one character
687    fn advance(&mut self) {
688        if let Some(c) = self.peek() {
689            self.pos += c.len_utf8();
690        }
691    }
692
693    /// Check if at end of input
694    fn at_end(&self) -> bool {
695        self.pos >= self.input.len()
696    }
697
698    /// Try to consume a string, returning true if successful
699    fn try_consume(&mut self, s: &str) -> bool {
700        if self.peek_str(s) {
701            self.pos += s.len();
702            true
703        } else {
704            false
705        }
706    }
707
708    /// Try to consume a keyword, returning true if successful
709    fn try_consume_keyword(&mut self, keyword: &str) -> bool {
710        self.skip_whitespace();
711        if self.peek_keyword(keyword) {
712            self.pos += keyword.len();
713            true
714        } else {
715            false
716        }
717    }
718
719    /// Expect a specific character
720    fn expect_char(&mut self, expected: char) -> Result<(), ParseError> {
721        self.skip_whitespace();
722        if self.peek_char(expected) {
723            self.advance();
724            Ok(())
725        } else {
726            Err(ParseError::Expected {
727                expected: format!("'{}'", expected),
728                found: self.peek().map(|c| c.to_string()).unwrap_or("EOF".to_string()),
729            })
730        }
731    }
732
733    /// Expect a specific string
734    fn expect_str(&mut self, expected: &str) -> Result<(), ParseError> {
735        self.skip_whitespace();
736        if self.try_consume(expected) {
737            Ok(())
738        } else {
739            let found: String = self.input[self.pos..].chars().take(expected.len()).collect();
740            Err(ParseError::Expected {
741                expected: format!("'{}'", expected),
742                found: if found.is_empty() { "EOF".to_string() } else { found },
743            })
744        }
745    }
746
747    /// Consume a keyword (must be followed by non-alphanumeric)
748    fn consume_keyword(&mut self, keyword: &str) -> Result<(), ParseError> {
749        self.skip_whitespace();
750        if self.peek_keyword(keyword) {
751            self.pos += keyword.len();
752            Ok(())
753        } else {
754            Err(ParseError::Expected {
755                expected: keyword.to_string(),
756                found: self.peek().map(|c| c.to_string()).unwrap_or("EOF".to_string()),
757            })
758        }
759    }
760
761    /// Get remaining input (for debugging)
762    #[allow(dead_code)]
763    fn remaining(&self) -> &str {
764        &self.input[self.pos..]
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    #[test]
773    fn test_parse_global() {
774        let term = TermParser::parse("Zero").unwrap();
775        assert!(matches!(term, Term::Global(ref s) if s == "Zero"));
776    }
777
778    #[test]
779    fn test_parse_sort() {
780        let term = TermParser::parse("Type").unwrap();
781        assert!(matches!(term, Term::Sort(Universe::Type(0))));
782
783        let term2 = TermParser::parse("Prop").unwrap();
784        assert!(matches!(term2, Term::Sort(Universe::Prop)));
785    }
786
787    #[test]
788    fn test_parse_app() {
789        let term = TermParser::parse("Succ Zero").unwrap();
790        if let Term::App(func, arg) = term {
791            assert!(matches!(*func, Term::Global(ref s) if s == "Succ"));
792            assert!(matches!(*arg, Term::Global(ref s) if s == "Zero"));
793        } else {
794            panic!("Expected App");
795        }
796    }
797
798    #[test]
799    fn test_parse_parens() {
800        let term = TermParser::parse("(Succ Zero)").unwrap();
801        assert!(matches!(term, Term::App(..)));
802    }
803
804    #[test]
805    fn test_parse_arrow() {
806        let term = TermParser::parse("Nat -> Nat").unwrap();
807        if let Term::Pi { param, param_type, body_type } = term {
808            assert_eq!(param, "_");
809            assert!(matches!(*param_type, Term::Global(ref s) if s == "Nat"));
810            assert!(matches!(*body_type, Term::Global(ref s) if s == "Nat"));
811        } else {
812            panic!("Expected Pi");
813        }
814    }
815
816    #[test]
817    fn test_parse_lambda() {
818        let term = TermParser::parse("fun x : Nat => Succ x").unwrap();
819        if let Term::Lambda { param, body, .. } = term {
820            assert_eq!(param, "x");
821            // Body should use Var for bound x
822            if let Term::App(_, arg) = *body {
823                assert!(matches!(*arg, Term::Var(ref s) if s == "x"));
824            } else {
825                panic!("Expected App in lambda body");
826            }
827        } else {
828            panic!("Expected Lambda");
829        }
830    }
831
832    #[test]
833    fn test_parse_lambda_bound_var() {
834        let term = TermParser::parse("fun n : Nat => Succ n").unwrap();
835        if let Term::Lambda { body, .. } = term {
836            if let Term::App(_, arg) = *body {
837                assert!(
838                    matches!(*arg, Term::Var(ref s) if s == "n"),
839                    "Expected Var(n), got {:?}",
840                    arg
841                );
842            } else {
843                panic!("Expected App in lambda body");
844            }
845        } else {
846            panic!("Expected Lambda");
847        }
848    }
849}