mezzaluna_conversion_methods/
lib.rs1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3 clippy::let_underscore_untyped,
4 reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7 clippy::question_mark,
8 reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13 clippy::unnecessary_lazy_evaluations,
14 reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17 test,
18 allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22 missing_copy_implementations,
23 missing_debug_implementations,
24 missing_docs,
25 rust_2024_compatibility,
26 trivial_casts,
27 trivial_numeric_casts,
28 unused_qualifications,
29 variant_size_differences
30)]
31#![forbid(unsafe_code)]
32#![cfg_attr(docsrs, feature(doc_cfg))]
37#![cfg_attr(docsrs, feature(doc_alias))]
38
39use core::error;
65use core::ffi::CStr;
66use core::fmt;
67use core::hash::BuildHasher;
68use std::sync::OnceLock;
69
70use intaglio::SymbolOverflowError;
71use intaglio::bytes::SymbolTable;
72
73#[cfg(doctest)]
75#[doc = include_str!("../README.md")]
76mod readme {}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ConversionType {
82 Implicit,
84 Coercion,
86}
87
88impl ConversionType {
89 #[inline]
91 #[must_use]
92 pub const fn is_implicit(&self) -> bool {
93 matches!(self, Self::Implicit)
94 }
95
96 #[inline]
98 #[must_use]
99 pub const fn is_coercion(&self) -> bool {
100 matches!(self, Self::Coercion)
101 }
102}
103
104#[rustfmt::skip]
118pub const CONVERSION_METHODS: [(&str, &CStr, ConversionType); 12] = [
119 ("to_int", c"to_int", ConversionType::Implicit),
120 ("to_ary", c"to_ary", ConversionType::Implicit),
121 ("to_str", c"to_str", ConversionType::Implicit),
122 ("to_sym", c"to_sym", ConversionType::Implicit),
123 ("to_hash", c"to_hash", ConversionType::Implicit),
124 ("to_proc", c"to_proc", ConversionType::Implicit),
125 ("to_io", c"to_io", ConversionType::Implicit),
126 ("to_a", c"to_a", ConversionType::Coercion),
127 ("to_s", c"to_s", ConversionType::Coercion),
128 ("to_i", c"to_i", ConversionType::Coercion),
129 ("to_f", c"to_f", ConversionType::Coercion),
130 ("to_r", c"to_r", ConversionType::Coercion),
131];
132
133#[derive(Default, Debug)]
137#[allow(missing_copy_implementations)]
138pub struct InitError {
139 cause: Option<SymbolOverflowError>,
140}
141
142impl fmt::Display for InitError {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(self.message())
145 }
146}
147
148impl error::Error for InitError {
149 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
150 if let Some(ref cause) = self.cause {
151 Some(cause)
152 } else {
153 None
154 }
155 }
156}
157
158impl From<SymbolOverflowError> for InitError {
159 fn from(err: SymbolOverflowError) -> Self {
160 Self { cause: Some(err) }
161 }
162}
163
164impl InitError {
165 #[must_use]
175 pub const fn new() -> Self {
176 Self { cause: None }
177 }
178
179 #[must_use]
190 pub const fn message(&self) -> &'static str {
191 "conversion method table initialization failed"
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct ConvMethod {
203 method: &'static str,
205 cstr: &'static CStr,
207 id: u32,
210 conversion_type: ConversionType,
212}
213
214impl ConvMethod {
215 #[inline]
217 #[must_use]
218 pub fn name(&self) -> &str {
219 self.method
220 }
221
222 #[inline]
224 #[must_use]
225 pub fn cstr(&self) -> &CStr {
226 self.cstr
227 }
228
229 #[inline]
231 #[must_use]
232 pub fn symbol(&self) -> u32 {
233 self.id
234 }
235
236 #[inline]
238 #[must_use]
239 pub const fn is_implicit(&self) -> bool {
240 self.conversion_type.is_implicit()
241 }
242
243 #[inline]
245 #[must_use]
246 pub const fn is_coercion(&self) -> bool {
247 self.conversion_type.is_coercion()
248 }
249}
250
251#[derive(Debug, Default)]
258pub struct ConvMethods {
259 table: OnceLock<[ConvMethod; 12]>,
260}
261
262impl ConvMethods {
263 #[must_use]
272 pub const fn new() -> Self {
273 Self { table: OnceLock::new() }
274 }
275
276 #[must_use]
290 pub fn get(&self) -> Option<&[ConvMethod; 12]> {
291 self.table.get()
292 }
293
294 pub fn get_or_init<'a, S>(&'a self, symbols: &mut SymbolTable<S>) -> Result<&'a [ConvMethod; 12], InitError>
328 where
329 S: BuildHasher,
330 {
331 Ok(self.table.get_or_init(|| {
332 let mut metadata = [ConvMethod {
333 method: "",
334 cstr: c"",
335 id: u32::MAX,
336 conversion_type: ConversionType::Implicit,
337 }; CONVERSION_METHODS.len()];
338
339 for (cell, (method, cstr, conversion_type)) in metadata.iter_mut().zip(CONVERSION_METHODS) {
340 let bytes = cstr.to_bytes_with_nul();
346
347 let sym = symbols
352 .intern(bytes)
353 .expect("interpreter setup requires interning conversion methods");
354
355 *cell = ConvMethod {
356 method,
357 cstr,
358 id: sym.into(),
359 conversion_type,
360 };
361 }
362
363 metadata
364 }))
365 }
366
367 pub fn find_method<S>(&self, symbols: &mut SymbolTable<S>, method: &str) -> Result<Option<ConvMethod>, InitError>
380 where
381 S: BuildHasher,
382 {
383 let table = self.get_or_init(symbols)?;
384 let method = table.iter().find(|conv| conv.method == method).copied();
385 Ok(method)
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use std::{error::Error, ptr};
392
393 use intaglio::bytes::SymbolTable;
394
395 use super::*;
396
397 #[test]
398 fn test_conversion_type_is_implicit() {
399 let conversion = ConversionType::Implicit;
400 assert!(conversion.is_implicit());
401 assert!(!conversion.is_coercion());
402 }
403
404 #[test]
405 fn test_conversion_type_is_coercion() {
406 let conversion = ConversionType::Coercion;
407 assert!(conversion.is_coercion());
408 assert!(!conversion.is_implicit());
409 }
410
411 #[test]
412 fn test_conversion_type_equality() {
413 assert_eq!(ConversionType::Implicit, ConversionType::Implicit);
414 assert_eq!(ConversionType::Coercion, ConversionType::Coercion);
415 assert_ne!(ConversionType::Implicit, ConversionType::Coercion);
416 assert_ne!(ConversionType::Coercion, ConversionType::Implicit);
417 }
418
419 #[test]
420 fn test_conversion_type_debug() {
421 assert_eq!(format!("{:?}", ConversionType::Implicit), "Implicit");
422 assert_eq!(format!("{:?}", ConversionType::Coercion), "Coercion");
423 }
424
425 #[test]
426 fn test_error_default() {
427 let error = InitError::default();
428 assert!(error.cause.is_none());
429 assert!(error.source().is_none());
430 }
431
432 #[test]
433 fn test_error_from_symbol_overflow_error() {
434 let error = SymbolOverflowError::new();
435 let init_error = InitError::from(error);
436 assert!(init_error.cause.is_some());
437 assert!(init_error.source().is_some());
438 }
439
440 #[test]
441 fn test_error_display() {
442 let error = InitError::default();
443 assert_eq!(error.to_string(), "conversion method table initialization failed");
444 }
445
446 #[test]
447 fn test_conv_method_name() {
448 let cstr = c"to_int";
449 let method = ConvMethod {
450 method: "to_int",
451 cstr,
452 id: 1,
453 conversion_type: ConversionType::Implicit,
454 };
455
456 assert_eq!(method.name(), "to_int");
457 }
458
459 #[test]
460 fn test_conv_method_cstr() {
461 let cstr = c"to_str";
462 let method = ConvMethod {
463 method: "to_str",
464 cstr,
465 id: 2,
466 conversion_type: ConversionType::Implicit,
467 };
468
469 assert_eq!(method.cstr().to_str().unwrap(), "to_str");
470 }
471
472 #[test]
473 fn test_conv_method_symbol() {
474 let cstr = c"to_sym";
475 let method = ConvMethod {
476 method: "to_sym",
477 cstr,
478 id: 42,
479 conversion_type: ConversionType::Coercion,
480 };
481
482 assert_eq!(method.symbol(), 42);
483 }
484
485 #[test]
486 fn test_conv_method_is_implicit() {
487 let cstr = c"to_ary";
488 let method = ConvMethod {
489 method: "to_ary",
490 cstr,
491 id: 3,
492 conversion_type: ConversionType::Implicit,
493 };
494
495 assert!(method.is_implicit());
496 assert!(!method.is_coercion());
497 }
498
499 #[test]
500 fn test_conv_method_is_coercion() {
501 let cstr = c"to_i";
502 let method = ConvMethod {
503 method: "to_i",
504 cstr,
505 id: 4,
506 conversion_type: ConversionType::Coercion,
507 };
508
509 assert!(method.is_coercion());
510 assert!(!method.is_implicit());
511 }
512
513 #[test]
514 fn test_get_or_init_populates_table() {
515 let mut symbols = SymbolTable::new();
516 let conv_methods = ConvMethods::new();
517
518 assert!(conv_methods.get().is_none());
520 assert!(conv_methods.table.get().is_none());
521
522 let result = conv_methods.get_or_init(&mut symbols);
524 assert!(result.is_ok());
525 let table = result.unwrap();
526
527 assert_eq!(table.len(), 12);
529 assert!(conv_methods.get().is_some());
530 assert!(conv_methods.table.get().is_some());
531
532 assert_eq!(symbols.len(), 12);
534 }
535
536 #[test]
537 fn test_find_method_existing_method() {
538 let mut symbols = SymbolTable::new();
539 let conv_methods = ConvMethods::new();
540
541 assert!(conv_methods.get_or_init(&mut symbols).is_ok());
543
544 let result = conv_methods.find_method(&mut symbols, "to_int");
546 assert!(result.is_ok());
547 let method = result.unwrap();
548 assert!(method.is_some());
549 assert_eq!(method.unwrap().method, "to_int");
550 }
551
552 #[test]
553 fn test_find_method_nonexistent_method() {
554 let mut symbols = SymbolTable::new();
555 let conv_methods = ConvMethods::new();
556
557 assert!(conv_methods.get_or_init(&mut symbols).is_ok());
559
560 let result = conv_methods.find_method(&mut symbols, "nonexistent_method");
562 assert!(result.is_ok());
563 assert!(result.unwrap().is_none());
564 }
565
566 #[test]
567 fn test_get_or_init_idempotent() {
568 let mut symbols = SymbolTable::new();
569 let conv_methods = ConvMethods::new();
570
571 let result1 = conv_methods.get_or_init(&mut symbols);
573 assert!(result1.is_ok());
574 let table1 = result1.unwrap();
575
576 let result2 = conv_methods.get_or_init(&mut symbols);
578 assert!(result2.is_ok());
579 let table2 = result2.unwrap();
580
581 assert!(ptr::eq(table1, table2));
583 }
584
585 #[test]
586 fn seven_implicit_conversions() {
587 let mut symbols = SymbolTable::new();
588 let conv_methods = ConvMethods::new();
589 let table = conv_methods.get_or_init(&mut symbols).unwrap();
590 let mut iter = table.iter();
591
592 for conv in iter.by_ref().take(7) {
593 assert!(conv.is_implicit(), "{} should be implicit conversion", conv.method);
594 assert_eq!(conv.conversion_type, ConversionType::Implicit);
595 }
596
597 for conv in iter {
598 assert!(conv.is_coercion(), "{} should be coercion", conv.method);
599 assert_eq!(conv.conversion_type, ConversionType::Coercion);
600 }
601 }
602
603 #[test]
604 fn implicit_conversions_setup() {
605 let mut symbols = SymbolTable::new();
606 let conv_methods = ConvMethods::new();
607
608 for method in ["to_int", "to_ary", "to_str", "to_sym", "to_hash", "to_proc", "to_io"] {
609 let conv = conv_methods.find_method(&mut symbols, method).unwrap();
610 let Some(conv) = conv else {
611 panic!("conversion method {method} should be found");
612 };
613 assert!(conv.is_implicit(), "{method} should be implicit conversion");
614 }
615 }
616
617 #[test]
618 fn coercion_conversions_setup() {
619 let mut symbols = SymbolTable::new();
620 let conv_methods = ConvMethods::new();
621
622 for method in ["to_i", "to_s", "to_a", "to_f", "to_r"] {
623 let conv = conv_methods.find_method(&mut symbols, method).unwrap();
624 let Some(conv) = conv else {
625 panic!("conversion method {method} should be found");
626 };
627 assert!(conv.is_coercion(), "{method} should be coercion");
628 }
629 }
630
631 #[test]
632 fn array_is_fully_initialized() {
633 let mut symbols = SymbolTable::new();
634 let conv_methods = ConvMethods::new();
635 let table = conv_methods.get_or_init(&mut symbols).unwrap();
636 for conv in table {
637 assert!(!conv.method.is_empty());
638 assert!(!conv.cstr.to_bytes().is_empty());
639 assert_ne!(conv.id, u32::MAX);
640 }
641 }
642}