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}