Skip to content

Commit 4aee9e9

Browse files
codexyouknowdot
authored andcommitted
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
1 parent 9f5620d commit 4aee9e9

2 files changed

Lines changed: 257 additions & 16 deletions

File tree

‎crates/vm/src/stdlib/_testinternalcapi.rs‎

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub(crate) use _testinternalcapi::module_def;
22

3+
mod pytime;
4+
35
use core::sync::atomic::{AtomicUsize, Ordering};
46

57
static RARE_SET_CLASS: AtomicUsize = AtomicUsize::new(0);
@@ -26,6 +28,7 @@ pub(crate) fn note_func_modification() {
2628

2729
#[pymodule]
2830
mod _testinternalcapi {
31+
use super::pytime::Round;
2932
use super::*;
3033
use crate::{
3134
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
@@ -581,29 +584,96 @@ mod _testinternalcapi {
581584
#[pyfunction(name = "_PyTime_AsTimeval_clamp")]
582585
fn pytime_as_timeval_clamp(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
583586
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval_clamp", vm)?;
584-
let round = pytime_c_int(round, vm)?;
585-
if !(0..=3).contains(&round) {
586-
return Err(vm.new_value_error("invalid rounding"));
587-
}
587+
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
588588
// Validate rounding before converting the timestamp, as the C wrapper does.
589-
let ns = pytime_i64(ns, vm)?;
590-
let quotient = ns / 1_000;
591-
let remainder = ns % 1_000;
592-
let round_away = match round {
593-
0 => remainder < 0,
594-
1 => remainder > 0,
595-
2 => remainder.abs() > 500 || (remainder.abs() == 500 && quotient % 2 != 0),
596-
_ => remainder != 0,
597-
};
598-
// Adjust the quotient, never ns: both signed 64-bit endpoints are valid.
599-
let us = quotient + if round_away { ns.signum() } else { 0 };
589+
pytime_timeval(pytime_i64(ns, vm)?, round, true, vm)
590+
}
591+
592+
fn pytime_timeval(
593+
ns: i64,
594+
round: Round,
595+
clamp: bool,
596+
vm: &VirtualMachine,
597+
) -> PyResult<(i64, i64)> {
598+
let us = round.divide(ns, 1_000);
600599
// Windows timeval uses C long even though the host time_t is 64-bit.
601600
#[cfg(windows)]
602601
let (minimum, maximum): (i64, i64) =
603602
(core::ffi::c_long::MIN as _, core::ffi::c_long::MAX as _);
604603
#[cfg(not(windows))]
605604
let (minimum, maximum): (i64, i64) = (TimeT::MIN as _, TimeT::MAX as _);
606-
pytime_pair(us, 1_000_000, minimum, maximum, true, vm)
605+
pytime_pair(us, 1_000_000, minimum, maximum, clamp, vm)
606+
}
607+
608+
#[pyfunction(name = "_PyTime_FromSeconds")]
609+
fn pytime_from_seconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
610+
let [seconds] = pytime_args::<1>(&args, "_PyTime_FromSeconds", vm)?;
611+
Ok(pytime::from_seconds(pytime_c_int(seconds, vm)?))
612+
}
613+
614+
#[pyfunction(name = "_PyTime_FromSecondsObject")]
615+
fn pytime_from_seconds_object(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
616+
let [object, round] = pytime_args::<2>(&args, "_PyTime_FromSecondsObject", vm)?;
617+
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
618+
pytime::from_seconds_object(object, round, vm)
619+
}
620+
621+
#[pyfunction(name = "_PyTime_AsTimeval")]
622+
fn pytime_as_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
623+
let [ns, round] = pytime_args::<2>(&args, "_PyTime_AsTimeval", vm)?;
624+
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
625+
pytime_timeval(pytime_i64(ns, vm)?, round, false, vm)
626+
}
627+
628+
fn pytime_as_subseconds(
629+
args: FuncArgs,
630+
name: &str,
631+
divisor: i64,
632+
vm: &VirtualMachine,
633+
) -> PyResult<i64> {
634+
let [ns, round] = pytime_args::<2>(&args, name, vm)?;
635+
let round = pytime_c_int(round, vm)?;
636+
// These two C wrappers convert the timestamp before validating the mode.
637+
let ns = pytime_i64(ns, vm)?;
638+
Ok(Round::from_int(round, vm)?.divide(ns, divisor))
639+
}
640+
641+
#[pyfunction(name = "_PyTime_AsMilliseconds")]
642+
fn pytime_as_milliseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
643+
pytime_as_subseconds(args, "_PyTime_AsMilliseconds", 1_000_000, vm)
644+
}
645+
646+
#[pyfunction(name = "_PyTime_AsMicroseconds")]
647+
fn pytime_as_microseconds(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
648+
pytime_as_subseconds(args, "_PyTime_AsMicroseconds", 1_000, vm)
649+
}
650+
651+
#[pyfunction(name = "_PyTime_ObjectToTime_t")]
652+
fn pytime_object_to_time_t(args: FuncArgs, vm: &VirtualMachine) -> PyResult<i64> {
653+
let [object, round] = pytime_args::<2>(&args, "_PyTime_ObjectToTime_t", vm)?;
654+
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
655+
pytime::object_to_time_t(object, round, vm)
656+
}
657+
658+
fn pytime_object_to_denominator(
659+
args: FuncArgs,
660+
name: &str,
661+
denominator: i64,
662+
vm: &VirtualMachine,
663+
) -> PyResult<(i64, i64)> {
664+
let [object, round] = pytime_args::<2>(&args, name, vm)?;
665+
let round = Round::from_int(pytime_c_int(round, vm)?, vm)?;
666+
pytime::object_to_denominator(object, denominator, round, vm)
667+
}
668+
669+
#[pyfunction(name = "_PyTime_ObjectToTimeval")]
670+
fn pytime_object_to_timeval(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
671+
pytime_object_to_denominator(args, "_PyTime_ObjectToTimeval", 1_000_000, vm)
672+
}
673+
674+
#[pyfunction(name = "_PyTime_ObjectToTimespec")]
675+
fn pytime_object_to_timespec(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(i64, i64)> {
676+
pytime_object_to_denominator(args, "_PyTime_ObjectToTimespec", 1_000_000_000, vm)
607677
}
608678

609679
#[pyfunction]
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//! Numeric conversions used by the internal PyTime test helpers.
2+
3+
#![cfg_attr(target_env = "musl", allow(deprecated))]
4+
5+
use crate::{AsObject, PyObject, PyResult, VirtualMachine, builtins::PyFloat};
6+
use num_traits::ToPrimitive;
7+
use rustpython_host_env::time::SEC_TO_NS;
8+
9+
#[cfg(any(unix, windows))]
10+
pub(super) use rustpython_host_env::time::TimeT;
11+
#[cfg(not(any(unix, windows)))]
12+
pub(super) type TimeT = i64;
13+
14+
const TIME_T_MIN: i64 = TimeT::MIN as _;
15+
const TIME_T_MAX: i64 = TimeT::MAX as _;
16+
const PYTIME_OVERFLOW: &str = "timestamp out of range for C PyTime_t";
17+
const TIME_T_OVERFLOW: &str = "timestamp out of range for platform time_t";
18+
19+
#[derive(Clone, Copy)]
20+
pub(super) enum Round {
21+
Floor,
22+
Ceiling,
23+
HalfEven,
24+
Up,
25+
}
26+
27+
impl Round {
28+
pub(super) fn from_int(value: i32, vm: &VirtualMachine) -> PyResult<Self> {
29+
match value {
30+
0 => Ok(Self::Floor),
31+
1 => Ok(Self::Ceiling),
32+
2 => Ok(Self::HalfEven),
33+
3 => Ok(Self::Up),
34+
_ => Err(vm.new_value_error("invalid rounding")),
35+
}
36+
}
37+
38+
fn round(self, value: f64) -> f64 {
39+
match self {
40+
Self::Floor => value.floor(),
41+
Self::Ceiling => value.ceil(),
42+
Self::HalfEven => value.round_ties_even(),
43+
Self::Up if value >= 0.0 => value.ceil(),
44+
Self::Up => value.floor(),
45+
}
46+
}
47+
48+
pub(super) fn divide(self, value: i64, divisor: i64) -> i64 {
49+
debug_assert!(divisor > 1);
50+
let quotient = value / divisor;
51+
let remainder = value % divisor;
52+
let round_away = match self {
53+
Self::Floor => remainder < 0,
54+
Self::Ceiling => remainder > 0,
55+
Self::HalfEven => {
56+
let remainder = remainder.abs();
57+
remainder > divisor / 2
58+
|| (divisor % 2 == 0 && remainder == divisor / 2 && quotient % 2 != 0)
59+
}
60+
Self::Up => remainder != 0,
61+
};
62+
// Adjust the quotient, not the input: adding to i64::MAX or
63+
// subtracting from i64::MIN before dividing would overflow.
64+
quotient + if round_away { value.signum() } else { 0 }
65+
}
66+
}
67+
68+
pub(super) fn from_seconds(seconds: i32) -> i64 {
69+
i64::from(seconds) * SEC_TO_NS
70+
}
71+
72+
fn index_seconds(object: &PyObject, message: &str, vm: &VirtualMachine) -> PyResult<i64> {
73+
let seconds = object.try_index(vm).map_err(|err| {
74+
if err.fast_isinstance(vm.ctx.exceptions.overflow_error) {
75+
vm.new_overflow_error(message)
76+
} else {
77+
err
78+
}
79+
})?;
80+
seconds
81+
.as_bigint()
82+
.to_i64()
83+
.ok_or_else(|| vm.new_overflow_error(message))
84+
}
85+
86+
fn float_seconds(object: &PyObject, vm: &VirtualMachine) -> PyResult<f64> {
87+
// PyFloat_AsDouble bypasses __float__ overrides on float subclasses.
88+
let seconds = match object.downcast_ref::<PyFloat>() {
89+
Some(float) => float.to_f64(),
90+
None => object.try_float(vm)?.to_f64(),
91+
};
92+
if seconds.is_nan() {
93+
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
94+
}
95+
Ok(seconds)
96+
}
97+
98+
pub(super) fn from_seconds_object(
99+
object: &PyObject,
100+
round: Round,
101+
vm: &VirtualMachine,
102+
) -> PyResult<i64> {
103+
// _PyTime_FromSecondsObject uses __index__ before __float__ and bounds
104+
// the converted value to signed 64-bit nanoseconds.
105+
if object.number().is_index() {
106+
index_seconds(object, PYTIME_OVERFLOW, vm)?
107+
.checked_mul(SEC_TO_NS)
108+
.ok_or_else(|| vm.new_overflow_error(PYTIME_OVERFLOW))
109+
} else {
110+
let nanoseconds = round.round(float_seconds(object, vm)? * SEC_TO_NS as f64);
111+
let minimum = i64::MIN as f64;
112+
// The maximum rounds up to 2**63 as a double, so use an exclusive bound.
113+
if !(minimum..-minimum).contains(&nanoseconds) {
114+
return Err(vm.new_overflow_error(PYTIME_OVERFLOW));
115+
}
116+
Ok(nanoseconds as i64)
117+
}
118+
}
119+
120+
fn check_time_t(seconds: i64, vm: &VirtualMachine) -> PyResult<i64> {
121+
if !(TIME_T_MIN..=TIME_T_MAX).contains(&seconds) {
122+
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
123+
}
124+
Ok(seconds)
125+
}
126+
127+
fn float_to_time_t(seconds: f64, vm: &VirtualMachine) -> PyResult<i64> {
128+
let minimum = TIME_T_MIN as f64;
129+
if !(minimum..-minimum).contains(&seconds) {
130+
return Err(vm.new_overflow_error(TIME_T_OVERFLOW));
131+
}
132+
Ok(seconds as i64)
133+
}
134+
135+
pub(super) fn object_to_time_t(
136+
object: &PyObject,
137+
round: Round,
138+
vm: &VirtualMachine,
139+
) -> PyResult<i64> {
140+
if object.number().is_index() {
141+
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)
142+
} else {
143+
float_to_time_t(round.round(float_seconds(object, vm)?), vm)
144+
}
145+
}
146+
147+
pub(super) fn object_to_denominator(
148+
object: &PyObject,
149+
denominator: i64,
150+
round: Round,
151+
vm: &VirtualMachine,
152+
) -> PyResult<(i64, i64)> {
153+
if object.number().is_index() {
154+
return Ok((
155+
check_time_t(index_seconds(object, TIME_T_OVERFLOW, vm)?, vm)?,
156+
0,
157+
));
158+
}
159+
let value = float_seconds(object, vm)?;
160+
let mut seconds = value.trunc();
161+
let denominator = denominator as f64;
162+
let mut numerator = round.round(value.fract() * denominator);
163+
if numerator >= denominator {
164+
numerator -= denominator;
165+
seconds += 1.0;
166+
} else if numerator < 0.0 {
167+
numerator += denominator;
168+
seconds -= 1.0;
169+
}
170+
Ok((float_to_time_t(seconds, vm)?, numerator as i64))
171+
}

0 commit comments

Comments
 (0)