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

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

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

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

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

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

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

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

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