artichoke_backend/extn/core/time/
subsec.rs

1//! Parser for Ruby Time subsecond parameters to help generate `Time`.
2//!
3//! This module implements the logic to parse two optional parameters in the
4//! `Time.at` function call. These parameters (if specified) provide the number
5//! of subsecond parts to add, and a scale of those subsecond parts (millis, micros,
6//! and nanos).
7
8use crate::convert::implicitly_convert_to_int;
9use crate::extn::core::symbol::Symbol;
10use crate::extn::prelude::*;
11
12const NANOS_IN_SECOND: i64 = 1_000_000_000;
13
14const MILLIS_IN_NANO: i64 = 1_000_000;
15const MICROS_IN_NANO: i64 = 1_000;
16const NANOS_IN_NANO: i64 = 1;
17
18#[expect(clippy::cast_precision_loss, reason = "this cast is intentionally lossy")]
19const MIN_FLOAT_SECONDS: f64 = i64::MIN as f64;
20#[expect(clippy::cast_precision_loss, reason = "this cast is intentionally lossy")]
21const MAX_FLOAT_SECONDS: f64 = i64::MAX as f64;
22const MIN_FLOAT_NANOS: f64 = 0.0;
23#[expect(
24    clippy::cast_precision_loss,
25    reason = "NANOS_IN_SECOND is always in range of f64 without loss"
26)]
27const MAX_FLOAT_NANOS: f64 = NANOS_IN_SECOND as f64;
28#[expect(
29    clippy::cast_precision_loss,
30    reason = "NANOS_IN_SECOND is always in range of f64 without loss"
31)]
32const NANOS_IN_SECOND_F64: f64 = NANOS_IN_SECOND as f64;
33
34enum SubsecMultiplier {
35    Millis,
36    Micros,
37    Nanos,
38}
39
40impl SubsecMultiplier {
41    #[must_use]
42    const fn as_nanos(&self) -> i64 {
43        match self {
44            Self::Millis => MILLIS_IN_NANO,
45            Self::Micros => MICROS_IN_NANO,
46            Self::Nanos => NANOS_IN_NANO,
47        }
48    }
49}
50
51impl TryConvertMut<Option<Value>, SubsecMultiplier> for Artichoke {
52    type Error = Error;
53
54    fn try_convert_mut(&mut self, subsec_type: Option<Value>) -> Result<SubsecMultiplier, Self::Error> {
55        let mut subsec_type = match subsec_type {
56            Some(t) => t,
57            None => return Ok(SubsecMultiplier::Micros),
58        };
59
60        let subsec_type_symbol = if let Ruby::Symbol = subsec_type.ruby_type() {
61            unsafe { Symbol::unbox_from_value(&mut subsec_type, self)? }.bytes(self)
62        } else {
63            let mut message = b"unexpected unit: ".to_vec();
64            message.extend_from_slice(subsec_type.inspect(self).as_slice());
65            return Err(ArgumentError::from(message).into());
66        };
67
68        match subsec_type_symbol {
69            b"milliseconds" => Ok(SubsecMultiplier::Millis),
70            b"usec" => Ok(SubsecMultiplier::Micros),
71            b"nsec" => Ok(SubsecMultiplier::Nanos),
72            _ => {
73                let mut message = b"unexpected unit: ".to_vec();
74                message.extend_from_slice(subsec_type_symbol);
75                Err(ArgumentError::from(message).into())
76            }
77        }
78    }
79}
80
81/// A struct that represents the adjustment needed to a `Time` based on a
82/// the parsing of optional Ruby Values. Seconds can require adjustment as a
83/// means for handling overflow of values. e.g. `1_001` millis can be requested
84/// which should result in 1 seconds, and `1_000_000` nanoseconds.
85///
86/// Note: Negative nanoseconds are not supported, thus any negative adjustment
87/// will generally result in at least -1 second, and the relevant positive
88/// amount of nanoseconds. e.g. `-1_000` microseconds should result in -1
89/// second, and `999_999_000` nanoseconds.
90#[derive(Debug, Copy, Clone)]
91pub struct Subsec {
92    secs: i64,
93    nanos: u32,
94}
95
96impl Subsec {
97    /// Returns a tuple of (seconds, nanoseconds). Subseconds are provided in
98    /// various accuracies, and can overflow. e.g. 1001 milliseconds, is 1
99    /// second, and `1_000_000` nanoseconds.
100    #[must_use]
101    pub fn to_tuple(self) -> (i64, u32) {
102        (self.secs, self.nanos)
103    }
104}
105
106impl TryConvertMut<(Option<Value>, Option<Value>), Subsec> for Artichoke {
107    type Error = Error;
108
109    fn try_convert_mut(&mut self, params: (Option<Value>, Option<Value>)) -> Result<Subsec, Self::Error> {
110        let (subsec, subsec_unit) = params;
111
112        let subsec = match subsec {
113            Some(subsec) => subsec,
114            None => return Ok(Subsec { secs: 0, nanos: 0 }),
115        };
116
117        let multiplier: SubsecMultiplier = self.try_convert_mut(subsec_unit)?;
118        let multiplier_nanos = multiplier.as_nanos();
119        // `subsec` represents the user provided value in `subsec_unit`
120        // resolution. The base used to derive the number of seconds is based on
121        // the `subsec_unit`. e.g. `1_001` milliseconds is 1 second, and
122        // `1_000_000` nanoseconds.
123        let seconds_base = NANOS_IN_SECOND / multiplier_nanos;
124
125        if subsec.ruby_type() == Ruby::Float {
126            // FIXME: The below deviates from the MRI implementation of Time MRI
127            // uses `to_r` for subsec calculation on floats subsec nanos, and
128            // this could result in different values.
129
130            let subsec: f64 = self.try_convert(subsec)?;
131
132            if subsec.is_nan() {
133                return Err(FloatDomainError::with_message("NaN").into());
134            }
135            if subsec.is_infinite() {
136                if subsec.is_sign_negative() {
137                    return Err(FloatDomainError::with_message("-Infinity").into());
138                }
139                return Err(FloatDomainError::with_message("Infinity").into());
140            }
141
142            // These conversions are luckily not lossy. `seconds_base` and
143            // `multiplier_nanos` are guaranteed to be represented without loss
144            // in a `f64`.
145            #[expect(
146                clippy::cast_precision_loss,
147                reason = "guaranteed to be represented without loss in a f64"
148            )]
149            let seconds_base = seconds_base as f64;
150            #[expect(
151                clippy::cast_precision_loss,
152                reason = "guaranteed to be represented without loss in a f64"
153            )]
154            let multiplier_nanos = multiplier_nanos as f64;
155
156            let mut secs = subsec / seconds_base;
157            let mut nanos = (subsec % seconds_base) * multiplier_nanos;
158
159            // `is_sign_negative()` is not enough here, since this logic should
160            // also be skilled for negative zero.
161            if subsec < -0.0 {
162                // Nanos always needs to be a positive `u32`. If subsec is
163                // negative, we will always need remove one second.  Nanos can
164                // then be adjusted since it will always be the inverse of the
165                // total nanos in a second.
166                secs -= 1.0;
167                if nanos != 0.0 && nanos != -0.0 {
168                    nanos += NANOS_IN_SECOND_F64;
169                }
170            }
171
172            if !(MIN_FLOAT_SECONDS..=MAX_FLOAT_SECONDS).contains(&secs)
173                || !(MIN_FLOAT_NANOS..=MAX_FLOAT_NANOS).contains(&nanos)
174            {
175                return Err(ArgumentError::with_message("subsec outside of bounds").into());
176            }
177
178            #[expect(
179                clippy::cast_possible_truncation,
180                clippy::cast_sign_loss,
181                reason = "nanos and secs will always be in range due to bounds check."
182            )]
183            Ok(Subsec {
184                secs: secs as i64,
185                nanos: nanos as u32,
186            })
187        } else {
188            let subsec: i64 = implicitly_convert_to_int(self, subsec)?;
189
190            // The below calculations should always be safe. The multiplier is
191            // guaranteed to not be 0, the remainder should never overflow, and
192            // is guaranteed to be less than `u32::MAX`.
193            let mut secs = subsec / seconds_base;
194            let mut nanos = (subsec % seconds_base) * multiplier_nanos;
195
196            if subsec.is_negative() {
197                // Nanos always needs to be a positive `u32`. If subsec is
198                // negative, we will always need remove one second.  Nanos can
199                // then be adjusted since it will always be the inverse of the
200                // total nanos in a second.
201                secs = secs
202                    .checked_sub(1)
203                    .ok_or(ArgumentError::with_message("Time too small"))?;
204
205                if nanos.signum() != 0 {
206                    nanos += NANOS_IN_SECOND;
207                }
208            }
209
210            #[expect(
211                clippy::cast_possible_truncation,
212                clippy::cast_sign_loss,
213                reason = "nanos will always be less than `NANOS_IN_SECOND` which is in u32 range due to modulo and negative adjustments."
214            )]
215            Ok(Subsec {
216                secs,
217                nanos: nanos as u32,
218            })
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use bstr::ByteSlice;
226
227    use super::Subsec;
228    use crate::test::prelude::*;
229
230    fn subsec(interp: &mut Artichoke, params: (Option<&[u8]>, Option<&[u8]>)) -> Result<Subsec, Error> {
231        let (subsec, subsec_type) = params;
232        let subsec = subsec.map(|s| interp.eval(s).unwrap());
233        let subsec_type = subsec_type.map(|s| interp.eval(s).unwrap());
234
235        interp.try_convert_mut((subsec, subsec_type))
236    }
237
238    #[test]
239    fn no_subsec_provided() {
240        let mut interp = interpreter();
241
242        let result: Subsec = interp.try_convert_mut((None, None)).unwrap();
243        let (secs, nanos) = result.to_tuple();
244        assert_eq!(secs, 0);
245        assert_eq!(nanos, 0);
246    }
247
248    #[test]
249    fn no_subsec_provided_but_has_unit() {
250        let mut interp = interpreter();
251        let unit = interp.eval(b":usec").unwrap();
252
253        let result: Subsec = interp.try_convert_mut((None, Some(unit))).unwrap();
254        let (secs, nanos) = result.to_tuple();
255        assert_eq!(secs, 0);
256        assert_eq!(nanos, 0);
257    }
258
259    #[test]
260    fn int_no_unit_implies_micros() {
261        let mut interp = interpreter();
262
263        let expectations = [
264            (b"-1000001".as_slice(), (-2, 999_999_000)),
265            (b"-1000000".as_slice(), (-2, 0)),
266            (b"-999999".as_slice(), (-1, 1_000)),
267            (b"-1".as_slice(), (-1, 999_999_000)),
268            (b"0".as_slice(), (0, 0)),
269            (b"1".as_slice(), (0, 1_000)),
270            (b"999999".as_slice(), (0, 999_999_000)),
271            (b"1000000".as_slice(), (1, 0)),
272            (b"1000001".as_slice(), (1, 1_000)),
273        ];
274
275        let subsec_unit: Option<&[u8]> = None;
276
277        for (input, expectation) in &expectations {
278            let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
279            assert_eq!(
280                result.to_tuple(),
281                *expectation,
282                "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
283                input.as_bstr(),
284                expectation.0,
285                expectation.1
286            );
287        }
288    }
289
290    #[test]
291    fn int_subsec_millis() {
292        let mut interp = interpreter();
293
294        let expectations = [
295            (b"-1001".as_slice(), (-2, 999_000_000)),
296            (b"-1000".as_slice(), (-2, 0)),
297            (b"-999".as_slice(), (-1, 1_000_000)),
298            (b"-1".as_slice(), (-1, 999_000_000)),
299            (b"0".as_slice(), (0, 0)),
300            (b"1".as_slice(), (0, 1_000_000)),
301            (b"999".as_slice(), (0, 999_000_000)),
302            (b"1000".as_slice(), (1, 0)),
303            (b"1001".as_slice(), (1, 1_000_000)),
304        ];
305
306        let subsec_unit: &[u8] = b":milliseconds";
307
308        for (input, expectation) in &expectations {
309            let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
310            assert_eq!(
311                result.to_tuple(),
312                *expectation,
313                "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
314                input.as_bstr(),
315                subsec_unit.as_bstr(),
316                expectation.0,
317                expectation.1
318            );
319        }
320    }
321
322    #[test]
323    fn int_subsec_micros() {
324        let mut interp = interpreter();
325
326        let expectations = [
327            (b"-1000001".as_slice(), (-2, 999_999_000)),
328            (b"-1000000".as_slice(), (-2, 0)),
329            (b"-999999".as_slice(), (-1, 1_000)),
330            (b"-1".as_slice(), (-1, 999_999_000)),
331            (b"0".as_slice(), (0, 0)),
332            (b"1".as_slice(), (0, 1_000)),
333            (b"999999".as_slice(), (0, 999_999_000)),
334            (b"1000000".as_slice(), (1, 0)),
335            (b"1000001".as_slice(), (1, 1_000)),
336        ];
337
338        let subsec_unit: &[u8] = b":usec";
339
340        for (input, expectation) in &expectations {
341            let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
342            assert_eq!(
343                result.to_tuple(),
344                *expectation,
345                "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
346                input.as_bstr(),
347                subsec_unit.as_bstr(),
348                expectation.0,
349                expectation.1
350            );
351        }
352    }
353
354    #[test]
355    fn int_subsec_nanos() {
356        let mut interp = interpreter();
357
358        let expectations = [
359            (b"-1000000001".as_slice(), (-2, 999_999_999)),
360            (b"-1000000000".as_slice(), (-2, 0)),
361            (b"-999999999".as_slice(), (-1, 1)),
362            (b"-1".as_slice(), (-1, 999_999_999)),
363            (b"0".as_slice(), (0, 0)),
364            (b"1".as_slice(), (0, 1)),
365            (b"999999999".as_slice(), (0, 999_999_999)),
366            (b"1000000000".as_slice(), (1, 0)),
367            (b"1000000001".as_slice(), (1, 1)),
368        ];
369
370        let subsec_unit: &[u8] = b":nsec";
371
372        for (input, expectation) in &expectations {
373            let result = subsec(&mut interp, (Some(input), Some(subsec_unit))).unwrap();
374            assert_eq!(
375                result.to_tuple(),
376                *expectation,
377                "Expected TryConvertMut<(Some({}), Some({})), Result<Subsec>>, to return {} secs, {} nanos",
378                input.as_bstr(),
379                subsec_unit.as_bstr(),
380                expectation.0,
381                expectation.1
382            );
383        }
384    }
385
386    #[test]
387    fn float_no_unit_implies_micros() {
388        let mut interp = interpreter();
389
390        let expectations = [
391            // Numbers in and around 0.
392            (b"-1000000.5".as_slice(), (-2, 999_999_500)),
393            (b"-1000000.0".as_slice(), (-2, 0)),
394            (b"-999999.5".as_slice(), (-1, 500)),
395            (b"-999999.0".as_slice(), (-1, 1_000)),
396            (b"-1000.5".as_slice(), (-1, 998_999_500)),
397            (b"-1.5".as_slice(), (-1, 999_998_500)),
398            (b"-1.0".as_slice(), (-1, 999_999_000)),
399            (b"-0.0".as_slice(), (0, 0)),
400            (b"0.0".as_slice(), (0, 0)),
401            (b"1.0".as_slice(), (0, 1_000)),
402            (b"1.5".as_slice(), (0, 1_500)),
403            (b"1000.5".as_slice(), (0, 1_000_500)),
404            (b"999999.0".as_slice(), (0, 999_999_000)),
405            (b"999999.5".as_slice(), (0, 999_999_500)),
406            (b"1000000.0".as_slice(), (1, 0)),
407            (b"1000000.5".as_slice(), (1, 500)),
408            (b"1000001.0".as_slice(), (1, 1000)),
409            // Nanosecond and below (truncates, does not round).
410            (b"0.123".as_slice(), (0, 123)),
411            (b"0.001".as_slice(), (0, 1)),
412            (b"0.0001".as_slice(), (0, 0)),
413            (b"0.0009".as_slice(), (0, 0)),
414        ];
415
416        let subsec_unit: Option<&[u8]> = None;
417
418        for (input, expectation) in &expectations {
419            let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
420            assert_eq!(
421                result.to_tuple(),
422                *expectation,
423                "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
424                input.as_bstr(),
425                expectation.0,
426                expectation.1
427            );
428        }
429    }
430
431    #[test]
432    fn float_subsec_millis() {
433        let mut interp = interpreter();
434
435        let expectations = [
436            // Numbers in and around 0.
437            (b"-1000.5".as_slice(), (-2, 999_500_000)),
438            (b"-1000.0".as_slice(), (-2, 0)),
439            (b"-999.5".as_slice(), (-1, 500_000)),
440            (b"-999.0".as_slice(), (-1, 1_000_000)),
441            (b"-1.5".as_slice(), (-1, 998_500_000)),
442            (b"-1.0".as_slice(), (-1, 999_000_000)),
443            (b"-0.0".as_slice(), (0, 0)),
444            (b"0.0".as_slice(), (0, 0)),
445            (b"1.0".as_slice(), (0, 1_000_000)),
446            (b"1.5".as_slice(), (0, 1_500_000)),
447            (b"999.0".as_slice(), (0, 999_000_000)),
448            (b"999.5".as_slice(), (0, 999_500_000)),
449            (b"1000.0".as_slice(), (1, 0)),
450            (b"1000.5".as_slice(), (1, 500_000)),
451            (b"1001.0".as_slice(), (1, 1_000_000)),
452            // Nanosecond and below (truncates, does not round).
453            (b"0.123456".as_slice(), (0, 123_456)),
454            (b"0.000001".as_slice(), (0, 1)),
455            (b"0.0000001".as_slice(), (0, 0)),
456            (b"0.0000009".as_slice(), (0, 0)),
457        ];
458
459        let subsec_unit: Option<&[u8]> = Some(b":milliseconds");
460
461        for (input, expectation) in &expectations {
462            let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
463            assert_eq!(
464                result.to_tuple(),
465                *expectation,
466                "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
467                input.as_bstr(),
468                expectation.0,
469                expectation.1
470            );
471        }
472    }
473
474    #[test]
475    fn float_subsec_micros() {
476        let mut interp = interpreter();
477
478        let expectations = [
479            // Numbers in and around 0.
480            (b"-1000000.5".as_slice(), (-2, 999_999_500)),
481            (b"-1000000.0".as_slice(), (-2, 0)),
482            (b"-999999.5".as_slice(), (-1, 500)),
483            (b"-999999.0".as_slice(), (-1, 1_000)),
484            (b"-1000.5".as_slice(), (-1, 998_999_500)),
485            (b"-1.5".as_slice(), (-1, 999_998_500)),
486            (b"-1.0".as_slice(), (-1, 999_999_000)),
487            (b"-0.0".as_slice(), (0, 0)),
488            (b"0.0".as_slice(), (0, 0)),
489            (b"1.0".as_slice(), (0, 1_000)),
490            (b"1.5".as_slice(), (0, 1_500)),
491            (b"1000.5".as_slice(), (0, 1_000_500)),
492            (b"999999.0".as_slice(), (0, 999_999_000)),
493            (b"999999.5".as_slice(), (0, 999_999_500)),
494            (b"1000000.0".as_slice(), (1, 0)),
495            (b"1000000.5".as_slice(), (1, 500)),
496            (b"1000001.0".as_slice(), (1, 1000)),
497            // Nanosecond and below (truncates, does not round).
498            (b"0.123".as_slice(), (0, 123)),
499            (b"0.001".as_slice(), (0, 1)),
500            (b"0.0001".as_slice(), (0, 0)),
501            (b"0.0009".as_slice(), (0, 0)),
502        ];
503
504        let subsec_unit: Option<&[u8]> = Some(b":usec");
505
506        for (input, expectation) in &expectations {
507            let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
508            assert_eq!(
509                result.to_tuple(),
510                *expectation,
511                "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
512                input.as_bstr(),
513                expectation.0,
514                expectation.1
515            );
516        }
517    }
518
519    #[test]
520    fn float_subsec_nanos() {
521        let mut interp = interpreter();
522
523        let expectations = [
524            // Numbers in and around 0.
525            (b"-1000000000.5".as_slice(), (-2, 999_999_999)),
526            (b"-1000000000.0".as_slice(), (-2, 0)),
527            (b"-999999999.5".as_slice(), (-1, 0)),
528            (b"-999999999.0".as_slice(), (-1, 1)),
529            (b"-1000.5".as_slice(), (-1, 999_998_999)),
530            (b"-1.5".as_slice(), (-1, 999_999_998)),
531            (b"-1.0".as_slice(), (-1, 999_999_999)),
532            (b"-0.0".as_slice(), (0, 0)),
533            (b"0.0".as_slice(), (0, 0)),
534            (b"1.0".as_slice(), (0, 1)),
535            (b"1.5".as_slice(), (0, 1)),
536            (b"1000.5".as_slice(), (0, 1_000)),
537            (b"999999999.0".as_slice(), (0, 999_999_999)),
538            (b"999999999.5".as_slice(), (0, 999_999_999)),
539            (b"1000000000.0".as_slice(), (1, 0)),
540            (b"1000000000.5".as_slice(), (1, 0)),
541            (b"1000000001.0".as_slice(), (1, 1)),
542            // Nanosecond and below (truncates, does not round).
543            (b"-0.1".as_slice(), (-1, 999_999_999)),
544            (b"0.1".as_slice(), (0, 0)),
545        ];
546
547        let subsec_unit: Option<&[u8]> = Some(b":nsec");
548
549        for (input, expectation) in &expectations {
550            let result = subsec(&mut interp, (Some(input), subsec_unit)).unwrap();
551            assert_eq!(
552                result.to_tuple(),
553                *expectation,
554                "Expected TryConvertMut<(Some({}), None), Result<Subsec>>, to return {} secs, {} nanos",
555                input.as_bstr(),
556                expectation.0,
557                expectation.1
558            );
559        }
560    }
561
562    #[test]
563    fn float_nan_raises() {
564        let mut interp = interpreter();
565
566        let err = subsec(&mut interp, (Some(b"Float::NAN"), None)).unwrap_err();
567
568        assert_eq!(err.name(), "FloatDomainError");
569        assert_eq!(err.message(), b"NaN".as_slice());
570    }
571
572    #[test]
573    fn float_infinite_raises() {
574        let mut interp = interpreter();
575
576        let err = subsec(&mut interp, (Some(b"Float::INFINITY"), None)).unwrap_err();
577
578        assert_eq!(err.name(), "FloatDomainError");
579        assert_eq!(err.message().as_bstr(), b"Infinity".as_bstr());
580
581        let err = subsec(&mut interp, (Some(b"-Float::INFINITY"), None)).unwrap_err();
582
583        assert_eq!(err.name(), "FloatDomainError");
584        assert_eq!(err.message().as_bstr(), b"-Infinity".as_bstr());
585    }
586
587    #[test]
588    fn invalid_subsec_unit() {
589        let mut interp = interpreter();
590
591        let err = subsec(&mut interp, (Some(b"1"), Some(b":bad_unit"))).unwrap_err();
592
593        assert_eq!(err.name(), "ArgumentError");
594        assert_eq!(err.message().as_bstr(), b"unexpected unit: bad_unit".as_bstr());
595    }
596
597    #[test]
598    fn subsec_unit_non_symbol() {
599        let mut interp = interpreter();
600
601        let err = subsec(&mut interp, (Some(b"1"), Some(b":bad_unit"))).unwrap_err();
602
603        assert_eq!(err.name(), "ArgumentError");
604        assert_eq!(err.message().as_bstr(), b"unexpected unit: bad_unit".as_bstr());
605
606        let err = subsec(&mut interp, (Some(b"1"), Some(b"1"))).unwrap_err();
607
608        assert_eq!(err.name(), "ArgumentError");
609        assert_eq!(err.message().as_bstr(), b"unexpected unit: 1".as_bstr());
610
611        let err = subsec(&mut interp, (Some(b"1"), Some(b"Object.new"))).unwrap_err();
612
613        assert_eq!(err.name(), "ArgumentError");
614        assert!(
615            err.message()
616                .as_bstr()
617                .starts_with(b"unexpected unit: #<Object:".as_bstr())
618        );
619    }
620
621    #[test]
622    fn subsec_unit_requires_explicit_symbol() {
623        let mut interp = interpreter();
624
625        let err = subsec(
626            &mut interp,
627            (Some(b"1"), Some(b"class A; def to_sym; :usec; end; end && A.new")),
628        )
629        .unwrap_err();
630
631        assert_eq!(err.name(), "ArgumentError");
632        assert!(err.message().as_bstr().starts_with(b"unexpected unit: #<A:".as_bstr()));
633    }
634}