1
//! Bare-symbol and `:keyword` atoms, plus the shared `is_delimiter`
2
//! predicate every number / atom parser consults to know where a
3
//! token ends.
4
//!
5
//! Colon handling (ADR-0029): a *leading* `:` is a keyword (`parse_keyword`,
6
//! tried first in `reader::parse_expr`); an *infix* `:` (or `::`) qualifies a
7
//! symbol with a namespace (`parse_symbol`). The two never collide — they are
8
//! distinguished by whether the first char is a colon.
9

            
10
use winnow::error::{AddContext, ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
11
use winnow::prelude::*;
12
use winnow::token::{none_of, take_while};
13

            
14
use crate::ast::Expr;
15

            
16
305664
pub(super) fn is_delimiter(c: char) -> bool {
17
    // `%` terminates a numeric token (`15%` reader sugar); `parse_number`
18
    // consumes the trailing `%` itself, so a number always ends at one.
19
305664
    c.is_whitespace() || "()\"'`,;%".contains(c)
20
305664
}
21

            
22
17282647
fn token_char(c: char) -> bool {
23
17282647
    !c.is_whitespace() && !"()\"'`,".contains(c)
24
17282647
}
25

            
26
/// Hard parse error (no backtrack) anchored at the current input position.
27
483
fn cut(input: &&str, label: &'static str, expected: &'static str) -> ErrMode<ContextError> {
28
483
    ErrMode::Cut(
29
483
        ContextError::new()
30
483
            .add_context(input, &input.checkpoint(), StrContext::Label(label))
31
483
            .add_context(
32
483
                input,
33
483
                &input.checkpoint(),
34
483
                StrContext::Expected(StrContextValue::Description(expected)),
35
483
            ),
36
483
    )
37
483
}
38

            
39
3482104
pub(super) fn parse_keyword(input: &mut &str) -> ModalResult<Expr> {
40
3482104
    let _ = ':'.parse_next(input)?;
41
    // A second leading colon (`::foo`) or a bare `:` is not a keyword — and
42
    // must NOT fall through to `parse_symbol` (which would reclaim the token).
43
    // Cut so the alternation stops here with a structured error.
44
40606
    if input.starts_with(':') {
45
69
        return Err(cut(input, "keyword", "no leading '::'"));
46
40537
    }
47
40537
    let first: char = none_of(|c: char| !token_char(c))
48
40537
        .parse_next(input)
49
40537
        .map_err(|_: ErrMode<ContextError>| cut(input, "keyword", "non-empty keyword name"))?;
50

            
51
40468
    let rest: &str = take_while(0.., token_char).parse_next(input)?;
52

            
53
40468
    let mut name = String::with_capacity(1 + rest.len());
54
40468
    name.push(first);
55
40468
    name.push_str(rest);
56
40468
    name.make_ascii_uppercase();
57
40468
    Ok(Expr::Keyword(name))
58
3482104
}
59

            
60
3441498
pub(super) fn parse_symbol(input: &mut &str) -> ModalResult<Expr> {
61
3441498
    let first: char = none_of(|c: char| !token_char(c)).parse_next(input)?;
62

            
63
3441430
    let rest: &str = take_while(0.., token_char).parse_next(input)?;
64

            
65
3441430
    let mut name = String::with_capacity(1 + rest.len());
66
3441430
    name.push(first);
67
3441430
    name.push_str(rest);
68
3441430
    name.make_ascii_uppercase();
69
3441430
    if name == "NIL" {
70
136
        return Ok(Expr::Nil);
71
3441294
    }
72
3441294
    if name == "T" {
73
8230
        return Ok(Expr::Bool(true));
74
3433064
    }
75
3433064
    canonicalize_symbol(input, name)
76
3441498
}
77

            
78
/// Validates and canonicalizes infix-colon namespace qualification (ADR-0029).
79
/// `NS:NAME` and `NS::NAME` both canonicalize to `NS:NAME` (the `::` separator
80
/// is accepted as input syntax but discarded — reserved for future visibility
81
/// semantics). Trailing colon, multiple separators, or empty sides are hard
82
/// errors. A token with no colon passes through unchanged.
83
3433064
fn canonicalize_symbol(input: &&str, name: String) -> Result<Expr, ErrMode<ContextError>> {
84
3433064
    if !name.contains(':') {
85
3103025
        return Ok(Expr::Symbol(name));
86
330039
    }
87
    // Split into colon-separated segments; collapse a single `::` (one empty
88
    // middle segment between two non-empty sides) to a single separator.
89
330039
    let segments: Vec<&str> = name.split(':').collect();
90
330039
    let qualified = match segments.as_slice() {
91
        // NS:NAME
92
329625
        [ns, base] if !ns.is_empty() && !base.is_empty() => {
93
329556
            crate::ast::canonical_symbol(Some(ns), base)
94
        }
95
        // NS::NAME — fold the double colon to the canonical single-colon key.
96
276
        [ns, mid, base] if !ns.is_empty() && mid.is_empty() && !base.is_empty() => {
97
138
            crate::ast::canonical_symbol(Some(ns), base)
98
        }
99
        _ => {
100
345
            return Err(cut(
101
345
                input,
102
345
                "qualified symbol",
103
345
                "one ':' or '::' separator with non-empty namespace and name",
104
345
            ));
105
        }
106
    };
107
329694
    Ok(Expr::Symbol(qualified))
108
3433064
}