mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Complete PyTime conversion fixtures and repair existing helpers (#9317)
* Fix rounding and bounds in existing PyTime test helpers Extract the already-exposed conversion helper prerequisite from the broader #9143 surface without adding new test C-API exports or changing time.sleep. Assisted-by: Codex:unknown * Complete PyTime conversion fixtures in _testinternalcapi Stack the eight consumed conversion APIs on the existing-helper prerequisite in #9317. Keep the arithmetic local to the real fixture consumers; do not expose _testcapi or change sleep/lifecycle behavior. Assisted-by: Codex:unknown --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
a788de29da
commit
413710c107
2 files changed
+354
-13
No files matched your search
@@ -1,5 +1,7 @@
|
||||
pub(crate) use _testinternalcapi::module_def;
|
||||
|
||||
mod pytime;
|
||||
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static RARE_SET_CLASS: AtomicUsize = AtomicUsize::new(0);
|
||||
@@ -26,6 +28,7 @@ pub(crate) fn note_func_modification() {
|
||||
|
||||
#[pymodule]
|
||||
mod _testinternalcapi {
|
||||
use super::pytime::Round;
|
||||
use super::*;
|
||||
use crate::{
|
||||
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
@@ -37,7 +40,7 @@ mod _testinternalcapi {
|
||||
common::{hash::PyHash, lock::LazyLock},
|
||||
dict_inner,
|
||||
frame::FrameObject,
|
||||
function::{OptionalArg, PyComparisonValue},
|
||||
function::{FuncArgs, OptionalArg, PyComparisonValue},
|
||||
object::{Traverse, TraverseFn},
|
||||
protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods},
|
||||
types::{
|
||||
@@ -45,8 +48,13 @@ mod _testinternalcapi {
|
||||
SelfIter,
|
||||
},
|
||||
};
|
||||
#[cfg(any(unix, windows))]
|
||||
use rustpython_host_env::time::TimeT;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
type TimeT = i64;
|
||||
|
||||
// ADAPTIVE_WARMUP_VALUE + 1
|
||||
#[pyattr]
|
||||
const SPECIALIZATION_THRESHOLD: usize = 2;
|
||||
@@ -69,7 +77,7 @@ mod _testinternalcapi {
|
||||
const SIZEOF_PYOBJECT: usize = core::mem::size_of::<crate::PyObject>();
|
||||
|
||||
#[pyattr]
|
||||
const SIZEOF_TIME_T: usize = 8;
|
||||
const SIZEOF_TIME_T: usize = core::mem::size_of::<TimeT>();
|
||||
|
||||
// JUMP_BACKWARD_INITIAL_VALUE + 1
|
||||
#[pyattr]
|
||||
@@ -486,24 +494,186 @@ mod _testinternalcapi {
|
||||
}
|
||||
}
|
||||
|
||||
fn pytime_args<'a, const N: usize>(
|
||||
args: &'a FuncArgs,
|
||||
name: &str,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<&'a [PyObjectRef; N]> {
|
||||
// These CPython test helpers use METH_VARARGS, not keyword parsing.
|
||||
if !args.kwargs.is_empty() {
|
||||
return Err(vm.new_type_error(format!("{name}() takes no keyword arguments")));
|
||||
}
|
||||
args.args.as_slice().try_into().map_err(|_| {
|
||||
vm.new_type_error(format!(
|
||||
"function takes exactly {} argument{} ({} given)",
|
||||
N,
|
||||
if N == 1 { "" } else { "s" },
|
||||
args.args.len()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn pytime_c_int(object: &PyObject, vm: &VirtualMachine) -> PyResult<i32> {
|
||||
// PyArg_ParseTuple's "i" converts to C long before checking C int bounds.
|
||||
let value: core::ffi::c_long = object
|
||||
.try_index(vm)?
|
||||
.as_bigint()
|
||||
.try_into()
|
||||
.map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?;
|
||||
num_traits::ToPrimitive::to_i32(&value).ok_or_else(|| {
|
||||
vm.new_overflow_error(if value < 0 {
|
||||
"signed integer is less than minimum"
|
||||
} else {
|
||||
"signed integer is greater than maximum"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn pytime_i64(object: &PyObject, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
object
|
||||
.try_index(vm)?
|
||||
.as_bigint()
|
||||
.try_into()
|
||||
.map_err(|_| vm.new_overflow_error("Python int too large to convert to C int64_t"))
|
||||
}
|
||||
|
||||
fn pytime_pair(
|
||||
value: i64,
|
||||
denominator: i64,
|
||||
minimum: i64,
|
||||
maximum: i64,
|
||||
clamp: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<(i64, i64)> {
|
||||
let seconds = value.div_euclid(denominator);
|
||||
if !(minimum..=maximum).contains(&seconds) {
|
||||
if !clamp {
|
||||
return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
|
||||
}
|
||||
return Ok((seconds.clamp(minimum, maximum), 0));
|
||||
}
|
||||
Ok((seconds, value.rem_euclid(denominator)))
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsTimespec")]
|
||||
fn pytime_as_timespec(ns: i64) -> (i64, i64) {
|
||||
let sec = ns.div_euclid(1_000_000_000);
|
||||
let nsec = ns.rem_euclid(1_000_000_000);
|
||||
(sec, nsec)
|
||||
fn pytime_as_timespec(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
let [ns] = pytime_args::<1>(&args, "_PyTime_AsTimespec", vm)?;
|
||||
pytime_pair(
|
||||
pytime_i64(ns, vm)?,
|
||||
1_000_000_000,
|
||||
TimeT::MIN as _,
|
||||
TimeT::MAX as _,
|
||||
false,
|
||||
vm,
|
||||
)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsTimespec_clamp")]
|
||||
fn pytime_as_timespec_clamp(ns: i64) -> (i64, i64) {
|
||||
pytime_as_timespec(ns)
|
||||
fn pytime_as_timespec_clamp(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
let [ns] = pytime_args::<1>(&args, "_PyTime_AsTimespec_clamp", vm)?;
|
||||
pytime_pair(
|
||||
pytime_i64(ns, vm)?,
|
||||
1_000_000_000,
|
||||
TimeT::MIN as _,
|
||||
TimeT::MAX as _,
|
||||
true,
|
||||
vm,
|
||||
)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsTimeval_clamp")]
|
||||
fn pytime_as_timeval_clamp(ns: i64, _rnd: OptionalArg<i32>) -> (i64, i64) {
|
||||
let us = ns.div_euclid(1000);
|
||||
let sec = us.div_euclid(1_000_000);
|
||||
let usec = us.rem_euclid(1_000_000);
|
||||
(sec, usec)
|
||||
fn pytime_as_timeval_clamp(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval_clamp", vm)?;
|
||||
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
|
||||
// Validate rounding before converting the timestamp, as the C wrapper does.
|
||||
pytime_timeval(pytime_i64(ns, vm)?, round, true, vm)
|
||||
}
|
||||
|
||||
fn pytime_timeval(
|
||||
ns: i64,
|
||||
round: Round,
|
||||
clamp: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<(i64, i64)> {
|
||||
let us = round.divide(ns, 1_000);
|
||||
// Windows timeval uses C long even though the host time_t is 64-bit.
|
||||
#[cfg(windows)]
|
||||
let (minimum, maximum): (i64, i64) =
|
||||
(core::ffi::c_long::MIN as _, core::ffi::c_long::MAX as _);
|
||||
#[cfg(not(windows))]
|
||||
let (minimum, maximum): (i64, i64) = (TimeT::MIN as _, TimeT::MAX as _);
|
||||
pytime_pair(us, 1_000_000, minimum, maximum, clamp, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_FromSeconds")]
|
||||
fn pytime_from_seconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
let [seconds] = pytime_args::<1>(&args, "_PyTime_FromSeconds", vm)?;
|
||||
Ok(pytime::from_seconds(pytime_c_int(seconds, vm)?))
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_FromSecondsObject")]
|
||||
fn pytime_from_seconds_object(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
let [object, round] = pytime_args::<2>(&args, "_PyTime_FromSecondsObject", vm)?;
|
||||
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
|
||||
pytime::from_seconds_object(object, round, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsTimeval")]
|
||||
fn pytime_as_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval", vm)?;
|
||||
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
|
||||
pytime_timeval(pytime_i64(ns, vm)?, round, false, vm)
|
||||
}
|
||||
|
||||
fn pytime_as_subseconds(
|
||||
args: FuncArgs,
|
||||
name: &str,
|
||||
divisor: i64,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<i64> {
|
||||
let [ns, round] = pytime_args::<2>(&args, name, vm)?;
|
||||
let round = pytime_c_int(round, vm)?;
|
||||
// These two C wrappers convert the timestamp before validating the mode.
|
||||
let ns = pytime_i64(ns, vm)?;
|
||||
Ok(Round::from_int(round, vm)?.divide(ns, divisor))
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsMilliseconds")]
|
||||
fn pytime_as_milliseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
pytime_as_subseconds(args, "_PyTime_AsMilliseconds", 1_000_000, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_AsMicroseconds")]
|
||||
fn pytime_as_microseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
pytime_as_subseconds(args, "_PyTime_AsMicroseconds", 1_000, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_ObjectToTime_t")]
|
||||
fn pytime_object_to_time_t(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
let [object, round] = pytime_args::<2>(&args, "_PyTime_ObjectToTime_t", vm)?;
|
||||
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
|
||||
pytime::object_to_time_t(object, round, vm)
|
||||
}
|
||||
|
||||
fn pytime_object_to_denominator(
|
||||
args: FuncArgs,
|
||||
name: &str,
|
||||
denominator: i64,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<(i64, i64)> {
|
||||
let [object, round] = pytime_args::<2>(&args, name, vm)?;
|
||||
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
|
||||
pytime::object_to_denominator(object, denominator, round, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_ObjectToTimeval")]
|
||||
fn pytime_object_to_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
pytime_object_to_denominator(args, "_PyTime_ObjectToTimeval", 1_000_000, vm)
|
||||
}
|
||||
|
||||
#[pyfunction(name = "_PyTime_ObjectToTimespec")]
|
||||
fn pytime_object_to_timespec(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
|
||||
pytime_object_to_denominator(args, "_PyTime_ObjectToTimespec", 1_000_000_000, vm)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Numeric conversions used by the internal PyTime test helpers.
|
||||
|
||||
#![cfg_attr(target_env = "musl", allow(deprecated))]
|
||||
|
||||
use crate::{AsObject, PyObject, PyResult, VirtualMachine, builtins::PyFloat};
|
||||
use num_traits::ToPrimitive;
|
||||
use rustpython_host_env::time::SEC_TO_NS;
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
pub(super) use rustpython_host_env::time::TimeT;
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub(super) type TimeT = i64;
|
||||
|
||||
const TIME_T_MIN: i64 = TimeT::MIN as _;
|
||||
const TIME_T_MAX: i64 = TimeT::MAX as _;
|
||||
const PYTIME_OVERFLOW: &str = "timestamp out of range for C PyTime_t";
|
||||
const TIME_T_OVERFLOW: &str = "timestamp out of range for platform time_t";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum Round {
|
||||
Floor,
|
||||
Ceiling,
|
||||
HalfEven,
|
||||
Up,
|
||||
}
|
||||
|
||||
impl Round {
|
||||
pub(super) fn from_int(value: i32, vm: &VirtualMachine) -> PyResult<Self> {
|
||||
match value {
|
||||
0 => Ok(Self::Floor),
|
||||
1 => Ok(Self::Ceiling),
|
||||
2 => Ok(Self::HalfEven),
|
||||
3 => Ok(Self::Up),
|
||||
_ => Err(vm.new_value_error("invalid rounding")),
|
||||
}
|
||||
}
|
||||
|
||||
fn round(self, value: f64) -> f64 {
|
||||
match self {
|
||||
Self::Floor => value.floor(),
|
||||
Self::Ceiling => value.ceil(),
|
||||
Self::HalfEven => value.round_ties_even(),
|
||||
Self::Up if value >= 0.0 => value.ceil(),
|
||||
Self::Up => value.floor(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn divide(self, value: i64, divisor: i64) -> i64 {
|
||||
debug_assert!(divisor > 1);
|
||||
let quotient = value / divisor;
|
||||
let remainder = value % divisor;
|
||||
let round_away = match self {
|
||||
Self::Floor => remainder < 0,
|
||||
Self::Ceiling => remainder > 0,
|
||||
Self::HalfEven => {
|
||||
let remainder = remainder.abs();
|
||||
remainder > divisor / 2
|
||||
|| (divisor % 2 == 0 && remainder == divisor / 2 && quotient % 2 != 0)
|
||||
}
|
||||
Self::Up => remainder != 0,
|
||||
};
|
||||
// Adjust the quotient, not the input: adding to i64::MAX or
|
||||
// subtracting from i64::MIN before dividing would overflow.
|
||||
quotient + if round_away { value.signum() } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_seconds(seconds: i32) -> i64 {
|
||||
i64::from(seconds) * SEC_TO_NS
|
||||
}
|
||||
|
||||
fn index_seconds(object: &PyObject, message: &str, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
let seconds = object.try_index(vm).map_err(|err| {
|
||||
if err.fast_isinstance(vm.ctx.exceptions.overflow_error) {
|
||||
vm.new_overflow_error(message)
|
||||
} else {
|
||||
err
|
||||
}
|
||||
})?;
|
||||
seconds
|
||||
.as_bigint()
|
||||
.to_i64()
|
||||
.ok_or_else(|| vm.new_overflow_error(message))
|
||||
}
|
||||
|
||||
fn float_seconds(object: &PyObject, vm: &VirtualMachine) -> PyResult<f64> {
|
||||
// PyFloat_AsDouble bypasses __float__ overrides on float subclasses.
|
||||
let seconds = match object.downcast_ref::<PyFloat>() {
|
||||
Some(float) => float.to_f64(),
|
||||
None => object.try_float(vm)?.to_f64(),
|
||||
};
|
||||
if seconds.is_nan() {
|
||||
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
|
||||
}
|
||||
Ok(seconds)
|
||||
}
|
||||
|
||||
pub(super) fn from_seconds_object(
|
||||
object: &PyObject,
|
||||
round: Round,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<i64> {
|
||||
// _PyTime_FromSecondsObject uses __index__ before __float__ and bounds
|
||||
// the converted value to signed 64-bit nanoseconds.
|
||||
if object.number().is_index() {
|
||||
index_seconds(object, PYTIME_OVERFLOW, vm)?
|
||||
.checked_mul(SEC_TO_NS)
|
||||
.ok_or_else(|| vm.new_overflow_error(PYTIME_OVERFLOW))
|
||||
} else {
|
||||
let nanoseconds = round.round(float_seconds(object, vm)? * SEC_TO_NS as f64);
|
||||
let minimum = i64::MIN as f64;
|
||||
// The maximum rounds up to 2**63 as a double, so use an exclusive bound.
|
||||
if !(minimum..-minimum).contains(&nanoseconds) {
|
||||
return Err(vm.new_overflow_error(PYTIME_OVERFLOW));
|
||||
}
|
||||
Ok(nanoseconds as i64)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_time_t(seconds: i64, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
if !(TIME_T_MIN..=TIME_T_MAX).contains(&seconds) {
|
||||
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
|
||||
}
|
||||
Ok(seconds)
|
||||
}
|
||||
|
||||
fn float_to_time_t(seconds: f64, vm: &VirtualMachine) -> PyResult<i64> {
|
||||
let minimum = TIME_T_MIN as f64;
|
||||
if !(minimum..-minimum).contains(&seconds) {
|
||||
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
|
||||
}
|
||||
Ok(seconds as i64)
|
||||
}
|
||||
|
||||
pub(super) fn object_to_time_t(
|
||||
object: &PyObject,
|
||||
round: Round,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<i64> {
|
||||
if object.number().is_index() {
|
||||
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)
|
||||
} else {
|
||||
float_to_time_t(round.round(float_seconds(object, vm)?), vm)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn object_to_denominator(
|
||||
object: &PyObject,
|
||||
denominator: i64,
|
||||
round: Round,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<(i64, i64)> {
|
||||
if object.number().is_index() {
|
||||
return Ok((
|
||||
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)?,
|
||||
0,
|
||||
));
|
||||
}
|
||||
let value = float_seconds(object, vm)?;
|
||||
let mut seconds = value.trunc();
|
||||
let denominator = denominator as f64;
|
||||
let mut numerator = round.round(value.fract() * denominator);
|
||||
if numerator >= denominator {
|
||||
numerator -= denominator;
|
||||
seconds += 1.0;
|
||||
} else if numerator < 0.0 {
|
||||
numerator += denominator;
|
||||
seconds -= 1.0;
|
||||
}
|
||||
Ok((float_to_time_t(seconds, vm)?, numerator as i64))
|
||||
}
|
||||
Reference in new issue
Block a user