Lines
99.28 %
Functions
48.33 %
Branches
100 %
//! Numbers (integer / decimal / rational), `#u8(...)` byte vectors,
//! `#"..."` base64-tagged byte literals, and the rejection of the Scheme
//! boolean literals `#t` / `#f`.
use std::fmt;
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, FromExternalError, 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_hash_literal(input: &mut &str) -> ModalResult<Expr> {
alt((
parse_byte_vector_literal,
parse_base64_literal,
reject_scheme_boolean,
))
.parse_next(input)
}
#[derive(Debug, Clone)]
struct SchemeBoolean {
literal: &'static str,
replacement: &'static str,
impl fmt::Display for SchemeBoolean {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} is not nomiscript syntax; use {}",
self.literal, self.replacement
)
impl std::error::Error for SchemeBoolean {}
fn reject_scheme_boolean(input: &mut &str) -> ModalResult<Expr> {
let scheme = alt((
alt(("#t", "#T")).value(SchemeBoolean {
literal: "#t",
replacement: "t",
}),
alt(("#f", "#F")).value(SchemeBoolean {
literal: "#f",
replacement: "nil",
.parse_next(input)?;
if input.starts_with(|c: char| !is_delimiter(c)) {
return Err(ErrMode::Backtrack(ContextError::new()));
Err(ErrMode::Cut(ContextError::from_external_error(
input, scheme,
)))
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",
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();
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))