1
//! Numbers (integer / decimal / rational), `#u8(...)` byte vectors,
2
//! `#"..."` base64-tagged byte literals, and the rejection of the Scheme
3
//! boolean literals `#t` / `#f`.
4

            
5
use std::fmt;
6

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

            
17
use crate::ast::{Expr, Fraction};
18

            
19
use super::atoms::is_delimiter;
20

            
21
8947636
pub(super) fn parse_hash_literal(input: &mut &str) -> ModalResult<Expr> {
22
8947636
    alt((
23
8947636
        parse_byte_vector_literal,
24
8947636
        parse_base64_literal,
25
8947636
        reject_scheme_boolean,
26
8947636
    ))
27
8947636
    .parse_next(input)
28
8947636
}
29

            
30
#[derive(Debug, Clone)]
31
struct SchemeBoolean {
32
    literal: &'static str,
33
    replacement: &'static str,
34
}
35

            
36
impl fmt::Display for SchemeBoolean {
37
172
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38
172
        write!(
39
172
            f,
40
            "{} is not nomiscript syntax; use {}",
41
            self.literal, self.replacement
42
        )
43
172
    }
44
}
45

            
46
impl std::error::Error for SchemeBoolean {}
47

            
48
8947372
fn reject_scheme_boolean(input: &mut &str) -> ModalResult<Expr> {
49
8947372
    let scheme = alt((
50
8947372
        alt(("#t", "#T")).value(SchemeBoolean {
51
8947372
            literal: "#t",
52
8947372
            replacement: "t",
53
8947372
        }),
54
8947372
        alt(("#f", "#F")).value(SchemeBoolean {
55
8947372
            literal: "#f",
56
8947372
            replacement: "nil",
57
8947372
        }),
58
8947372
    ))
59
8947372
    .parse_next(input)?;
60
172
    if input.starts_with(|c: char| !is_delimiter(c)) {
61
        return Err(ErrMode::Backtrack(ContextError::new()));
62
172
    }
63
172
    Err(ErrMode::Cut(ContextError::from_external_error(
64
172
        input, scheme,
65
172
    )))
66
8947372
}
67

            
68
8947636
fn parse_byte_vector_literal(input: &mut &str) -> ModalResult<Expr> {
69
8947636
    let _ = "#u8(".parse_next(input)?;
70
257
    let mut bytes: Vec<u8> = Vec::new();
71
    loop {
72
941
        let _ = space0.parse_next(input)?;
73
941
        if input.starts_with(')') {
74
255
            let _ = ')'.parse_next(input)?;
75
255
            return Ok(Expr::Bytes(bytes));
76
686
        }
77
686
        if input.is_empty() {
78
1
            return Err(ErrMode::Cut(
79
1
                ContextError::new()
80
1
                    .add_context(input, &input.checkpoint(), StrContext::Label("byte vector"))
81
1
                    .add_context(
82
1
                        input,
83
1
                        &input.checkpoint(),
84
1
                        StrContext::Expected(StrContextValue::Description("closing paren")),
85
1
                    ),
86
1
            ));
87
685
        }
88
685
        let digits: &str = cut_err(digit1)
89
685
            .context(StrContext::Label("byte"))
90
685
            .context(StrContext::Expected(StrContextValue::Description(
91
685
                "0..255 integer",
92
685
            )))
93
685
            .parse_next(input)?;
94
685
        let val: u8 = digits.parse().map_err(|_| {
95
1
            ErrMode::Cut(ContextError::new().add_context(
96
1
                input,
97
1
                &input.checkpoint(),
98
1
                StrContext::Expected(StrContextValue::Description("byte must fit in 0..255")),
99
1
            ))
100
1
        })?;
101
684
        bytes.push(val);
102
    }
103
8947636
}
104

            
105
8947379
fn parse_base64_literal(input: &mut &str) -> ModalResult<Expr> {
106
8947379
    let _ = "#\"".parse_next(input)?;
107
1405
    let payload: &str = take_till(0.., |c| c == '"').parse_next(input)?;
108
7
    let _ = cut_err('"')
109
7
        .context(StrContext::Label("base64 string"))
110
7
        .context(StrContext::Expected(StrContextValue::Description(
111
7
            "closing quote",
112
7
        )))
113
7
        .parse_next(input)?;
114
6
    let bytes = BASE64_STANDARD.decode(payload).map_err(|_| {
115
1
        ErrMode::Cut(ContextError::new().add_context(
116
1
            input,
117
1
            &input.checkpoint(),
118
1
            StrContext::Expected(StrContextValue::Description("valid base64 payload")),
119
1
        ))
120
1
    })?;
121
5
    Ok(Expr::Bytes(bytes))
122
8947379
}
123

            
124
/// A hard (`Cut`) parse error for a numeric literal that is well-formed but
125
/// outside the `i64`-backed `Fraction` range. `Cut` (not `Backtrack`) because
126
/// the token is unambiguously a number — it must not fall back to a symbol.
127
250
fn range_cut(input: &mut &str, what: &'static str) -> ErrMode<ContextError> {
128
250
    ErrMode::Cut(ContextError::new().add_context(
129
250
        input,
130
250
        &input.checkpoint(),
131
250
        StrContext::Expected(StrContextValue::Description(what)),
132
250
    ))
133
250
}
134

            
135
8947200
pub(super) fn parse_number(input: &mut &str) -> ModalResult<Expr> {
136
8947200
    let sign = opt(alt(("-".value(-1i64), "+".value(1i64))))
137
8947200
        .map(|s| s.unwrap_or(1))
138
8947200
        .parse_next(input)?;
139

            
140
8947200
    let int_part: &str = digit1.parse_next(input)?;
141
450489
    let int_val: i64 = int_part
142
450489
        .parse()
143
450489
        .map_err(|_| ErrMode::Cut(ContextError::new()))?;
144

            
145
450406
    let frac_part = opt(preceded('.', digit1)).parse_next(input)?;
146
450406
    let denom_part = if frac_part.is_none() {
147
446504
        opt(preceded('/', digit1)).parse_next(input)?
148
    } else {
149
3902
        None
150
    };
151

            
152
    // Optional trailing `%`: `15%` reads as `15/100`. Consumed BEFORE the
153
    // delimiter check so the `%` never leaks out as a stray token.
154
450406
    let percent = opt('%').parse_next(input)?.is_some();
155

            
156
450406
    if input.starts_with(|c: char| !is_delimiter(c)) {
157
51128
        return Err(ErrMode::Backtrack(ContextError::new()));
158
399278
    }
159

            
160
399278
    let fraction = match (frac_part, denom_part) {
161
3902
        (Some(decimals), _) => {
162
3902
            let decimal_places = decimals.len() as u32;
163
3902
            let decimal_val: i64 = decimals
164
3902
                .parse()
165
3902
                .map_err(|_| ErrMode::Cut(ContextError::new()))?;
166
            // Checked: `1.0000000000000000000` (10^19) or `9.99…9` overflows
167
            // i64 — a structured parse error, not a debug panic / release wrap.
168
3819
            let denom = 10i64
169
3902
                .checked_pow(decimal_places)
170
3902
                .ok_or_else(|| range_cut(input, "decimal literal: too many fractional digits"))?;
171
3819
            let scaled = int_val
172
3819
                .checked_mul(denom)
173
3819
                .and_then(|v| v.checked_add(decimal_val))
174
3819
                .ok_or_else(|| range_cut(input, "decimal literal out of i64 range"))?;
175
3736
            Fraction::new(sign * scaled, denom)
176
        }
177
8725
        (None, Some(denom_str)) => {
178
8725
            let denom: i64 = denom_str
179
8725
                .parse()
180
8725
                .map_err(|_| ErrMode::Cut(ContextError::new()))?;
181
8725
            if denom == 0 {
182
1
                return Err(range_cut(input, "non-zero denominator in rational literal"));
183
8724
            }
184
8724
            Fraction::new(sign * int_val, denom)
185
        }
186
386651
        (None, None) => Fraction::from_integer(sign * int_val),
187
    };
188

            
189
399111
    let fraction = if percent {
190
        // Divide by 100 = scale the denominator by 100. Checked, so a
191
        // pathological literal (`1/9223372036854775807%`) is a structured parse
192
        // error, not an `i64` overflow panic (debug) / silent wrap (release) in
193
        // num-rational's unchecked `Div`.
194
664
        let denom = fraction
195
664
            .denom()
196
664
            .checked_mul(100)
197
664
            .ok_or_else(|| range_cut(input, "percent literal denominator within i64 range"))?;
198
581
        Fraction::new(*fraction.numer(), denom)
199
    } else {
200
398447
        fraction
201
    };
202

            
203
399028
    Ok(Expr::Number(fraction))
204
8947200
}