cljrs_reader/chars.rs
1//! Character classification for the reader.
2//!
3//! The bottom stratum of the crate: pure `char` predicates with no lexer or
4//! parser state, so both the tokenizer and the pure rewrites can use them.
5
6/// Returns `true` if `ch` is a valid constituent character for a symbol or
7/// keyword. Defined *negatively*: everything that isn't a delimiter, whitespace,
8/// or special syntax character is a symbol constituent.
9///
10/// `#` is included here: it is a *non-terminating* macro character in the
11/// Clojure reader, meaning it doesn't end a symbol token that's already in
12/// progress (only a leading `#` triggers `#`-dispatch — see
13/// [`is_symbol_start`]). This is what makes auto-gensym symbols like `x#`
14/// tokenize as a single symbol rather than `x` followed by a stray `#`.
15///
16/// `:` is included here too — it is also non-terminating, so a keyword like
17/// `:xlink:href` reads as one token with the literal name `xlink:href`
18/// rather than splitting into two keywords at the embedded colon. Only a
19/// *leading* `:`/`::` is special (it triggers keyword dispatch — see
20/// [`is_symbol_start`]).
21pub(crate) fn is_symbol_char(ch: char) -> bool {
22 !matches!(
23 ch,
24 ' ' | '\t'
25 | '\n'
26 | '\r'
27 | ','
28 | '('
29 | ')'
30 | '['
31 | ']'
32 | '{'
33 | '}'
34 | '"'
35 | ';'
36 | '`'
37 | '~'
38 | '^'
39 | '@'
40 | '\\'
41 )
42}
43
44/// Returns `true` if `ch` can *start* a symbol (not a digit, not `#` since a
45/// leading `#` always triggers `#`-dispatch, not `:` since a leading `:`
46/// always triggers keyword dispatch, not `+`/`-` when the following char is
47/// a digit — but the caller handles the `+`/`-` case).
48pub(crate) fn is_symbol_start(ch: char) -> bool {
49 is_symbol_char(ch) && !ch.is_ascii_digit() && ch != '#' && ch != ':'
50}