Skip to content

Commit

Permalink
Extract Subsec to handle all supported ruby 2.[6|7].x use cases
Browse files Browse the repository at this point in the history
Ruby 3.x supports a lot more than what is expected form 2.7.x. This is
the base for the changes to support ruby 3.x changes at least
  • Loading branch information
b-n committed Aug 14, 2022
1 parent 5f24b68 commit 3dd1b6b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 41 deletions.
1 change: 1 addition & 0 deletions artichoke-backend/src/extn/core/time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::extn::prelude::*;

pub mod mruby;
pub mod trampoline;
pub mod subsec;

#[doc(inline)]
pub use spinoso_time::tzrs::*;
Expand Down
216 changes: 216 additions & 0 deletions artichoke-backend/src/extn/core/time/subsec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
///! Parser of Ruby Time subsecond parameters to help generate `Time`.
///!
///! This module implements the logic to parse two optional parameters in the
///! `Time.at` function call. These parameters (if specified) provide the number
///! of subsecond parts to add, and a scale of those subsecond parts (millis, micros,
///! and nanos).

use crate::extn::prelude::*;
use crate::extn::core::symbol::Symbol;
use crate::convert::implicitly_convert_to_int;

const NANOS_IN_SECOND: i64 = 1_000_000_000;

const MILLIS_IN_NANO: i64 = 1_000_000;
const MICROS_IN_NANO: i64 = 1_000;
const NANOS_IN_NANO: i64 = 1;

enum SubsecMultiplier {
Millis,
Micros,
Nanos,
}

impl SubsecMultiplier {
const fn as_nanos(self) -> i64 {
match self {
Self::Millis => MILLIS_IN_NANO,
Self::Micros => MICROS_IN_NANO,
Self::Nanos => NANOS_IN_NANO,
}
}
}

impl TryConvertMut<Option<Value>, SubsecMultiplier> for Artichoke {
type Error = Error;

fn try_convert_mut(&mut self, subsec_type: Option<Value>) -> Result<SubsecMultiplier, Self::Error> {
if let Some(mut subsec_type) = subsec_type {
let subsec_symbol = unsafe { Symbol::unbox_from_value(&mut subsec_type, self)? }.bytes(self);
match subsec_symbol {
b"milliseconds" => Ok(SubsecMultiplier::Millis),
b"usec" => Ok(SubsecMultiplier::Micros),
b"nsec" => Ok(SubsecMultiplier::Nanos),
_ => Err(ArgumentError::with_message("unexpected unit. expects :milliseconds, :usec, :nsec").into()),
}
} else {
Ok(SubsecMultiplier::Micros)
}
}
}



#[derive(Debug, Copy, Clone)]
pub struct Subsec {
secs: i64,
nanos: u32,
}

impl Subsec {
/// Returns a tuple of (seconds, nanoseconds). Subseconds are provided in
/// various accuracies, and can overflow. e.g. 1001 milliseconds, is 1
/// second, and 1_000_000 nanoseconds.
pub fn to_tuple(&self) -> (i64, u32) {
(self.secs, self.nanos)
}
}


impl TryConvertMut<(Option<Value>, Option<Value>), Subsec> for Artichoke {
type Error = Error;

fn try_convert_mut(&mut self, params: (Option<Value>, Option<Value>)) -> Result<Subsec, Self::Error> {
let (subsec, subsec_type) = params;

if let Some(subsec) = subsec {
let multiplier: SubsecMultiplier = self.try_convert_mut(subsec_type)?;
let multiplier_nanos = multiplier.as_nanos();
let seconds_base = NANOS_IN_SECOND / multiplier_nanos;

match subsec.ruby_type() {
Ruby::Fixnum => {
let subsec: i64 = subsec.try_convert_into(self)?;

// The below conversions should be safe. The multiplier is gauranteed to not be
// 0, the remainder should never overflow, and is gauranteed to be less than
// u32::MAX;
let secs = subsec / seconds_base;
let nanos = ((subsec % seconds_base) * multiplier_nanos) as u32;
Ok(Subsec { secs, nanos })
},
Ruby::Float => {
// TODO: Safe conversions here are really hard, there may end up being some
// loss in accuracy.
unreachable!("Not yet implemented")
},
_ => {
let subsec: i64 = implicitly_convert_to_int(self, subsec)?;

// The below conversions should be safe. The multiplier is gauranteed to not be
// 0, the remainder should never overflow, and is gauranteed to be less than
// u32::MAX;
let secs = subsec / seconds_base;
let nanos = ((subsec % seconds_base) * multiplier_nanos) as u32;
Ok(Subsec { secs, nanos })
}
}
} else {
Ok(Subsec { secs: 0, nanos: 0 })
}
}
}

