Lines
100 %
Functions
36 %
Branches
//! `nil`, `#t`/`#f`, numbers (integer / decimal / rational), `#u8(...)`
//! byte vectors, and `#"..."` base64-tagged byte literals.
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use winnow::ascii::{digit1, space0};
use winnow::combinator::{alt, cut_err, opt, preceded};
use winnow::error::{AddContext, ContextError, ErrMode, ModalResult, StrContext, StrContextValue};
use winnow::prelude::*;
use winnow::token::take_till;
use crate::ast::{Expr, Fraction};
use super::atoms::is_delimiter;
pub(super) fn parse_nil(input: &mut &str) -> ModalResult<Expr> {
alt(("nil", "NIL", "Nil"))
.value(Expr::Nil)
.parse_next(input)
}
pub(super) fn parse_hash_literal(input: &mut &str) -> ModalResult<Expr> {
alt((parse_byte_vector_literal, parse_base64_literal, parse_bool)).parse_next(input)
fn parse_bool(input: &mut &str) -> ModalResult<Expr> {
alt(("#t", "#T", "#f", "#F"))
.map(|s: &str| match s {
"#t" | "#T" => Expr::Bool(true),
_ => Expr::Nil,
})
fn parse_byte_vector_literal(input: &mut &str) -> ModalResult<Expr> {
let _ = "#u8(".parse_next(input)?;
let mut bytes: Vec<u8> = Vec::new();
loop {
let _ = space0.parse_next(input)?;
if input.starts_with(')') {
let _ = ')'.parse_next(input)?;
return Ok(Expr::Bytes(bytes));
if input.is_empty() {
return Err(ErrMode::Cut(
ContextError::new()
.add_context(input, &input.checkpoint(), StrContext::Label("byte vector"))
.add_context(
input,
&input.checkpoint(),
StrContext::Expected(StrContextValue::Description("closing paren")),
),
));
let digits: &str = cut_err(digit1)
.context(StrContext::Label("byte"))
.context(StrContext::Expected(StrContextValue::Description(
"0..255 integer",
)))
.parse_next(input)?;
let val: u8 = digits.parse().map_err(|_| {
ErrMode::Cut(ContextError::new().add_context(
StrContext::Expected(StrContextValue::Description("byte must fit in 0..255")),
))
})?;
bytes.push(val);
fn parse_base64_literal(input: &mut &str) -> ModalResult<Expr> {
let _ = "#\"".parse_next(input)?;
let payload: &str = take_till(0.., |c| c == '"').parse_next(input)?;
let _ = cut_err('"')
.context(StrContext::Label("base64 string"))
"closing quote",
let bytes = BASE64_STANDARD.decode(payload).map_err(|_| {
StrContext::Expected(StrContextValue::Description("valid base64 payload")),
Ok(Expr::Bytes(bytes))
/// A hard (`Cut`) parse error for a numeric literal that is well-formed but
/// outside the `i64`-backed `Fraction` range. `Cut` (not `Backtrack`) because
/// the token is unambiguously a number — it must not fall back to a symbol.
fn range_cut(input: &mut &str, what: &'static str) -> ErrMode<ContextError> {
StrContext::Expected(StrContextValue::Description(what)),
pub(super) fn parse_number(input: &mut &str) -> ModalResult<Expr> {
let sign = opt(alt(("-".value(-1i64), "+".value(1i64))))
.map(|s| s.unwrap_or(1))
let int_part: &str = digit1.parse_next(input)?;
let int_val: i64 = int_part
.parse()
.map_err(|_| ErrMode::Cut(ContextError::new()))?;
let frac_part = opt(preceded('.', digit1)).parse_next(input)?;
let denom_part = if frac_part.is_none() {
opt(preceded('/', digit1)).parse_next(input)?
} else {
None
};
// Optional trailing `%`: `15%` reads as `15/100`. Consumed BEFORE the
// delimiter check so the `%` never leaks out as a stray token.
let percent = opt('%').parse_next(input)?.is_some();
if input.starts_with(|c: char| !is_delimiter(c)) {
return Err(ErrMode::Backtrack(ContextError::new()));
let fraction = match (frac_part, denom_part) {
(Some(decimals), _) => {
let decimal_places = decimals.len() as u32;
let decimal_val: i64 = decimals
// Checked: `1.0000000000000000000` (10^19) or `9.99…9` overflows
// i64 — a structured parse error, not a debug panic / release wrap.
let denom = 10i64
.checked_pow(decimal_places)
.ok_or_else(|| range_cut(input, "decimal literal: too many fractional digits"))?;
let scaled = int_val
.checked_mul(denom)
.and_then(|v| v.checked_add(decimal_val))
.ok_or_else(|| range_cut(input, "decimal literal out of i64 range"))?;
Fraction::new(sign * scaled, denom)
(None, Some(denom_str)) => {
let denom: i64 = denom_str
if denom == 0 {
return Err(range_cut(input, "non-zero denominator in rational literal"));
Fraction::new(sign * int_val, denom)
(None, None) => Fraction::from_integer(sign * int_val),
let fraction = if percent {
// Divide by 100 = scale the denominator by 100. Checked, so a
// pathological literal (`1/9223372036854775807%`) is a structured parse
// error, not an `i64` overflow panic (debug) / silent wrap (release) in
// num-rational's unchecked `Div`.
let denom = fraction
.denom()
.checked_mul(100)
.ok_or_else(|| range_cut(input, "percent literal denominator within i64 range"))?;
Fraction::new(*fraction.numer(), denom)
fraction
Ok(Expr::Number(fraction))