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
346086
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
346086
    c.is_whitespace() || "()\"'`,;%".contains(c)
20
346086
}
21

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

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

            
39
3832490
pub(super) fn parse_keyword(input: &mut &str) -> ModalResult<Expr> {
40
3832490
    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
146696
    if input.starts_with(':') {
45
72
        return Err(cut(input, "keyword", "no leading '::'"));
46
146624
    }
47
146624
    let first: char = none_of(|c: char| !token_char(c))
48
146624
        .parse_next(input)
49
146624
        .map_err(|_: ErrMode<ContextError>| cut(input, "keyword", "non-empty keyword name"))?;
50

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

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

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

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

            
65
3685723
    let mut name = String::with_capacity(1 + rest.len());
66
3685723
    name.push(first);
67
3685723
    name.push_str(rest);
68
3685723
    name.make_ascii_uppercase();
69
3685723
    if name == "NIL" {
70
142
        return Ok(Expr::Nil);
71
3685581
    }
72
3685581
    if name == "T" {
73
8806
        return Ok(Expr::Bool(true));
74
3676775
    }
75
3676775
    canonicalize_symbol(input, name)
76
3685794
}
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
3676775
fn canonicalize_symbol(input: &&str, name: String) -> Result<Expr, ErrMode<ContextError>> {
84
3676775
    if !name.contains(':') {
85
3319773
        return Ok(Expr::Symbol(name));
86
357002
    }
87
    // Split into colon-separated segments; collapse a single `::` (one empty
88
    // middle segment between two non-empty sides) to a single separator.
89
357002
    let segments: Vec<&str> = name.split(':').collect();
90
357002
    let qualified = match segments.as_slice() {
91
        // NS:NAME
92
356570
        [ns, base] if !ns.is_empty() && !base.is_empty() => {
93
356498
            crate::ast::canonical_symbol(Some(ns), base)
94
        }
95
        // NS::NAME — fold the double colon to the canonical single-colon key.
96
288
        [ns, mid, base] if !ns.is_empty() && mid.is_empty() && !base.is_empty() => {
97
144
            crate::ast::canonical_symbol(Some(ns), base)
98
        }
99
        _ => {
100
360
            return Err(cut(
101
360
                input,
102
360
                "qualified symbol",
103
360
                "one ':' or '::' separator with non-empty namespace and name",
104
360
            ));
105
        }
106
    };
107
356642
    Ok(Expr::Symbol(qualified))
108
3676775
}