#[cfg(test)]
mod tests {
use crate::test::prelude::*;

use super::Subsec;

fn subsec(interp: &mut Artichoke, params: (Option<&[u8]>, Option<&[u8]>)) -> Result<Subsec, Error> {
let (subsec, subsec_type) = params;
let subsec = subsec.map(|s| interp.eval(s).unwrap());
let subsec_type = subsec_type.map(|s| interp.eval(s).unwrap());

interp.try_convert_mut((subsec, subsec_type))
}

#[test]
fn no_subsec_provided() {
let mut interp = interpreter();

let result: Subsec = interp.try_convert_mut((None, None)).unwrap();
let (secs, nanos) = result.to_tuple();
assert_eq!(secs, 0);
assert_eq!(nanos, 0);
}

#[test]
fn no_subsec_provided_but_has_unit() {
let mut interp = interpreter();
let unit = interp.eval(b":usec").unwrap();

let result: Subsec = interp.try_convert_mut((None, Some(unit))).unwrap();
let (secs, nanos) = result.to_tuple();
assert_eq!(secs, 0);
assert_eq!(nanos, 0);
}

#[test]
fn no_unit_implies_micros() {
let mut interp = interpreter();

let result = subsec(&mut interp, (Some(b"0"), None)).unwrap();
assert_eq!(result.to_tuple(), (0, 0));

let result = subsec(&mut interp, (Some(b"999999"), None)).unwrap();
assert_eq!(result.to_tuple(), (0, 999_999_000));

let result = subsec(&mut interp, (Some(b"1000000"), None)).unwrap();
assert_eq!(result.to_tuple(), (1, 0));

let result = subsec(&mut interp, (Some(b"1000001"), None)).unwrap();
assert_eq!(result.to_tuple(), (1, 1_000));
}

#[test]
fn subsec_millis() {
let mut interp = interpreter();

let result = subsec(&mut interp, (Some(b"0"), Some(b":milliseconds"))).unwrap();
assert_eq!(result.to_tuple(), (0, 0));

let result = subsec(&mut interp, (Some(b"999"), Some(b":milliseconds"))).unwrap();
assert_eq!(result.to_tuple(), (0, 999_000_000));

let result = subsec(&mut interp, (Some(b"1000"), Some(b":milliseconds"))).unwrap();
assert_eq!(result.to_tuple(), (1, 0));

let result = subsec(&mut interp, (Some(b"1001"), Some(b":milliseconds"))).unwrap();
assert_eq!(result.to_tuple(), (1, 1_000_000));
}

#[test]
fn subsec_micros() {
let mut interp = interpreter();

let result = subsec(&mut interp, (Some(b"0"), Some(b":usec"))).unwrap();
assert_eq!(result.to_tuple(), (0, 0));

let result = subsec(&mut interp, (Some(b"999999"), Some(b":usec"))).unwrap();
assert_eq!(result.to_tuple(), (0, 999_999_000));

let result = subsec(&mut interp, (Some(b"1000000"), Some(b":usec"))).unwrap();
assert_eq!(result.to_tuple(), (1, 0));

let result = subsec(&mut interp, (Some(b"1000001"), Some(b":usec"))).unwrap();
assert_eq!(result.to_tuple(), (1, 1_000));
}

#[test]
fn subsub_nanos() {
let mut interp = interpreter();

let result = subsec(&mut interp, (Some(b"0"), Some(b":nsec"))).unwrap();
assert_eq!(result.to_tuple(), (0, 0));

let result = subsec(&mut interp, (Some(b"999999999"), Some(b":nsec"))).unwrap();
assert_eq!(result.to_tuple(), (0, 999_999_999));

let result = subsec(&mut interp, (Some(b"1000000000"), Some(b":nsec"))).unwrap();
assert_eq!(result.to_tuple(), (1, 0));

let result = subsec(&mut interp, (Some(b"1000000001"), Some(b":nsec"))).unwrap();
assert_eq!(result.to_tuple(), (1, 1));
}
}
49 changes: 8 additions & 41 deletions artichoke-backend/src/extn/core/time/trampoline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,9 @@

