1
//! `nil`, `#t`/`#f`, numbers (integer / decimal / rational), `#u8(...)`
2
//! byte vectors, and `#"..."` base64-tagged byte literals.
3

            
4
use base64::Engine;
5
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
6
use winnow::ascii::{digit1, space0};
7
use winnow::combinator::{alt, cut_err, opt, preceded};
8
use winnow::error::{AddContext, ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
9
use winnow::prelude::*;
10
use winnow::token::take_till;
11

            
12
use crate::ast::{Expr, Fraction};
13

            
14
use super::atoms::is_delimiter;
15

            
16
6059579
pub(super) fn parse_nil(input: &mut &str) -> ModalResult<Expr> {
17
6059579
    alt(("nil", "NIL", "Nil"))
18
6059579
        .value(Expr::Nil)
19
6059579
        .parse_next(input)
20
6059579
}
21

            
22
6046648
pub(super) fn parse_hash_literal(input: &mut &str) -> ModalResult<Expr> {
23
6046648
    alt((parse_byte_vector_literal, parse_base64_literal, parse_bool)).parse_next(input)
24
6046648
}
25

            
26
6046565
fn parse_bool(input: &mut &str) -> ModalResult<Expr> {
27
6046565
    alt(("#t", "#T", "#f", "#F"))
28
6046565
        .map(|s: &str| match s {
29
6604
            "#t" | "#T" => Expr::Bool(true),
30
1024
            _ => Expr::Nil,
31
6604
        })
32
6046565
        .parse_next(input)
33
6046565
}
34

            
35
6046648
fn parse_byte_vector_literal(input: &mut &str) -> ModalResult<Expr> {
36
6046648
    let _ = "#u8(".parse_next(input)?;
37
76
    let mut bytes: Vec<u8> = Vec::new();
38
    loop {
39
300
        let _ = space0.parse_next(input)?;
40
300
        if input.starts_with(')') {
41
74
            let _ = ')'.parse_next(input)?;
42
74
            return Ok(Expr::Bytes(bytes));
43
226
        }
44
226
        if input.is_empty() {
45
1
            return Err(ErrMode::Cut(
46
1
                ContextError::new()
47
1
                    .add_context(input, &input.checkpoint(), StrContext::Label("byte vector"))
48
1
                    .add_context(
49
1
                        input,
50
1
                        &input.checkpoint(),
51
1
                        StrContext::Expected(StrContextValue::Description("closing paren")),
52
1
                    ),
53
1
            ));
54
225
        }
55
225
        let digits: &str = cut_err(digit1)
56
225
            .context(StrContext::Label("byte"))
57
225
            .context(StrContext::Expected(StrContextValue::Description(
58
225
                "0..255 integer",
59
225
            )))
60
225
            .parse_next(input)?;
61
225
        let val: u8 = digits.parse().map_err(|_| {
62
1
            ErrMode::Cut(ContextError::new().add_context(
63
1
                input,
64
1
                &input.checkpoint(),
65
1
                StrContext::Expected(StrContextValue::Description("byte must fit in 0..255")),
66
1
            ))
67
1
        })?;
68
224
        bytes.push(val);
69
    }
70
6046648
}
71

            
72
6046572
fn parse_base64_literal(input: &mut &str) -> ModalResult<Expr> {
73
6046572
    let _ = "#\"".parse_next(input)?;
74
1405
    let payload: &str = take_till(0.., |c| c == '"').parse_next(input)?;
75
7
    let _ = cut_err('"')
76
7
        .context(StrContext::Label("base64 string"))
77
7
        .context(StrContext::Expected(StrContextValue::Description(
78
7
            "closing quote",
79
7
        )))
80
7
        .parse_next(input)?;
81
6
    let bytes = BASE64_STANDARD.decode(payload).map_err(|_| {
82
1
        ErrMode::Cut(ContextError::new().add_context(
83
1
            input,
84
1
            &input.checkpoint(),
85
1
            StrContext::Expected(StrContextValue::Description("valid base64 payload")),
86
1
        ))
87
1
    })?;
88
5
    Ok(Expr::Bytes(bytes))
89
6046572
}
90

            
91
/// A hard (`Cut`) parse error for a numeric literal that is well-formed but
92
/// outside the `i64`-backed `Fraction` range. `Cut` (not `Backtrack`) because
93
/// the token is unambiguously a number — it must not fall back to a symbol.
94
205
fn range_cut(input: &mut &str, what: &'static str) -> ErrMode<ContextError> {
95
205
    ErrMode::Cut(ContextError::new().add_context(
96
205
        input,
97
205
        &input.checkpoint(),
98
205
        StrContext::Expected(StrContextValue::Description(what)),
99
205
    ))
100
205
}
101

            
102
6039961
pub(super) fn parse_number(input: &mut &str) -> ModalResult<Expr> {
103
6039961
    let sign = opt(alt(("-".value(-1i64), "+".value(1i64))))
104
6039961
        .map(|s| s.unwrap_or(1))
105
6039961
        .parse_next(input)?;
106

            
107
6039961
    let int_part: &str = digit1.parse_next(input)?;
108
308944
    let int_val: i64 = int_part
109
308944
        .parse()
110
308944
        .map_err(|_| ErrMode::Cut(ContextError::new()))?;
111

            
112
308944
    let frac_part = opt(preceded('.', digit1)).parse_next(input)?;
113
308944
    let denom_part = if frac_part.is_none() {
114
305747
        opt(preceded('/', digit1)).parse_next(input)?
115
    } else {
116
3197
        None
117
    };
118

            
119
    // Optional trailing `%`: `15%` reads as `15/100`. Consumed BEFORE the
120
    // delimiter check so the `%` never leaks out as a stray token.
121
308944
    let percent = opt('%').parse_next(input)?.is_some();
122

            
123
308944
    if input.starts_with(|c: char| !is_delimiter(c)) {
124
41888
        return Err(ErrMode::Backtrack(ContextError::new()));
125
267056
    }
126

            
127
267056
    let fraction = match (frac_part, denom_part) {
128
3197
        (Some(decimals), _) => {
129
3197
            let decimal_places = decimals.len() as u32;
130
3197
            let decimal_val: i64 = decimals
131
3197
                .parse()
132
3197
                .map_err(|_| ErrMode::Cut(ContextError::new()))?;
133
            // Checked: `1.0000000000000000000` (10^19) or `9.99…9` overflows
134
            // i64 — a structured parse error, not a debug panic / release wrap.
135
3129
            let denom = 10i64
136
3197
                .checked_pow(decimal_places)
137
3197
                .ok_or_else(|| range_cut(input, "decimal literal: too many fractional digits"))?;
138
3129
            let scaled = int_val
139
3129
                .checked_mul(denom)
140
3129
                .and_then(|v| v.checked_add(decimal_val))
141
3129
                .ok_or_else(|| range_cut(input, "decimal literal out of i64 range"))?;
142
3061
            Fraction::new(sign * scaled, denom)
143
        }
144
5450
        (None, Some(denom_str)) => {
145
5450
            let denom: i64 = denom_str
146
5450
                .parse()
147
5450
                .map_err(|_| ErrMode::Cut(ContextError::new()))?;
148
5450
            if denom == 0 {
149
1
                return Err(range_cut(input, "non-zero denominator in rational literal"));
150
5449
            }
151
5449
            Fraction::new(sign * int_val, denom)
152
        }
153
258409
        (None, None) => Fraction::from_integer(sign * int_val),
154
    };
155

            
156
266919
    let fraction = if percent {
157
        // Divide by 100 = scale the denominator by 100. Checked, so a
158
        // pathological literal (`1/9223372036854775807%`) is a structured parse
159
        // error, not an `i64` overflow panic (debug) / silent wrap (release) in
160
        // num-rational's unchecked `Div`.
161
544
        let denom = fraction
162
544
            .denom()
163
544
            .checked_mul(100)
164
544
            .ok_or_else(|| range_cut(input, "percent literal denominator within i64 range"))?;
165
476
        Fraction::new(*fraction.numer(), denom)
166
    } else {
167
266375
        fraction
168
    };
169

            
170
266851
    Ok(Expr::Number(fraction))
171
6039961
}