Skip to main content

rlean_search/parser/
decl.rs

1//! Extract theorem / lemma / axiom declarations from Lean 4 source files.
2
3use crate::ast::{Binder, BinderKind, DeclKind, Declaration, TypeExpr};
4use crate::lexer::{Lexer, Token};
5use crate::parser::type_expr::{parse_type, ParseError};
6use std::path::Path;
7
8/// Parse all theorem/lemma/axiom declarations from a `.lean` source string.
9pub fn parse_declarations(source: &str, file: &str) -> Vec<Declaration> {
10    parse_declarations_with_path(source, file, None)
11}
12
13pub fn parse_declarations_with_path(
14    source: &str,
15    file: &str,
16    module_hint: Option<&str>,
17) -> Vec<Declaration> {
18    let mut decls = Vec::new();
19    let mut namespace_stack: Vec<String> = Vec::new();
20    let mut section_depth: usize = 0;
21
22    // Strip / track namespaces with a simple line scan; extract decls with a more careful scan.
23    let lines: Vec<&str> = source.lines().collect();
24    let mut i = 0usize;
25
26    while i < lines.len() {
27        let raw = lines[i];
28        let line = strip_line_comment(raw).trim();
29
30        if line.is_empty() {
31            i += 1;
32            continue;
33        }
34
35        // Block comment only lines — skip start; naive skip until -/
36        if line.starts_with("/-") && !line.contains("-/") {
37            i += 1;
38            while i < lines.len() && !lines[i].contains("-/") {
39                i += 1;
40            }
41            i += 1;
42            continue;
43        }
44
45        if let Some(rest) = line.strip_prefix("namespace ") {
46            let name = rest.split_whitespace().next().unwrap_or("").trim();
47            if !name.is_empty() && name != "_root_" {
48                for part in name.split('.') {
49                    if !part.is_empty() {
50                        namespace_stack.push(part.to_string());
51                    }
52                }
53            }
54            i += 1;
55            continue;
56        }
57        if line == "end" || line.starts_with("end ") {
58            // end Namespace / end Section — pop if matches last ns, else section
59            let name = line.strip_prefix("end").unwrap_or("").trim();
60            if name.is_empty() {
61                if section_depth > 0 {
62                    section_depth -= 1;
63                } else if !namespace_stack.is_empty() {
64                    namespace_stack.pop();
65                }
66            } else if namespace_stack.last().map(|s| s.as_str()) == Some(name)
67                || namespace_stack
68                    .iter()
69                    .rev()
70                    .take(name.matches('.').count() + 1)
71                    .cloned()
72                    .collect::<Vec<_>>()
73                    .into_iter()
74                    .rev()
75                    .collect::<Vec<_>>()
76                    .join(".")
77                    == name
78            {
79                let parts = name.split('.').filter(|p| !p.is_empty()).count();
80                for _ in 0..parts.max(1) {
81                    namespace_stack.pop();
82                }
83            } else if section_depth > 0 {
84                section_depth -= 1;
85            } else {
86                // still try pop
87                namespace_stack.pop();
88            }
89            i += 1;
90            continue;
91        }
92        if line.starts_with("section") {
93            section_depth += 1;
94            i += 1;
95            continue;
96        }
97
98        // Attribute lines may precede declaration
99        let (attrs, decl_line, start_line_idx) = collect_decl_start(&lines, i);
100        if let Some((kind, after_kw)) = match_decl_keyword(decl_line) {
101            if let Some((decl, end_i)) = extract_one_decl_with_end(
102                &lines,
103                start_line_idx,
104                kind,
105                after_kw,
106                attrs,
107                file,
108                &namespace_stack,
109                module_hint,
110            ) {
111                decls.push(decl);
112                i = end_i;
113                continue;
114            }
115        }
116
117        i += 1;
118    }
119
120    decls
121}
122
123fn strip_line_comment(line: &str) -> &str {
124    // careful with `"--"` in strings — good enough for declarations
125    if let Some(idx) = find_line_comment(line) {
126        &line[..idx]
127    } else {
128        line
129    }
130}
131
132fn find_line_comment(line: &str) -> Option<usize> {
133    let bytes = line.as_bytes();
134    let mut i = 0;
135    let mut in_str = false;
136    while i + 1 < bytes.len() {
137        let c = bytes[i];
138        if in_str {
139            if c == b'\\' {
140                i += 2;
141                continue;
142            }
143            if c == b'"' {
144                in_str = false;
145            }
146            i += 1;
147            continue;
148        }
149        if c == b'"' {
150            in_str = true;
151            i += 1;
152            continue;
153        }
154        if c == b'-' && bytes[i + 1] == b'-' {
155            return Some(i);
156        }
157        i += 1;
158    }
159    None
160}
161
162fn collect_decl_start<'a>(lines: &[&'a str], i: usize) -> (Vec<String>, &'a str, usize) {
163    let mut attrs = Vec::new();
164    let mut j = i;
165    // gather pure attribute lines
166    while j < lines.len() {
167        let t = strip_line_comment(lines[j]).trim();
168        if t.starts_with("@[") && !contains_decl_keyword(t) {
169            attrs.push(t.to_string());
170            j += 1;
171            continue;
172        }
173        break;
174    }
175    if j >= lines.len() {
176        return (attrs, "", i);
177    }
178    let t = strip_line_comment(lines[j]).trim();
179    // attributes on same line as theorem
180    let (more_attrs, rest) = split_leading_attrs(t);
181    attrs.extend(more_attrs);
182    (attrs, rest, j)
183}
184
185fn split_leading_attrs(s: &str) -> (Vec<String>, &str) {
186    let mut attrs = Vec::new();
187    let mut rest = s;
188    while rest.starts_with('@') {
189        if let Some(end) = find_matching_bracket(rest, 1) {
190            // @[...]
191            attrs.push(rest[..=end].to_string());
192            rest = rest[end + 1..].trim_start();
193        } else {
194            break;
195        }
196    }
197    (attrs, rest)
198}
199
200fn find_matching_bracket(s: &str, open_idx: usize) -> Option<usize> {
201    // open_idx points at '['
202    let bytes = s.as_bytes();
203    if open_idx >= bytes.len() || bytes[open_idx] != b'[' {
204        return None;
205    }
206    let mut depth = 0i32;
207    let mut i = open_idx;
208    while i < bytes.len() {
209        match bytes[i] {
210            b'[' => depth += 1,
211            b']' => {
212                depth -= 1;
213                if depth == 0 {
214                    return Some(i);
215                }
216            }
217            _ => {}
218        }
219        i += 1;
220    }
221    None
222}
223
224fn contains_decl_keyword(s: &str) -> bool {
225    s.contains("theorem ") || s.contains("lemma ") || s.contains("axiom ")
226}
227
228fn match_decl_keyword(s: &str) -> Option<(DeclKind, &str)> {
229    let s = s.trim();
230    // modifiers
231    let mut rest = s;
232    loop {
233        let trimmed = rest.trim_start();
234        if let Some(r) = strip_modifier(trimmed) {
235            rest = r;
236            continue;
237        }
238        break;
239    }
240    let rest = rest.trim_start();
241    for (kw, kind) in [
242        ("theorem ", DeclKind::Theorem),
243        ("lemma ", DeclKind::Lemma),
244        ("axiom ", DeclKind::Axiom),
245    ] {
246        if let Some(after) = rest.strip_prefix(kw) {
247            return Some((kind, after.trim_start()));
248        }
249    }
250    None
251}
252
253fn strip_modifier(s: &str) -> Option<&str> {
254    for m in [
255        "protected ",
256        "private ",
257        "noncomputable ",
258        "public ",
259        "unsafe ",
260        "partial ",
261    ] {
262        if let Some(r) = s.strip_prefix(m) {
263            return Some(r);
264        }
265    }
266    None
267}
268
269fn extract_one_decl_with_end(
270    lines: &[&str],
271    start: usize,
272    kind: DeclKind,
273    after_kw: &str,
274    attrs: Vec<String>,
275    file: &str,
276    namespace_stack: &[String],
277    module_hint: Option<&str>,
278) -> Option<(Declaration, usize)> {
279    // Build a buffer from after_kw through the type, stopping at `:=` or bare axiom end.
280    let mut buf = after_kw.to_string();
281    let mut end = start;
282    let mut depth_paren = count_balance(&buf, '(', ')');
283    let mut depth_brace = count_balance(&buf, '{', '}');
284    let mut depth_brack = count_balance(&buf, '[', ']');
285    let seen_assign = buf.contains(":=");
286
287    if !seen_assign {
288        let mut j = start + 1;
289        while j < lines.len() {
290            let t = strip_line_comment(lines[j]);
291            let trimmed = t.trim();
292            // stop at next top-level declaration-like if we've finished type
293            if depth_paren <= 0
294                && depth_brace <= 0
295                && depth_brack <= 0
296                && looks_like_new_decl(trimmed)
297                && buf.contains(':')
298            {
299                break;
300            }
301            buf.push('\n');
302            buf.push_str(t);
303            depth_paren += count_balance(t, '(', ')');
304            depth_brace += count_balance(t, '{', '}');
305            depth_brack += count_balance(t, '[', ']');
306            end = j;
307            if t.contains(":=") {
308                break;
309            }
310            // `where` clause after type
311            if depth_paren <= 0
312                && depth_brace <= 0
313                && depth_brack <= 0
314                && (trimmed.starts_with("where") || trimmed.contains(" where "))
315                && buf.contains(':')
316            {
317                break;
318            }
319            j += 1;
320            // safety limit
321            if j > start + 80 {
322                break;
323            }
324        }
325    }
326
327    // Split name / binders / type
328    let (name, binders_src, type_src) = split_name_binders_type(&buf)?;
329    if name.is_empty() {
330        return None;
331    }
332
333    let binders = parse_binders_src(&binders_src);
334    let type_surface = collapse_ws(&type_src);
335    if type_surface.is_empty() {
336        return None;
337    }
338
339    let ty = match parse_type(&type_surface) {
340        Ok(t) => t,
341        Err(_) => {
342            // Fallback: store as Raw so we still index the declaration
343            TypeExpr::Raw(type_surface.clone())
344        }
345    };
346
347    let full_name = if namespace_stack.is_empty() {
348        name.clone()
349    } else {
350        format!("{}.{}", namespace_stack.join("."), name)
351    };
352
353    let module = module_hint.map(|s| s.to_string()).or_else(|| {
354        Path::new(file)
355            .file_stem()
356            .map(|s| s.to_string_lossy().into_owned())
357    });
358
359    let decl = Declaration {
360        kind,
361        name,
362        full_name,
363        binders,
364        ty,
365        type_surface,
366        file: file.to_string(),
367        line: start + 1,
368        module,
369        namespace_path: namespace_stack.to_vec(),
370        attributes: attrs,
371    };
372
373    Some((decl, end + 1))
374}
375
376fn looks_like_new_decl(trimmed: &str) -> bool {
377    let t = trimmed.trim_start_matches('@');
378    // attribute-only
379    if trimmed.starts_with("@[") && !contains_decl_keyword(trimmed) {
380        return true;
381    }
382    for p in [
383        "theorem ",
384        "lemma ",
385        "axiom ",
386        "def ",
387        "instance ",
388        "class ",
389        "structure ",
390        "inductive ",
391        "namespace ",
392        "end ",
393        "section ",
394        "variable ",
395        "example ",
396        "abbrev ",
397        "opaque ",
398        "mutual ",
399    ] {
400        if t.starts_with(p) || t == "end" {
401            return true;
402        }
403    }
404    // protected theorem etc.
405    for m in ["protected ", "private ", "noncomputable ", "public "] {
406        if let Some(r) = t.strip_prefix(m) {
407            return looks_like_new_decl(r);
408        }
409    }
410    false
411}
412
413fn count_balance(s: &str, open: char, close: char) -> i32 {
414    let mut d = 0i32;
415    for c in s.chars() {
416        if c == open {
417            d += 1;
418        } else if c == close {
419            d -= 1;
420        }
421    }
422    d
423}
424
425/// Split `name binders* : type` (type may include nested `:`).
426fn split_name_binders_type(buf: &str) -> Option<(String, String, String)> {
427    // Remove `:= ...` and `where ...`
428    let mut s = buf;
429    if let Some(idx) = find_top_level(s, ":=") {
430        s = &s[..idx];
431    }
432    if let Some(idx) = find_top_level_word(s, "where") {
433        s = &s[..idx];
434    }
435    // Equation-compiler clauses: `theorem foo : T | pat => proof`
436    if let Some(idx) = find_top_level_eqns(s) {
437        s = &s[..idx];
438    }
439    let s = s.trim();
440
441    // Name: first identifier (possibly dotted, or «escaped»)
442    let mut lx = Lexer::new(s);
443    let name_tok = lx.next_token();
444    let name = match name_tok {
445        Token::Ident(n) => n,
446        _ => return None,
447    };
448    // optional `.` continuation already in Ident for simple names; dotted `Foo.bar` may be two tokens
449    let name_end = lx.position();
450    // Rest is binders + : type. Find the type colon at binder depth 0.
451    let rest = s[name_end..].trim_start();
452    let (binders_src, type_src) = split_binders_and_type(rest)?;
453    Some((name, binders_src, type_src))
454}
455
456fn split_binders_and_type(rest: &str) -> Option<(String, String)> {
457    // Scan for `:` at depth 0 that starts the type.
458    // Binders are (...) {...} [...] ⦃...⦄ and bare ids rarely before colon in Lean 4
459    // for theorems usually all binders are grouped.
460    let chars: Vec<char> = rest.chars().collect();
461    let mut i = 0usize;
462
463    // Skip leading binder groups and whitespace
464    while i < chars.len() {
465        match chars[i] {
466            c if c.is_whitespace() => i += 1,
467            '(' => {
468                let mut depth_p = 1i32;
469                i += 1;
470                while i < chars.len() && depth_p > 0 {
471                    match chars[i] {
472                        '(' => depth_p += 1,
473                        ')' => depth_p -= 1,
474                        _ => {}
475                    }
476                    i += 1;
477                }
478            }
479            '{' => {
480                let mut depth_b = 1i32;
481                i += 1;
482                while i < chars.len() && depth_b > 0 {
483                    match chars[i] {
484                        '{' => depth_b += 1,
485                        '}' => depth_b -= 1,
486                        _ => {}
487                    }
488                    i += 1;
489                }
490            }
491            '[' => {
492                let mut depth_k = 1i32;
493                i += 1;
494                while i < chars.len() && depth_k > 0 {
495                    match chars[i] {
496                        '[' => depth_k += 1,
497                        ']' => depth_k -= 1,
498                        _ => {}
499                    }
500                    i += 1;
501                }
502            }
503            '⦃' => {
504                let mut depth_s = 1i32;
505                i += 1;
506                while i < chars.len() && depth_s > 0 {
507                    if chars[i] == '⦃' {
508                        depth_s += 1;
509                    } else if chars[i] == '⦄' {
510                        depth_s -= 1;
511                    }
512                    i += 1;
513                }
514            }
515            ':' => {
516                // type starts after this
517                let binders = rest[..char_byte_index(rest, i)].trim().to_string();
518                let ty = rest[char_byte_index(rest, i) + 1..].trim().to_string();
519                return Some((binders, ty));
520            }
521            // bare binder name without parens: `theorem foo n : ...` rare but handle
522            c if is_name_start(c) => {
523                // consume ident
524                i += 1;
525                while i < chars.len() && is_name_continue(chars[i]) {
526                    i += 1;
527                }
528            }
529            _ => {
530                // unexpected — try finding first top-level colon
531                break;
532            }
533        }
534    }
535
536    // Fallback: first top-level colon
537    if let Some(idx) = find_top_level(rest, ":") {
538        // ensure not :=
539        if rest[idx..].starts_with(":=") {
540            return None;
541        }
542        let binders = rest[..idx].trim().to_string();
543        let ty = rest[idx + 1..].trim().to_string();
544        Some((binders, ty))
545    } else {
546        None
547    }
548}
549
550fn char_byte_index(s: &str, char_idx: usize) -> usize {
551    s.char_indices()
552        .nth(char_idx)
553        .map(|(i, _)| i)
554        .unwrap_or(s.len())
555}
556
557fn is_name_start(c: char) -> bool {
558    c.is_alphabetic() || c == '_' || c == '«'
559}
560
561fn is_name_continue(c: char) -> bool {
562    is_name_start(c) || c.is_ascii_digit() || c == '\'' || c == '?' || c == '»'
563}
564
565fn find_top_level(s: &str, pat: &str) -> Option<usize> {
566    let mut depth_p = 0i32;
567    let mut depth_b = 0i32;
568    let mut depth_k = 0i32;
569    let bytes = s.as_bytes();
570    let mut i = 0usize;
571    while i < bytes.len() {
572        let c = s[i..].chars().next()?;
573        match c {
574            '(' => depth_p += 1,
575            ')' => depth_p -= 1,
576            '{' => depth_b += 1,
577            '}' => depth_b -= 1,
578            '[' => depth_k += 1,
579            ']' => depth_k -= 1,
580            _ => {}
581        }
582        if depth_p == 0
583            && depth_b == 0
584            && depth_k == 0
585            && s[i..].starts_with(pat)
586        {
587            // special: for ":" don't match ":="
588            if pat == ":" && s[i..].starts_with(":=") {
589                i += c.len_utf8();
590                continue;
591            }
592            return Some(i);
593        }
594        i += c.len_utf8();
595    }
596    None
597}
598
599/// Find start of equation-compiler arms after a type: top-level ` | ` with `=>` later.
600fn find_top_level_eqns(s: &str) -> Option<usize> {
601    let mut depth_p = 0i32;
602    let mut depth_b = 0i32;
603    let mut depth_k = 0i32;
604    let mut i = 0usize;
605    // Only consider after the type colon has appeared at depth 0.
606    let mut seen_type_colon = false;
607    while i < s.len() {
608        let c = s[i..].chars().next()?;
609        match c {
610            '(' => depth_p += 1,
611            ')' => depth_p -= 1,
612            '{' => depth_b += 1,
613            '}' => depth_b -= 1,
614            '[' => depth_k += 1,
615            ']' => depth_k -= 1,
616            ':' if depth_p == 0 && depth_b == 0 && depth_k == 0 && !s[i..].starts_with(":=") => {
617                seen_type_colon = true;
618            }
619            '|' if seen_type_colon && depth_p == 0 && depth_b == 0 && depth_k == 0 => {
620                // Ensure it's not `||` or infix `∣` already handled; look for `=>` or `↦` after
621                let rest = &s[i..];
622                if rest.starts_with("||") || rest.starts_with("|>") {
623                    i += c.len_utf8();
624                    continue;
625                }
626                // Match arms typically: `| pat =>` or multi `| a, b =>`
627                if rest.contains("=>") || rest.contains('↦') {
628                    // require the pipe to be preceded by whitespace or start
629                    let before_ok = i == 0
630                        || s[..i]
631                            .chars()
632                            .next_back()
633                            .map(|ch| ch.is_whitespace())
634                            .unwrap_or(true);
635                    if before_ok {
636                        return Some(i);
637                    }
638                }
639            }
640            _ => {}
641        }
642        i += c.len_utf8();
643    }
644    None
645}
646
647fn find_top_level_word(s: &str, word: &str) -> Option<usize> {
648    let mut depth_p = 0i32;
649    let mut depth_b = 0i32;
650    let mut depth_k = 0i32;
651    let mut i = 0usize;
652    while i < s.len() {
653        let c = s[i..].chars().next()?;
654        match c {
655            '(' => depth_p += 1,
656            ')' => depth_p -= 1,
657            '{' => depth_b += 1,
658            '}' => depth_b -= 1,
659            '[' => depth_k += 1,
660            ']' => depth_k -= 1,
661            _ => {}
662        }
663        if depth_p == 0 && depth_b == 0 && depth_k == 0 && s[i..].starts_with(word) {
664            let before_ok = i == 0
665                || s[..i]
666                    .chars()
667                    .next_back()
668                    .map(|ch| !ch.is_alphanumeric() && ch != '_')
669                    .unwrap_or(true);
670            let after = i + word.len();
671            let after_ok = after >= s.len()
672                || s[after..]
673                    .chars()
674                    .next()
675                    .map(|ch| !ch.is_alphanumeric() && ch != '_')
676                    .unwrap_or(true);
677            if before_ok && after_ok {
678                return Some(i);
679            }
680        }
681        i += c.len_utf8();
682    }
683    None
684}
685
686fn parse_binders_src(src: &str) -> Vec<Binder> {
687    let src = src.trim();
688    if src.is_empty() {
689        return Vec::new();
690    }
691    // Reuse type parser binder groups by wrapping: parse successive groups via lexer
692    let mut binders = Vec::new();
693    let mut rest = src;
694    while !rest.is_empty() {
695        let rest_trim = rest.trim_start();
696        if rest_trim.is_empty() {
697            break;
698        }
699        let (b, consumed) = match rest_trim.chars().next() {
700            Some('(') => parse_one_group(rest_trim, BinderKind::Default, '(', ')'),
701            Some('{') => parse_one_group(rest_trim, BinderKind::Implicit, '{', '}'),
702            Some('[') => parse_one_group(rest_trim, BinderKind::Instance, '[', ']'),
703            Some('⦃') => parse_one_group_strict(rest_trim),
704            _ => {
705                // bare name
706                let mut lx = Lexer::new(rest_trim);
707                match lx.next_token() {
708                    Token::Ident(n) => {
709                        let c = lx.position();
710                        binders.push(Binder {
711                            kind: BinderKind::Default,
712                            names: vec![n],
713                            ty: None,
714                        });
715                        rest = &rest_trim[c..];
716                        continue;
717                    }
718                    _ => break,
719                }
720            }
721        };
722        if let Some(b) = b {
723            binders.push(b);
724        }
725        if consumed == 0 {
726            break;
727        }
728        rest = &rest_trim[consumed..];
729    }
730    binders
731}
732
733fn parse_one_group(
734    s: &str,
735    kind: BinderKind,
736    open: char,
737    close: char,
738) -> (Option<Binder>, usize) {
739    if !s.starts_with(open) {
740        return (None, 0);
741    }
742    let mut depth = 0i32;
743    let mut end = 0usize;
744    for (idx, c) in s.char_indices() {
745        if c == open {
746            depth += 1;
747        } else if c == close {
748            depth -= 1;
749            if depth == 0 {
750                end = idx + c.len_utf8();
751                break;
752            }
753        }
754    }
755    if end == 0 {
756        return (None, 0);
757    }
758    let inner = &s[open.len_utf8()..end - close.len_utf8()];
759    let binder = parse_binder_inner(kind, inner);
760    (Some(binder), end)
761}
762
763fn parse_one_group_strict(s: &str) -> (Option<Binder>, usize) {
764    if !s.starts_with('⦃') {
765        return (None, 0);
766    }
767    let mut depth = 0i32;
768    let mut end = 0usize;
769    for (idx, c) in s.char_indices() {
770        if c == '⦃' {
771            depth += 1;
772        } else if c == '⦄' {
773            depth -= 1;
774            if depth == 0 {
775                end = idx + c.len_utf8();
776                break;
777            }
778        }
779    }
780    if end == 0 {
781        return (None, 0);
782    }
783    let inner = &s['⦃'.len_utf8()..end - '⦄'.len_utf8()];
784    (Some(parse_binder_inner(BinderKind::StrictImplicit, inner)), end)
785}
786
787fn parse_binder_inner(kind: BinderKind, inner: &str) -> Binder {
788    let inner = inner.trim();
789    if let Some(colon) = find_top_level(inner, ":") {
790        let names_part = inner[..colon].trim();
791        let ty_part = inner[colon + 1..].trim();
792        let names = if names_part.is_empty() {
793            vec![]
794        } else {
795            names_part
796                .split_whitespace()
797                .map(|s| s.to_string())
798                .collect()
799        };
800        let ty = parse_type(ty_part).ok().map(Box::new);
801        Binder { kind, names, ty }
802    } else {
803        // type only (instance) or names only
804        if inner.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '\'' || c == ' ' || c == '.') {
805            let names: Vec<_> = inner.split_whitespace().map(|s| s.to_string()).collect();
806            // if looks like Type App, store as type
807            if names.len() > 1 && names[0].chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
808            {
809                Binder {
810                    kind,
811                    names: vec![],
812                    ty: parse_type(inner).ok().map(Box::new),
813                }
814            } else {
815                Binder {
816                    kind,
817                    names,
818                    ty: None,
819                }
820            }
821        } else {
822            Binder {
823                kind,
824                names: vec![],
825                ty: parse_type(inner).ok().map(Box::new),
826            }
827        }
828    }
829}
830
831fn collapse_ws(s: &str) -> String {
832    s.split_whitespace().collect::<Vec<_>>().join(" ")
833}
834
835#[allow(dead_code)]
836fn _parse_error_unused(_: ParseError) {}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    #[test]
843    fn parse_simple_theorems() {
844        let src = r#"
845namespace Nat
846
847theorem add_comm (n m : Nat) : n + m = m + n := sorry
848
849@[simp] theorem add_zero (n : Nat) : n + 0 = n := rfl
850
851lemma foo : True := trivial
852
853axiom choice {α : Sort u} : Nonempty α → α
854
855end Nat
856"#;
857        let decls = parse_declarations(src, "Test.lean");
858        assert!(decls.len() >= 4, "got {:?}", decls.iter().map(|d| &d.name).collect::<Vec<_>>());
859        let add_comm = decls.iter().find(|d| d.name == "add_comm").unwrap();
860        assert_eq!(add_comm.kind, DeclKind::Theorem);
861        assert!(add_comm.full_name.contains("add_comm"));
862        assert!(matches!(&add_comm.ty, TypeExpr::BinOp { op, .. } if op == "="));
863        let ax = decls.iter().find(|d| d.name == "choice").unwrap();
864        assert_eq!(ax.kind, DeclKind::Axiom);
865    }
866
867    #[test]
868    fn parse_multiline_type() {
869        let src = r#"
870theorem multi (n : Nat)
871    (m : Nat) :
872    n + m = m + n := by
873  sorry
874"#;
875        let decls = parse_declarations(src, "M.lean");
876        assert_eq!(decls.len(), 1);
877        assert!(decls[0].type_surface.contains('='));
878    }
879}