use crate::convert::{implicitly_convert_to_int, implicitly_convert_to_string};
use crate::extn::core::symbol::Symbol;
use crate::extn::core::time::{Offset, Time};
use crate::extn::core::time::{Offset, Time, subsec::Subsec};
use crate::extn::prelude::*;

const MAX_NANOS: i64 = 1_000_000_000 - 1;
const MILLIS_IN_NANO: i64 = 1_000_000;
const MICROS_IN_NANO: i64 = 1_000;
const NANOS_IN_NANO: i64 = 1;

// Generate a subsecond multiplier from the given ruby value.
//
// - If not provided, the defaults to Micros.
// - Otherwise, expects a symbol with :milliseconds, :usec, or :nsec.
fn subsec_multiplier(interp: &mut Artichoke, subsec_type: Option<Value>) -> Result<i64, Error> {
subsec_type.map_or(Ok(MICROS_IN_NANO), |mut value| {
let subsec_symbol = unsafe { Symbol::unbox_from_value(&mut value, interp)? }.bytes(interp);
match subsec_symbol {
b"milliseconds" => Ok(MILLIS_IN_NANO),
b"usec" => Ok(MICROS_IN_NANO),
b"nsec" => Ok(NANOS_IN_NANO),
_ => Err(ArgumentError::with_message("unexpected unit. expects :milliseconds, :usec, :nsec").into()),
}
})
}

// Convert a Ruby Value to a Offset which can be used to construct a _time_.
fn offset_from_value(interp: &mut Artichoke, mut value: Value) -> Result<Offset, Error> {
if let Ok(offset_seconds) = implicitly_convert_to_int(interp, value) {
Expand Down Expand Up @@ -118,25 +97,13 @@ pub fn at(
_ => Err(ArgumentError::with_message("invalid arguments"))?,
};

let seconds = implicitly_convert_to_int(interp, seconds)?;

let subsec_nanos = if let Some(subsec) = subsec {
let subsec_multiplier = subsec_multiplier(interp, subsec_type)?;
let subsec = implicitly_convert_to_int(interp, subsec)?
.checked_mul(subsec_multiplier)
.ok_or_else(|| ArgumentError::with_message("Time too large"))?;

// 0..=MAX_NANOS is a safe conversion since it's gauranteed to be
// inside u32 range.
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
match subsec {
0..=MAX_NANOS => Ok(subsec as u32),
i64::MIN..=-1 => Err(ArgumentError::with_message("subseconds needs to be > 0")),
_ => Err(ArgumentError::with_message("subseconds outside of range")),
}?
} else {
0
};

let subsec: Subsec = interp.try_convert_mut((subsec, subsec_type))?;
let (subsec_secs, subsec_nanos) = subsec.to_tuple();

let seconds = implicitly_convert_to_int(interp, seconds)?
.checked_add(subsec_secs)
.ok_or(ArgumentError::with_message("Time too large"))?;

let offset = match options {
Some(options) => offset_from_options(interp, options)?,
Expand Down

0 comments on commit 3dd1b6b

Please sign in to comment.