mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
* build(rust): upgrade reqwest to 0.13 and move MCP out of litellm-http The workspace moves to reqwest 0.13, so litellm-http no longer carries a second reqwest just for rmcp. The mcp feature, its pool and its proxy conversion go away, and the gateway hands rmcp a pooled NoRedirect client that already carries the TLS, proxy and timeout settings The Client test helpers collapse into for_test, a no-redirect client without TLS or proxy settings, and wrap_for_test for the few tests that need a hand-built reqwest client Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(http): honor transport settings for Rust WebSockets The Responses WebSocket connected through tokio-tungstenite with its own native-roots TLS, so it ignored the configured CA bundle, verify flag, client certificate, proxies and timeouts. The handshake now goes through a pooled reqwest client (ClientVariant::WebSocket, HTTP/1.1, no redirects) and upgrades the response, so it shares the settings every other outbound request uses The upgrade response is checked for the Sec-WebSocket-Accept key, the Upgrade and Connection headers and a subprotocol that was offered Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(http): build outbound requests as reqwest::Request OutboundRequest was a hand-rolled copy of reqwest::Request. Routes now build a reqwest::Request directly and RequestSigner reads the body bytes back out of it, so the signer still covers exactly what is sent. SigV4 signing moves onto HeaderMap and takes the method, which drops the BTreeMap plumbing and the header casing table Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(http): make litellm_http::Error the one opaque error transport::Error and media::Error fold into litellm_http::Error, an Arc<Repr> newtype whose private variants keep the typed reqwest, io, rustls and tungstenite causes and own every message. Callers can no longer build an error with a message of their own. They ask through dispatched, is_request, status, upstream_response and tls_source One From<reqwest::Error> now classifies connect, network and timeout and strips the URL. The WebSocket handshake reports each failed check as its own variant and owns its connect timeout. OCR drops the media variants it mirrored, and inference::Error maps a header the signer computes to Unsupported for every route, not only chat The error rules move out of AGENTS.md into the rust-error skill, which also records the decisions behind this design Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(rust): split rust-error skill into thin SKILL.md with references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): render causes once and ask predicates at the boundaries The four WebSocket handshake variants collapse into Handshake(HandshakeFault), and media download failures no longer answer dispatched() since they happen before the provider call ErrorDetail in llms and auth-types, the inference error and the OCR error now store their cause in a field named cause with no #[source], so to_string() carries it exactly once and source() is None. Tests that walked the chain now destructure the variant and downcast the cause The gateway asks is_unsupported() and is_missing_credential() on the inference and OCR errors instead of matching through nested enums, and litellm_auth::Error answers is_missing_credential() for both The rust-error skill gains axum and pyo3 references, boundaries.md describes the real predicate order and the known divergences, the testing example asserts dispatched() correctly, and shape, classification, conversions, messages-and-causes and recipes agree with the code again * docs(rust): focus the rust-error gateway page on the error flow and add a rust-gateway skill The former axum page toured gateway-inference: its status match, the envelope table, an extractor, the unsupported route and the Sse closure. Those details belong to the gateway crates and go stale with them. The page is now gateway.md and follows an error from the provider failure through each From into IntoResponse, where it is classified once and rendered by surface and framing. The gateway-structure material moves to a new rust-gateway skill: envelopes per API surface, extractors and path registration, SSE relay, and the router test harness. * test(inference): pin provider URL redaction on every route's transport error Chat, Messages, Responses and transcription each get a route-level test that fails if a reqwest error reaches the caller with its URL. The Messages stream decoder also takes body errors through litellm_http::Error like every other reqwest error Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(rust-error): adopt std source chains and add litellm-error Report Messages describe their own layer and the cause is the source, boundaries render the chain once with litellm_error::Report. Corrects skill claims the audit found stale against the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): keep causes as sources across http, auth, secrets, cache and utility crates Messages describe their own layer and every cause is a typed source, predicates match exhaustively, and boundaries render with litellm_error::Report. Media downloads no longer count as dispatched, Error::upstream bounds the provider body itself, and a forwarded header SigV4 cannot sign is the caller's fault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): keep causes as sources in llms and the gateway crates ErrorDetail loses its string variant, OCR provider variants become data, host faults stop counting as the caller's, a tunnelled transport error classifies the same on every stream path, and each gateway crate has one Error with one status() and extractors that reject in its envelope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): nest llms errors in inference and ask predicates in route tests litellm_inference::Error nests litellm_llms::Error instead of mirroring its variants, host driver faults get their own variant, every predicate matches exhaustively, and route tests ask predicates instead of matching transport variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): map bridge and gateway-inference errors once and render them with Report RustUpstreamError has one constructor that always sets headers, argument checks raise typed errors mapped once, OCR asks upstream_response(), the Responses stream error event comes from the error type, gateway-inference loses its string variants, and auth faults classify through one litellm_auth predicate on every route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(rust-error): carry OCR provider headers as a HeaderMap and keep outages off the caller The OCR provider error and get_error_class take reqwest's HeaderMap, the bridge turns it into Python's (name, value) pairs once. Credential acquisition and provider SDK failures are no longer the caller's fault, the UI hides auth and backend causes, and chat keeps a provider answer as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(rust-error): correct skill claims against the aligned code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): fix the remaining sweep findings in gateway, llms, auth and cache crates Auth rejections hide their backend cause on every inference renderer, path parameters reject in the surface envelope, Textract's unsupported document and Azure DI's unparseable poll URL get typed variants, the credential chain exposes its last failure as the source, gateway-mcp asks predicates over rmcp errors, and the S3 and Azure blob transports classify through litellm_http::Error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(rust-error): fix the remaining sweep findings on the Python side host-python keeps a native error's chain and returns blocking-pool cancellation instead of panicking, Vault transport failures convert through litellm_http::Error so no URL reaches Python, the bridge asks predicates instead of matching three levels into Vault errors, and the last string-built PyErrs become typed errors mapped once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(deps): refresh compatible dependencies flagged by OSV * refactor(rust): remove redundant client construction comment * test(rust): cover configured MCP proxy and CA bundle * fix(http): preserve native WebSocket trust and optional protocols * fix(http): allow plain WebSockets without native roots * fix(http): normalize provider URLs before building outbound requests * fix(rust): preserve OCR download status and reduce error size * fix(rust): preserve upstream bodies and bound diagnostics only * test(rust): preserve bridge serialization boundary in body regression --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
745 lines
25 KiB
Rust
745 lines
25 KiB
Rust
//! `ast.literal_eval(text)`, as a single-pass recursive-descent parser.
|
|
//!
|
|
//! Tokens follow CPython's tokenizer (string prefixes, escapes, implicit concatenation,
|
|
//! numeric underscores and radixes, comments, line continuations). Expressions follow
|
|
//! `ast.literal_eval`'s evaluator:
|
|
//!
|
|
//! - one unary `+`/`-`, applied only to a numeric constant (`-(1)` is fine, `--1` is not)
|
|
//! - `a + b` / `a - b` only as a signed real plus or minus a complex constant, with 3.14's
|
|
//! mixed-mode rules (`1 - 0j` is `(1-0j)`)
|
|
//! - parentheses group without making a tuple; `set()` is the only call
|
|
//! - dict keys and set members are deduplicated with Python equality (`1 == 1.0 == True`)
|
|
//!
|
|
//! Not supported, each pinned by a fixture: `\N{NAME}` escapes, `...`, and escapes that
|
|
//! produce lone surrogates.
|
|
|
|
use std::collections::{HashMap, hash_map::Entry};
|
|
|
|
use num_bigint::BigInt;
|
|
use num_traits::{FromPrimitive, ToPrimitive};
|
|
|
|
use crate::{Error, MAX_DEPTH, Value};
|
|
|
|
pub fn literal_eval(text: &str) -> Result<Value, Error> {
|
|
let mut parser = Parser {
|
|
bytes: text.trim_start_matches([' ', '\t']).as_bytes(),
|
|
offset: text.len() - text.trim_start_matches([' ', '\t']).len(),
|
|
pos: 0,
|
|
depth: 0,
|
|
brackets: 0,
|
|
};
|
|
parser.skip_leading_lines()?;
|
|
let value = parser.top_level()?.value;
|
|
parser.skip_trivia(true);
|
|
if parser.pos != parser.bytes.len() {
|
|
return Err(parser.error());
|
|
}
|
|
Ok(value)
|
|
}
|
|
|
|
/// How a parsed term may take part in `+`/`-`, per `ast.literal_eval`'s `_convert_num`
|
|
/// (`Constant`) and `_convert_signed_num` (`Signed`). `Other` is any other node.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Kind {
|
|
Constant,
|
|
Signed,
|
|
Other,
|
|
}
|
|
|
|
struct Term {
|
|
value: Value,
|
|
kind: Kind,
|
|
}
|
|
|
|
impl Term {
|
|
fn other(value: Value) -> Self {
|
|
Self {
|
|
value,
|
|
kind: Kind::Other,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parser<'a> {
|
|
bytes: &'a [u8],
|
|
/// Bytes stripped before `bytes` starts, so errors report offsets into the input.
|
|
offset: usize,
|
|
pos: usize,
|
|
depth: usize,
|
|
/// Open brackets: newlines are insignificant only inside them.
|
|
brackets: usize,
|
|
}
|
|
|
|
impl Parser<'_> {
|
|
fn error(&self) -> Error {
|
|
Error::InvalidLiteral(self.offset + self.pos)
|
|
}
|
|
|
|
fn peek(&self) -> Option<u8> {
|
|
self.bytes.get(self.pos).copied()
|
|
}
|
|
|
|
fn peek_at(&self, ahead: usize) -> Option<u8> {
|
|
self.bytes.get(self.pos + ahead).copied()
|
|
}
|
|
|
|
fn expect(&mut self, byte: u8) -> Result<(), Error> {
|
|
if self.peek() != Some(byte) {
|
|
return Err(self.error());
|
|
}
|
|
self.pos += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn enter(&mut self) -> Result<(), Error> {
|
|
self.depth += 1;
|
|
if self.depth > MAX_DEPTH {
|
|
return Err(Error::TooDeep);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Whitespace, comments, and backslash continuations; newlines too when `newlines`.
|
|
fn skip_trivia(&mut self, newlines: bool) {
|
|
while let Some(byte) = self.peek() {
|
|
match byte {
|
|
b' ' | b'\t' | b'\x0c' => self.pos += 1,
|
|
b'#' => {
|
|
while !matches!(self.peek(), None | Some(b'\n' | b'\r')) {
|
|
self.pos += 1;
|
|
}
|
|
}
|
|
// A continuation joins two lines; one that ends the input is an EOF error.
|
|
b'\\' if matches!(self.peek_at(1), Some(b'\n' | b'\r')) => {
|
|
let len = match (self.peek_at(1), self.peek_at(2)) {
|
|
(Some(b'\r'), Some(b'\n')) => 3,
|
|
_ => 2,
|
|
};
|
|
if self.pos + len >= self.bytes.len() {
|
|
break;
|
|
}
|
|
self.pos += len;
|
|
}
|
|
b'\n' | b'\r' if newlines => self.pos += 1,
|
|
_ => break,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Blank and comment-only lines may precede the expression, whose own line must not be
|
|
/// indented (CPython raises `IndentationError`).
|
|
fn skip_leading_lines(&mut self) -> Result<(), Error> {
|
|
loop {
|
|
let line_start = self.pos;
|
|
self.skip_trivia(false);
|
|
match self.peek() {
|
|
Some(b'\n' | b'\r') => self.pos += 1,
|
|
Some(_) if line_start > 0 && self.pos > line_start => {
|
|
self.pos = line_start;
|
|
return Err(self.error());
|
|
}
|
|
_ => return Ok(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn at_logical_line_end(&self) -> bool {
|
|
matches!(self.peek(), None | Some(b'\n' | b'\r'))
|
|
}
|
|
|
|
/// The `eval` input: an expression, or a tuple without parentheses.
|
|
fn top_level(&mut self) -> Result<Term, Error> {
|
|
let first = self.expression()?;
|
|
self.skip_trivia(false);
|
|
if self.peek() != Some(b',') {
|
|
return Ok(first);
|
|
}
|
|
let mut values = vec![first.value];
|
|
while self.peek() == Some(b',') {
|
|
self.pos += 1;
|
|
self.skip_trivia(false);
|
|
if self.at_logical_line_end() {
|
|
break;
|
|
}
|
|
values.push(self.expression()?.value);
|
|
self.skip_trivia(false);
|
|
}
|
|
Ok(Term::other(Value::Tuple(values)))
|
|
}
|
|
|
|
/// A sum of unary terms, checked as `ast.literal_eval` checks `BinOp`.
|
|
fn expression(&mut self) -> Result<Term, Error> {
|
|
let mut left = self.unary()?;
|
|
loop {
|
|
self.skip_trivia(self.brackets > 0);
|
|
let subtract = match self.peek() {
|
|
Some(b'+') => false,
|
|
Some(b'-') => true,
|
|
_ => return Ok(left),
|
|
};
|
|
let at = self.pos;
|
|
self.pos += 1;
|
|
let right = self.unary()?;
|
|
left = complex_sum(left, subtract, right)
|
|
.ok_or(Error::InvalidLiteral(self.offset + at))?;
|
|
}
|
|
}
|
|
|
|
fn unary(&mut self) -> Result<Term, Error> {
|
|
self.skip_trivia(self.brackets > 0);
|
|
let negative = match self.peek() {
|
|
Some(b'+') => false,
|
|
Some(b'-') => true,
|
|
_ => return self.primary(),
|
|
};
|
|
let at = self.pos;
|
|
self.pos += 1;
|
|
self.enter()?;
|
|
let operand = self.unary()?;
|
|
self.depth -= 1;
|
|
if operand.kind != Kind::Constant {
|
|
return Err(Error::InvalidLiteral(self.offset + at));
|
|
}
|
|
let value = match (operand.value, negative) {
|
|
(value @ (Value::Int(_) | Value::Float(_) | Value::Complex { .. }), false) => value,
|
|
(Value::Int(value), true) => Value::Int(-value),
|
|
(Value::Float(value), true) => Value::Float(-value),
|
|
(Value::Complex { re, im }, true) => Value::Complex { re: -re, im: -im },
|
|
(
|
|
Value::None
|
|
| Value::Bool(_)
|
|
| Value::Str(_)
|
|
| Value::Bytes(_)
|
|
| Value::List(_)
|
|
| Value::Tuple(_)
|
|
| Value::Dict(_)
|
|
| Value::Set(_),
|
|
_,
|
|
) => return Err(Error::InvalidLiteral(self.offset + at)),
|
|
};
|
|
Ok(Term {
|
|
value,
|
|
kind: Kind::Signed,
|
|
})
|
|
}
|
|
|
|
fn primary(&mut self) -> Result<Term, Error> {
|
|
match self.peek() {
|
|
Some(b'(') => self.parenthesized(),
|
|
Some(b'[') => self.list(),
|
|
Some(b'{') => self.braced(),
|
|
Some(b'0'..=b'9') => self.number(),
|
|
Some(b'.') if matches!(self.peek_at(1), Some(b'0'..=b'9')) => self.number(),
|
|
Some(b'\'' | b'"') => self.strings(),
|
|
Some(byte) if byte.is_ascii_alphabetic() || byte == b'_' => {
|
|
if self.string_prefix_len().is_some() {
|
|
return self.strings();
|
|
}
|
|
self.name()
|
|
}
|
|
_ => Err(self.error()),
|
|
}
|
|
}
|
|
|
|
fn open(&mut self) -> Result<(), Error> {
|
|
self.enter()?;
|
|
self.brackets += 1;
|
|
self.pos += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn close(&mut self, byte: u8) -> Result<(), Error> {
|
|
self.skip_trivia(true);
|
|
self.expect(byte)?;
|
|
self.brackets -= 1;
|
|
self.depth -= 1;
|
|
Ok(())
|
|
}
|
|
|
|
/// Comma-separated expressions up to `close`, with an optional trailing comma.
|
|
fn elements(&mut self, close: u8) -> Result<Vec<Value>, Error> {
|
|
let mut values = Vec::new();
|
|
loop {
|
|
self.skip_trivia(true);
|
|
if self.peek() == Some(close) {
|
|
return Ok(values);
|
|
}
|
|
values.push(self.expression()?.value);
|
|
self.skip_trivia(true);
|
|
if self.peek() != Some(b',') {
|
|
return Ok(values);
|
|
}
|
|
self.pos += 1;
|
|
}
|
|
}
|
|
|
|
fn parenthesized(&mut self) -> Result<Term, Error> {
|
|
self.open()?;
|
|
self.skip_trivia(true);
|
|
if self.peek() == Some(b')') {
|
|
self.close(b')')?;
|
|
return Ok(Term::other(Value::Tuple(Vec::new())));
|
|
}
|
|
let first = self.expression()?;
|
|
self.skip_trivia(true);
|
|
if self.peek() != Some(b',') {
|
|
self.close(b')')?;
|
|
return Ok(first);
|
|
}
|
|
self.pos += 1;
|
|
let mut values = vec![first.value];
|
|
values.extend(self.elements(b')')?);
|
|
self.close(b')')?;
|
|
Ok(Term::other(Value::Tuple(values)))
|
|
}
|
|
|
|
fn list(&mut self) -> Result<Term, Error> {
|
|
self.open()?;
|
|
let values = self.elements(b']')?;
|
|
self.close(b']')?;
|
|
Ok(Term::other(Value::List(values)))
|
|
}
|
|
|
|
fn braced(&mut self) -> Result<Term, Error> {
|
|
self.open()?;
|
|
self.skip_trivia(true);
|
|
if self.peek() == Some(b'}') {
|
|
self.close(b'}')?;
|
|
return Ok(Term::other(Value::Dict(Vec::new())));
|
|
}
|
|
let first = self.expression()?.value;
|
|
self.skip_trivia(true);
|
|
if self.peek() != Some(b':') {
|
|
let mut members = UniqueValues::default();
|
|
members.insert(first, None)?;
|
|
if self.peek() == Some(b',') {
|
|
self.pos += 1;
|
|
for member in self.elements(b'}')? {
|
|
members.insert(member, None)?;
|
|
}
|
|
}
|
|
self.close(b'}')?;
|
|
return Ok(Term::other(Value::Set(members.keys)));
|
|
}
|
|
let mut entries = UniqueValues::default();
|
|
let mut key = first;
|
|
loop {
|
|
self.expect(b':')?;
|
|
let value = self.expression()?.value;
|
|
entries.insert(key, Some(value))?;
|
|
self.skip_trivia(true);
|
|
if self.peek() != Some(b',') {
|
|
break;
|
|
}
|
|
self.pos += 1;
|
|
self.skip_trivia(true);
|
|
if self.peek() == Some(b'}') {
|
|
break;
|
|
}
|
|
key = self.expression()?.value;
|
|
self.skip_trivia(true);
|
|
}
|
|
self.close(b'}')?;
|
|
Ok(Term::other(Value::Dict(entries.into_entries())))
|
|
}
|
|
|
|
fn identifier(&mut self) -> &[u8] {
|
|
let start = self.pos;
|
|
while matches!(self.peek(), Some(byte) if byte.is_ascii_alphanumeric() || byte == b'_') {
|
|
self.pos += 1;
|
|
}
|
|
&self.bytes[start..self.pos]
|
|
}
|
|
|
|
fn name(&mut self) -> Result<Term, Error> {
|
|
let at = self.pos;
|
|
let value = match self.identifier() {
|
|
b"True" => Value::Bool(true),
|
|
b"False" => Value::Bool(false),
|
|
b"None" => Value::None,
|
|
b"set" => {
|
|
self.skip_trivia(self.brackets > 0);
|
|
self.expect(b'(')?;
|
|
self.skip_trivia(true);
|
|
self.expect(b')')?;
|
|
return Ok(Term::other(Value::Set(Vec::new())));
|
|
}
|
|
_ => return Err(Error::InvalidLiteral(self.offset + at)),
|
|
};
|
|
Ok(Term {
|
|
value,
|
|
kind: Kind::Constant,
|
|
})
|
|
}
|
|
|
|
/// Digits with single underscores between them, as CPython's `digitpart`.
|
|
fn digits(&mut self, radix: u32, out: &mut String) -> Result<(), Error> {
|
|
let start = out.len();
|
|
loop {
|
|
match self.peek() {
|
|
Some(byte) if (byte as char).is_digit(radix) => {
|
|
out.push(byte as char);
|
|
self.pos += 1;
|
|
}
|
|
Some(b'_')
|
|
if out.len() > start
|
|
&& matches!(self.peek_at(1), Some(next) if (next as char).is_digit(radix)) =>
|
|
{
|
|
self.pos += 1;
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
if out.len() == start {
|
|
return Err(self.error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn number(&mut self) -> Result<Term, Error> {
|
|
let start = self.pos;
|
|
let radix = match (
|
|
self.peek(),
|
|
self.peek_at(1).map(|byte| byte.to_ascii_lowercase()),
|
|
) {
|
|
(Some(b'0'), Some(b'x')) => Some(16),
|
|
(Some(b'0'), Some(b'o')) => Some(8),
|
|
(Some(b'0'), Some(b'b')) => Some(2),
|
|
_ => None,
|
|
};
|
|
let mut text = String::new();
|
|
let value = if let Some(radix) = radix {
|
|
self.pos += 2;
|
|
if self.peek() == Some(b'_') {
|
|
self.pos += 1;
|
|
}
|
|
self.digits(radix, &mut text)?;
|
|
Value::Int(BigInt::parse_bytes(text.as_bytes(), radix).ok_or(self.error())?)
|
|
} else {
|
|
let mut is_float = false;
|
|
if self.peek() != Some(b'.') {
|
|
self.digits(10, &mut text)?;
|
|
}
|
|
let integer_digits = text.clone();
|
|
if self.peek() == Some(b'.') {
|
|
is_float = true;
|
|
self.pos += 1;
|
|
text.push('.');
|
|
if matches!(self.peek(), Some(b'0'..=b'9')) {
|
|
self.digits(10, &mut text)?;
|
|
}
|
|
}
|
|
if matches!(self.peek(), Some(b'e' | b'E')) {
|
|
is_float = true;
|
|
self.pos += 1;
|
|
text.push('e');
|
|
if let Some(sign @ (b'+' | b'-')) = self.peek() {
|
|
text.push(sign as char);
|
|
self.pos += 1;
|
|
}
|
|
self.digits(10, &mut text)?;
|
|
}
|
|
if matches!(self.peek(), Some(b'j' | b'J')) {
|
|
self.pos += 1;
|
|
let im = text.parse::<f64>().map_err(|_| self.error())?;
|
|
Value::Complex { re: 0.0, im }
|
|
} else if is_float {
|
|
Value::Float(text.parse::<f64>().map_err(|_| self.error())?)
|
|
} else {
|
|
if integer_digits.len() > 1
|
|
&& integer_digits.starts_with('0')
|
|
&& integer_digits.bytes().any(|digit| digit != b'0')
|
|
{
|
|
return Err(Error::InvalidLiteral(self.offset + start));
|
|
}
|
|
Value::Int(BigInt::parse_bytes(integer_digits.as_bytes(), 10).ok_or(self.error())?)
|
|
}
|
|
};
|
|
if matches!(self.peek(), Some(byte) if byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'.')
|
|
{
|
|
return Err(self.error());
|
|
}
|
|
Ok(Term {
|
|
value,
|
|
kind: Kind::Constant,
|
|
})
|
|
}
|
|
|
|
/// The length of a valid string prefix (`r`, `u`, `b`, `br`, `rb`, any case) directly
|
|
/// followed by a quote.
|
|
fn string_prefix_len(&self) -> Option<usize> {
|
|
let mut len = 0;
|
|
while matches!(self.peek_at(len), Some(byte) if byte.is_ascii_alphabetic()) && len < 3 {
|
|
len += 1;
|
|
}
|
|
if !matches!(self.peek_at(len), Some(b'\'' | b'"')) {
|
|
return None;
|
|
}
|
|
let prefix: Vec<u8> = self.bytes[self.pos..self.pos + len]
|
|
.iter()
|
|
.map(u8::to_ascii_lowercase)
|
|
.collect();
|
|
matches!(prefix.as_slice(), b"" | b"r" | b"u" | b"b" | b"br" | b"rb").then_some(len)
|
|
}
|
|
|
|
/// Adjacent string literals concatenate; `str` and `bytes` cannot mix.
|
|
fn strings(&mut self) -> Result<Term, Error> {
|
|
let mut text: Option<String> = None;
|
|
let mut bytes: Option<Vec<u8>> = None;
|
|
loop {
|
|
let at = self.pos;
|
|
let Some(prefix_len) = self.string_prefix_len() else {
|
|
break;
|
|
};
|
|
let prefix = &self.bytes[self.pos..self.pos + prefix_len];
|
|
let raw = prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'r'));
|
|
let is_bytes = prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'b'));
|
|
self.pos += prefix_len;
|
|
let mut out = Vec::new();
|
|
self.string_body(raw, is_bytes, &mut out)?;
|
|
if is_bytes {
|
|
if text.is_some() {
|
|
return Err(Error::InvalidLiteral(self.offset + at));
|
|
}
|
|
bytes.get_or_insert_with(Vec::new).extend(out);
|
|
} else {
|
|
if bytes.is_some() {
|
|
return Err(Error::InvalidLiteral(self.offset + at));
|
|
}
|
|
let piece =
|
|
String::from_utf8(out).map_err(|_| Error::InvalidLiteral(self.offset + at))?;
|
|
text.get_or_insert_with(String::new).push_str(&piece);
|
|
}
|
|
self.skip_trivia(self.brackets > 0);
|
|
}
|
|
let value = match (text, bytes) {
|
|
(Some(text), None) => Value::Str(text),
|
|
(None, Some(bytes)) => Value::Bytes(bytes),
|
|
_ => return Err(self.error()),
|
|
};
|
|
Ok(Term {
|
|
value,
|
|
kind: Kind::Constant,
|
|
})
|
|
}
|
|
|
|
/// One quoted body, decoded into UTF-8 (`str`) or raw bytes (`bytes`).
|
|
fn string_body(&mut self, raw: bool, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
|
let quote = self.bytes[self.pos];
|
|
let triple = self.peek_at(1) == Some(quote) && self.peek_at(2) == Some(quote);
|
|
self.pos += if triple { 3 } else { 1 };
|
|
loop {
|
|
let Some(byte) = self.peek() else {
|
|
return Err(self.error());
|
|
};
|
|
if byte == quote
|
|
&& (!triple || (self.peek_at(1) == Some(quote) && self.peek_at(2) == Some(quote)))
|
|
{
|
|
self.pos += if triple { 3 } else { 1 };
|
|
return Ok(());
|
|
}
|
|
match byte {
|
|
b'\n' | b'\r' if !triple => return Err(self.error()),
|
|
b'\\' if raw => {
|
|
let Some(next) = self.peek_at(1) else {
|
|
return Err(self.error());
|
|
};
|
|
out.push(b'\\');
|
|
self.pos += 1;
|
|
if next == b'\n' || next == b'\r' || next == quote || next == b'\\' {
|
|
out.push(next);
|
|
self.pos += 1;
|
|
}
|
|
}
|
|
b'\\' => {
|
|
self.pos += 1;
|
|
self.escape(is_bytes, out)?;
|
|
}
|
|
byte if is_bytes && !byte.is_ascii() => return Err(self.error()),
|
|
byte => {
|
|
out.push(byte);
|
|
self.pos += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn escape(&mut self, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
|
let Some(byte) = self.peek() else {
|
|
return Err(self.error());
|
|
};
|
|
self.pos += 1;
|
|
let simple = match byte {
|
|
b'\n' => return Ok(()),
|
|
b'\r' => {
|
|
if self.peek() == Some(b'\n') {
|
|
self.pos += 1;
|
|
}
|
|
return Ok(());
|
|
}
|
|
b'\\' | b'\'' | b'"' => byte,
|
|
b'a' => 0x07,
|
|
b'b' => 0x08,
|
|
b'f' => 0x0c,
|
|
b'n' => b'\n',
|
|
b'r' => b'\r',
|
|
b't' => b'\t',
|
|
b'v' => 0x0b,
|
|
b'0'..=b'7' => {
|
|
let mut code = u32::from(byte - b'0');
|
|
for _ in 0..2 {
|
|
match self.peek() {
|
|
Some(digit @ b'0'..=b'7') => {
|
|
code = code * 8 + u32::from(digit - b'0');
|
|
self.pos += 1;
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
// Bytes keep the low eight bits of `\400`-`\777`, as CPython does.
|
|
return self.push_code(if is_bytes { code & 0xff } else { code }, is_bytes, out);
|
|
}
|
|
b'x' => {
|
|
let code = self.hex(2)?;
|
|
return self.push_code(code, is_bytes, out);
|
|
}
|
|
b'u' if !is_bytes => {
|
|
let code = self.hex(4)?;
|
|
return self.push_code(code, is_bytes, out);
|
|
}
|
|
b'U' if !is_bytes => {
|
|
let code = self.hex(8)?;
|
|
return self.push_code(code, is_bytes, out);
|
|
}
|
|
b'N' if !is_bytes => return Err(self.error()),
|
|
_ => {
|
|
// Unknown escapes keep the backslash (a `SyntaxWarning` in CPython).
|
|
out.push(b'\\');
|
|
self.pos -= 1;
|
|
return Ok(());
|
|
}
|
|
};
|
|
out.push(simple);
|
|
Ok(())
|
|
}
|
|
|
|
fn hex(&mut self, count: usize) -> Result<u32, Error> {
|
|
let mut code = 0u32;
|
|
for _ in 0..count {
|
|
let digit = self
|
|
.peek()
|
|
.and_then(|byte| (byte as char).to_digit(16))
|
|
.ok_or(self.error())?;
|
|
code = code * 16 + digit;
|
|
self.pos += 1;
|
|
}
|
|
Ok(code)
|
|
}
|
|
|
|
fn push_code(&self, code: u32, is_bytes: bool, out: &mut Vec<u8>) -> Result<(), Error> {
|
|
if is_bytes {
|
|
out.push(u8::try_from(code).map_err(|_| self.error())?);
|
|
return Ok(());
|
|
}
|
|
let ch = char::from_u32(code).ok_or(self.error())?;
|
|
let mut buffer = [0u8; 4];
|
|
out.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// `left + right` or `left - right` as `ast.literal_eval` permits: a signed real on the
|
|
/// left and an unsigned complex constant on the right, combined with CPython 3.14's
|
|
/// mixed-mode rules, which leave the imaginary part untouched by the real operand.
|
|
fn complex_sum(left: Term, subtract: bool, right: Term) -> Option<Term> {
|
|
if left.kind == Kind::Other || right.kind != Kind::Constant {
|
|
return None;
|
|
}
|
|
let real = match &left.value {
|
|
Value::Int(value) => value.to_f64().filter(|value| value.is_finite())?,
|
|
Value::Float(value) => *value,
|
|
_ => return None,
|
|
};
|
|
let Value::Complex { re, im } = right.value else {
|
|
return None;
|
|
};
|
|
let value = if subtract {
|
|
Value::Complex {
|
|
re: real - re,
|
|
im: -im,
|
|
}
|
|
} else {
|
|
Value::Complex { re: real + re, im }
|
|
};
|
|
Some(Term::other(value))
|
|
}
|
|
|
|
/// Python equality for hashable literal values: numbers compare by value across `bool`,
|
|
/// `int`, `float`, and `complex`, so `1`, `1.0`, `True`, and `(1+0j)` are one key.
|
|
#[derive(Hash, PartialEq, Eq)]
|
|
enum KeyId {
|
|
None,
|
|
Int(BigInt),
|
|
Float(u64),
|
|
Complex(u64, u64),
|
|
Str(String),
|
|
Bytes(Vec<u8>),
|
|
Tuple(Vec<KeyId>),
|
|
}
|
|
|
|
fn float_key(value: f64) -> KeyId {
|
|
if value.fract() == 0.0
|
|
&& let Some(integer) = BigInt::from_f64(value)
|
|
{
|
|
return KeyId::Int(integer);
|
|
}
|
|
KeyId::Float(value.to_bits())
|
|
}
|
|
|
|
fn key_id(value: &Value) -> Result<KeyId, Error> {
|
|
Ok(match value {
|
|
Value::None => KeyId::None,
|
|
Value::Bool(value) => KeyId::Int(BigInt::from(u8::from(*value))),
|
|
Value::Int(value) => KeyId::Int(value.clone()),
|
|
Value::Float(value) => float_key(*value),
|
|
Value::Complex { re, im } if *im == 0.0 => float_key(*re),
|
|
Value::Complex { re, im } => KeyId::Complex((re + 0.0).to_bits(), (im + 0.0).to_bits()),
|
|
Value::Str(text) => KeyId::Str(text.clone()),
|
|
Value::Bytes(bytes) => KeyId::Bytes(bytes.clone()),
|
|
Value::Tuple(values) => KeyId::Tuple(values.iter().map(key_id).collect::<Result<_, _>>()?),
|
|
value @ (Value::List(_) | Value::Dict(_) | Value::Set(_)) => {
|
|
return Err(Error::Unhashable(value.type_name()));
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Dict entries or set members in first-seen order: a repeated key keeps its first
|
|
/// position and, for dicts, takes the latest value.
|
|
#[derive(Default)]
|
|
struct UniqueValues {
|
|
keys: Vec<Value>,
|
|
values: Vec<Value>,
|
|
index: HashMap<KeyId, usize>,
|
|
}
|
|
|
|
impl UniqueValues {
|
|
fn insert(&mut self, key: Value, value: Option<Value>) -> Result<(), Error> {
|
|
match self.index.entry(key_id(&key)?) {
|
|
Entry::Occupied(slot) => {
|
|
if let Some(value) = value {
|
|
self.values[*slot.get()] = value;
|
|
}
|
|
}
|
|
Entry::Vacant(slot) => {
|
|
slot.insert(self.keys.len());
|
|
self.keys.push(key);
|
|
self.values.extend(value);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn into_entries(self) -> Vec<(Value, Value)> {
|
|
self.keys.into_iter().zip(self.values).collect()
|
|
}
|
|
}
|