Skip to main content

praxis_syntax/
numeric.rs

1//! The one digit-separator rule for numeric literals (§4.3).
2//!
3//! `1_000_000` is one literal, and the `_`s in it are punctuation the *value*
4//! does not contain. Two places have to agree about that: the lexer, which
5//! decides how far a numeric literal runs, and lowering, which turns its text
6//! into an `i64` or an `f64`. If the lexer refuses a separator the decoder
7//! strips, the literal is broken up before the decoder ever sees it and the
8//! strip is unreachable.
9//!
10//! Both halves live here so neither can drift: [`separator_run_len`] is what
11//! the lexer consumes, [`strip_digit_separators`] is what the decoder removes,
12//! and the rule is stated once.
13//!
14//! The rule: **a separator is one or more `_` with a digit on each side.** So
15//! `1_0`, `1_000_000` and `1__0` are literals; `_1` is an identifier (the lexer
16//! never reaches here — dispatch routes on the first byte) and `1_` is the
17//! literal `1` followed by the `_` token. Nothing here decides where digits may
18//! appear; the caller owns that, which is why a separator is legal in a fraction
19//! and an exponent too (`1_0.5_5e1_0`).
20
21use std::borrow::Cow;
22
23/// The length in bytes of the digit-separator run at `at`, or `0` if there is
24/// none.
25///
26/// Total on any input: it checks *both* sides, so a caller cannot get a nonzero
27/// answer for a `_` that is not between digits. The lexer's three digit runs
28/// each ask only after consuming at least one digit, so the left-hand check is
29/// always satisfied there — it is written out anyway rather than leaving this
30/// predicate's correctness dependent on where the caller stands.
31#[must_use]
32pub fn separator_run_len(bytes: &[u8], at: usize) -> usize {
33    if at == 0 || !matches!(bytes.get(at - 1), Some(d) if d.is_ascii_digit()) {
34        return 0;
35    }
36    let mut end = at;
37    while matches!(bytes.get(end), Some(b'_')) {
38        end += 1;
39    }
40    if end == at || !matches!(bytes.get(end), Some(d) if d.is_ascii_digit()) {
41        return 0;
42    }
43    end - at
44}
45
46/// `s` with every digit separator removed, ready for `str::parse`.
47///
48/// Borrows when there is nothing to remove, which is every literal anyone
49/// actually writes. Only the separators the lexer accepts can be present, so
50/// this is a filter and not a validator: a malformed `_` never reaches it.
51#[must_use]
52pub fn strip_digit_separators(s: &str) -> Cow<'_, str> {
53    if s.contains('_') {
54        Cow::Owned(s.chars().filter(|c| *c != '_').collect())
55    } else {
56        Cow::Borrowed(s)
57    }
58}
59
60/// The `i64` an `IntLit` token's text names, or `None` when it names a value
61/// outside `Int` (§4.3).
62///
63/// Three passes decode an integer literal — inference, which is where `Y013` is
64/// decided; the pattern builder, at a literal pattern; and lowering, which puts
65/// the value in the typed tree — and "out of range" has to mean the same thing
66/// in all three or one of them reports a literal another one accepts. The strip
67/// and the `parse` are one line each; keeping them together is what makes the
68/// range one rule instead of three copies of it.
69#[must_use]
70pub fn parse_int_literal(text: &str) -> Option<i64> {
71    strip_digit_separators(text).parse::<i64>().ok()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// The rule, at the level it is decided: a separator has digits on both
79    /// sides. Each case here is a *lexing* outcome — a nonzero length is how
80    /// much of the literal the `_`s occupy, and zero means the literal ended.
81    #[test]
82    fn a_separator_is_underscores_with_a_digit_on_each_side() {
83        // `1_0`: one `_`, digits both sides.
84        assert_eq!(separator_run_len(b"1_0", 1), 1);
85        // `1__0`: a run, still one separator.
86        assert_eq!(separator_run_len(b"1__0", 1), 2);
87        // `1_`: nothing follows, so the literal is `1` and the `_` is a token.
88        assert_eq!(separator_run_len(b"1_", 1), 0);
89        // `1_a`: an identifier follows, not a digit.
90        assert_eq!(separator_run_len(b"1_a", 1), 0);
91        // `_1`: nothing precedes. Unreachable from the lexer, answered anyway.
92        assert_eq!(separator_run_len(b"_1", 0), 0);
93        // `a_1`: a letter precedes — this is one identifier, not a literal.
94        assert_eq!(separator_run_len(b"a_1", 1), 0);
95        // Not at a `_` at all.
96        assert_eq!(separator_run_len(b"10", 1), 0);
97        assert_eq!(separator_run_len(b"", 0), 0);
98    }
99
100    /// The other half: what the decoder removes is exactly what the lexer let
101    /// through, and a literal with none is not copied.
102    #[test]
103    fn stripping_removes_every_separator_and_borrows_when_there_are_none() {
104        assert_eq!(strip_digit_separators("1_000"), "1000");
105        assert_eq!(strip_digit_separators("1_0_0"), "100");
106        assert_eq!(strip_digit_separators("3.141_592"), "3.141592");
107        assert!(matches!(strip_digit_separators("1000"), Cow::Borrowed(_)));
108        assert!(matches!(strip_digit_separators("1_000"), Cow::Owned(_)));
109    }
110
111    /// The range is one rule: separators come out first, and the boundary is
112    /// `i64`'s own. Everything past it is `None`, which is what `Y013` is.
113    #[test]
114    fn an_int_literal_decodes_through_the_range_or_not_at_all() {
115        assert_eq!(parse_int_literal("0"), Some(0));
116        assert_eq!(parse_int_literal("1_000"), Some(1000));
117        assert_eq!(parse_int_literal("9223372036854775807"), Some(i64::MAX));
118        assert_eq!(parse_int_literal("9223372036854775808"), None);
119        // The separated spelling is the same literal and the same answer, which
120        // is why the strip and the range test are one function.
121        assert_eq!(parse_int_literal("9_223_372_036_854_775_808"), None);
122        assert_eq!(parse_int_literal("99999999999999999999999"), None);
123    }
124}