1use proc_macro::TokenStream;
33use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
34use quote::quote;
35use syn::{Expr, ExprLit, ExprUnary, Lit, Result, UnOp};
36
37#[derive(Clone, Copy)]
43struct Width {
44 name: &'static str,
47 max_scale: u32,
50 type_leaf: &'static str,
54 storage_path: &'static str,
61 wide: bool,
64}
65
66fn crate_root() -> proc_macro2::TokenStream {
79 use proc_macro_crate::{FoundCrate, crate_name};
80 use quote::quote;
81 if manifest_has_unrenamed_dep() {
89 return quote! { ::decimal_scaled };
90 }
91 match crate_name("decimal-scaled") {
92 Ok(FoundCrate::Itself) => quote! { ::decimal_scaled },
93 Ok(FoundCrate::Name(name)) => {
94 let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
95 quote! { ::#ident }
96 }
97 Err(_) => quote! { ::decimal_scaled },
98 }
99}
100
101fn manifest_has_unrenamed_dep() -> bool {
107 let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") else {
108 return false;
109 };
110 let Ok(text) = std::fs::read_to_string(std::path::Path::new(&dir).join("Cargo.toml")) else {
111 return false;
112 };
113 let Ok(doc) = text.parse::<toml_edit::DocumentMut>() else {
114 return false;
115 };
116 ["dependencies", "dev-dependencies", "build-dependencies"]
117 .iter()
118 .any(|table| {
119 doc.get(table)
120 .and_then(|t| t.get("decimal-scaled"))
121 .is_some_and(|dep| {
122 dep.get("package")
123 .and_then(|p| p.as_str())
124 .is_none_or(|p| p == "decimal-scaled")
125 })
126 })
127}
128
129fn type_path(width: Width) -> proc_macro2::TokenStream {
131 let root = crate_root();
132 let leaf = proc_macro2::Ident::new(width.type_leaf, proc_macro2::Span::call_site());
133 quote::quote! { #root :: #leaf }
134}
135
136fn storage_path_tokens(width: Width) -> proc_macro2::TokenStream {
140 if width.wide {
141 let root = crate_root();
142 let leaf: proc_macro2::TokenStream = width
146 .storage_path
147 .parse()
148 .expect("storage_path is a valid type path");
149 quote::quote! { #root :: #leaf }
150 } else {
151 width.storage_path.parse().unwrap()
152 }
153}
154
155const D18: Width = Width {
156 name: "d18",
157 max_scale: 17,
158 type_leaf: "D18",
159 storage_path: "Int::<1>",
161 wide: true,
162};
163const D38: Width = Width {
164 name: "d38",
165 max_scale: 37,
166 type_leaf: "D38",
167 storage_path: "Int::<2>",
170 wide: true,
171};
172const D76: Width = Width {
173 name: "d76",
174 max_scale: 75,
175 type_leaf: "D76",
176 storage_path: "Int::<4>",
177 wide: true,
178};
179const D153: Width = Width {
180 name: "d153",
181 max_scale: 152,
182 type_leaf: "D153",
183 storage_path: "Int::<8>",
184 wide: true,
185};
186const D307: Width = Width {
187 name: "d307",
188 max_scale: 306,
189 type_leaf: "D307",
190 storage_path: "Int::<16>",
191 wide: true,
192};
193const D57: Width = Width {
194 name: "d57",
195 max_scale: 56,
196 type_leaf: "D57",
197 storage_path: "Int::<3>",
198 wide: true,
199};
200const D115: Width = Width {
201 name: "d115",
202 max_scale: 114,
203 type_leaf: "D115",
204 storage_path: "Int::<6>",
205 wide: true,
206};
207const D230: Width = Width {
208 name: "d230",
209 max_scale: 229,
210 type_leaf: "D230",
211 storage_path: "Int::<12>",
212 wide: true,
213};
214const D462: Width = Width {
215 name: "d462",
216 max_scale: 461,
217 type_leaf: "D462",
218 storage_path: "Int::<24>",
219 wide: true,
220};
221const D616: Width = Width {
222 name: "d616",
223 max_scale: 615,
224 type_leaf: "D616",
225 storage_path: "Int::<32>",
226 wide: true,
227};
228const D924: Width = Width {
229 name: "d924",
230 max_scale: 923,
231 type_leaf: "D924",
232 storage_path: "Int::<48>",
233 wide: true,
234};
235const D1232: Width = Width {
236 name: "d1232",
237 max_scale: 1231,
238 type_leaf: "D1232",
239 storage_path: "Int::<64>",
240 wide: true,
241};
242
243#[proc_macro]
248pub fn d18(input: TokenStream) -> TokenStream {
249 expand_for(D18, input)
250}
251
252#[proc_macro]
255pub fn d38(input: TokenStream) -> TokenStream {
256 expand_for(D38, input)
257}
258
259#[proc_macro]
262pub fn d76(input: TokenStream) -> TokenStream {
263 expand_for(D76, input)
264}
265
266#[proc_macro]
269pub fn d153(input: TokenStream) -> TokenStream {
270 expand_for(D153, input)
271}
272
273#[proc_macro]
277pub fn d307(input: TokenStream) -> TokenStream {
278 expand_for(D307, input)
279}
280
281#[proc_macro]
284pub fn d57(input: TokenStream) -> TokenStream {
285 expand_for(D57, input)
286}
287
288#[proc_macro]
291pub fn d115(input: TokenStream) -> TokenStream {
292 expand_for(D115, input)
293}
294
295#[proc_macro]
298pub fn d230(input: TokenStream) -> TokenStream {
299 expand_for(D230, input)
300}
301
302#[proc_macro]
306pub fn d462(input: TokenStream) -> TokenStream {
307 expand_for(D462, input)
308}
309
310#[proc_macro]
314pub fn d616(input: TokenStream) -> TokenStream {
315 expand_for(D616, input)
316}
317
318#[proc_macro]
322pub fn d924(input: TokenStream) -> TokenStream {
323 expand_for(D924, input)
324}
325
326#[proc_macro]
330pub fn d1232(input: TokenStream) -> TokenStream {
331 expand_for(D1232, input)
332}
333
334fn expand_for(width: Width, input: TokenStream) -> TokenStream {
335 let tokens: TokenStream2 = input.into();
340 match parse_invocation(tokens, width) {
341 Ok(inv) => inv.expand(),
342 Err(e) => e.into_compile_error().into(),
343 }
344}
345
346fn parse_invocation(tokens: TokenStream2, width: Width) -> Result<Invocation> {
353 let segments = split_top_commas(tokens);
354 if segments.is_empty() || segments[0].is_empty() {
355 return Err(syn::Error::new(
356 Span::call_site(),
357 format!("{}!() requires a value argument", width.name),
358 ));
359 }
360
361 let mut explicit_radix: Option<(u32, Span)> = None;
365 for seg in &segments[1..] {
366 let Some((TokenTree::Ident(id), TokenTree::Literal(lit))) =
367 seg.first().zip(seg.get(1))
368 else {
369 continue;
370 };
371 if id != "radix" {
372 continue;
373 }
374 if let Ok(r) = lit.to_string().parse::<u32>() {
375 explicit_radix = Some((r, lit.span()));
376 }
377 }
378
379 let value_segment = &segments[0];
383 let value_parse = try_radix_fractional(value_segment, explicit_radix)?;
384
385 if let Some((digits, sign, natural_scale, value_span)) = value_parse {
386 let (scale_qualifier, _radix_q, rounded) = parse_qualifier_segments(&segments[1..], width)?;
390 return Ok(Invocation::Literal(LiteralForm {
391 width,
392 digits,
393 sign,
394 natural_scale,
395 scale_qualifier,
396 rounded,
397 radix_literal: true,
398 value_span,
399 }));
400 }
401
402 let value_ts: TokenStream2 = value_segment.iter().cloned().collect();
405 let value_expr: Expr = syn::parse2(value_ts)?;
406 let value_span = expr_span(&value_expr);
407
408 let (scale_qualifier, radix_qualifier, rounded) =
409 parse_qualifier_segments(&segments[1..], width)?;
410
411 if let Some((sign, raw_str, lit_span)) = try_decimal_literal(&value_expr) {
412 let radix = pick_radix(&raw_str, radix_qualifier, lit_span)?;
413 let (digits, natural_scale) = parse_value_token(&raw_str, lit_span, radix)?;
414 Ok(Invocation::Literal(LiteralForm {
415 width,
416 digits,
417 sign,
418 natural_scale,
419 scale_qualifier,
420 rounded,
421 radix_literal: radix != 10,
422 value_span,
423 }))
424 } else {
425 if let Some((_, radix_span)) = radix_qualifier {
426 return Err(syn::Error::new(
427 radix_span,
428 "`radix` qualifier is only valid with a literal value",
429 ));
430 }
431 let (scale, scale_span) = scale_qualifier.ok_or_else(|| {
432 syn::Error::new(
433 value_span,
434 format!(
435 "scale must be specified for an expression value: `{}!(expr, scale N)`",
436 width.name
437 ),
438 )
439 })?;
440 let _ = rounded;
441 Ok(Invocation::Expression {
442 width,
443 expr: value_expr,
444 scale,
445 scale_span,
446 })
447 }
448}
449
450fn split_top_commas(tokens: TokenStream2) -> Vec<Vec<TokenTree>> {
454 let mut out: Vec<Vec<TokenTree>> = vec![Vec::new()];
455 for tt in tokens {
456 match &tt {
457 TokenTree::Punct(p)
458 if p.as_char() == ',' && p.spacing() == proc_macro2::Spacing::Alone =>
459 {
460 out.push(Vec::new());
461 }
462 _ => out.last_mut().unwrap().push(tt),
463 }
464 }
465 out
466}
467
468fn try_radix_fractional(
475 segment: &[TokenTree],
476 explicit_radix: Option<(u32, Span)>,
477) -> Result<Option<(String, i128, u32, Span)>> {
478 let Some((radix, _radix_span)) = explicit_radix else {
479 return Ok(None);
480 };
481 if radix == 10 {
482 return Ok(None);
483 }
484
485 let mut i = 0;
486 let mut sign: i128 = 1;
487 if let Some(TokenTree::Punct(p)) = segment.first() {
488 if p.as_char() == '-' {
489 sign = -1;
490 i += 1;
491 } else if p.as_char() == '+' {
492 i += 1;
493 }
494 }
495
496 let int_tok = segment.get(i);
505 let dot_tok = segment.get(i + 1);
506 let frac_tok = segment.get(i + 2);
507 let extra = segment.get(i + 3);
508
509 let int_lit = match int_tok {
510 Some(TokenTree::Literal(lit)) => lit.to_string(),
511 Some(TokenTree::Ident(id)) => id.to_string(),
512 _ => return Ok(None),
513 };
514
515 let (int_part, frac_part, span) = if int_lit.contains('.') && dot_tok.is_none() {
516 let span = match int_tok.unwrap() {
518 TokenTree::Literal(lit) => lit.span(),
519 _ => Span::call_site(),
520 };
521 let (head, tail) = int_lit.split_once('.').unwrap();
522 (head.to_string(), tail.to_string(), span)
523 } else {
524 match (dot_tok, frac_tok, extra) {
525 (Some(TokenTree::Punct(p)), Some(frac), None) if p.as_char() == '.' => {
526 let frac_str = match frac {
527 TokenTree::Literal(lit) => lit.to_string(),
528 TokenTree::Ident(id) => id.to_string(),
529 _ => return Ok(None),
530 };
531 let span = match int_tok.unwrap() {
532 TokenTree::Literal(lit) => lit.span(),
533 TokenTree::Ident(id) => id.span(),
534 _ => Span::call_site(),
535 };
536 (int_lit, frac_str, span)
537 }
538 (None, None, _) => {
539 let span = match int_tok.unwrap() {
540 TokenTree::Literal(lit) => lit.span(),
541 TokenTree::Ident(id) => id.span(),
542 _ => Span::call_site(),
543 };
544 (int_lit, String::new(), span)
545 }
546 _ => return Ok(None),
547 }
548 };
549
550 let cleaned_int = if let Some((prefix_r, rest)) = strip_radix_prefix(&int_part) {
554 if prefix_r != radix {
555 return Err(syn::Error::new(
556 span,
557 format!(
558 "radix qualifier ({radix}) disagrees with integer-part prefix (radix {prefix_r})"
559 ),
560 ));
561 }
562 rest.to_string()
563 } else {
564 int_part
565 };
566
567 let int_cleaned: String = cleaned_int.chars().filter(|c| *c != '_').collect();
568 let frac_cleaned: String = frac_part.chars().filter(|c| *c != '_').collect();
569
570 if int_cleaned.is_empty() {
571 return Err(syn::Error::new(
572 span,
573 "decimal literals require a digit on each side of the dot (write `0.A3` not `.A3`)",
574 ));
575 }
576 for c in int_cleaned.chars().chain(frac_cleaned.chars()) {
577 if !is_radix_digit(c, radix) {
578 return Err(syn::Error::new(
579 span,
580 format!("digit `{c}` not valid for radix {radix}"),
581 ));
582 }
583 }
584
585 let combined = format!("{int_cleaned}{frac_cleaned}");
591 let magnitude = match i128::from_str_radix(&combined, radix) {
592 Ok(v) => v,
593 Err(_) => {
594 return Err(syn::Error::new(
595 span,
596 format!("digit string `{combined}` overflows i128 when parsed in radix {radix}"),
597 ));
598 }
599 };
600 Ok(Some((
601 magnitude.to_string(),
602 sign,
603 frac_cleaned.len() as u32,
604 span,
605 )))
606}
607
608fn is_radix_digit(c: char, radix: u32) -> bool {
609 c.is_digit(radix)
610}
611
612type ParsedQualifiers = (Option<(u32, Span)>, Option<(u32, Span)>, bool);
616
617fn parse_qualifier_segments(
621 segments: &[Vec<TokenTree>],
622 width: Width,
623) -> Result<ParsedQualifiers> {
624 let _ = width;
625 let mut scale: Option<(u32, Span)> = None;
626 let mut radix: Option<(u32, Span)> = None;
627 let mut rounded = false;
628 for seg in segments {
629 if seg.is_empty() {
630 continue;
631 }
632 let TokenTree::Ident(kw) = &seg[0] else {
633 return Err(syn::Error::new(
634 tt_span(&seg[0]),
635 "expected qualifier identifier (scale | radix | rounded)",
636 ));
637 };
638 match kw.to_string().as_str() {
639 "scale" => {
640 let lit = seg.get(1).and_then(|t| match t {
641 TokenTree::Literal(l) => Some(l),
642 _ => None,
643 });
644 let lit = lit.ok_or_else(|| {
645 syn::Error::new(kw.span(), "`scale` requires an integer literal: `scale N`")
646 })?;
647 let n: u32 = lit.to_string().parse().map_err(|_| {
648 syn::Error::new(lit.span(), "scale must be a non-negative integer")
649 })?;
650 if scale.is_some() {
651 return Err(syn::Error::new(kw.span(), "duplicate `scale` qualifier"));
652 }
653 scale = Some((n, lit.span()));
654 }
655 "radix" => {
656 let lit = seg.get(1).and_then(|t| match t {
657 TokenTree::Literal(l) => Some(l),
658 _ => None,
659 });
660 let lit = lit.ok_or_else(|| {
661 syn::Error::new(kw.span(), "`radix` requires an integer literal: `radix N`")
662 })?;
663 let r: u32 = lit.to_string().parse().map_err(|_| {
664 syn::Error::new(lit.span(), "radix must be one of 2, 8, 10, 16")
665 })?;
666 if !matches!(r, 2 | 8 | 10 | 16) {
667 return Err(syn::Error::new(
668 lit.span(),
669 format!("radix must be one of 2, 8, 10, 16 (got {r})"),
670 ));
671 }
672 if radix.is_some() {
673 return Err(syn::Error::new(kw.span(), "duplicate `radix` qualifier"));
674 }
675 radix = Some((r, lit.span()));
676 }
677 "rounded" => {
678 if rounded {
679 return Err(syn::Error::new(kw.span(), "duplicate `rounded` qualifier"));
680 }
681 if seg.len() > 1 {
682 return Err(syn::Error::new(
683 tt_span(&seg[1]),
684 "`rounded` takes no argument",
685 ));
686 }
687 rounded = true;
688 }
689 other => {
690 return Err(syn::Error::new(
691 kw.span(),
692 format!("unknown qualifier `{other}`; expected one of: scale, radix, rounded"),
693 ));
694 }
695 }
696 }
697 Ok((scale, radix, rounded))
698}
699
700fn tt_span(tt: &TokenTree) -> Span {
701 match tt {
702 TokenTree::Group(g) => g.span(),
703 TokenTree::Ident(i) => i.span(),
704 TokenTree::Punct(p) => p.span(),
705 TokenTree::Literal(l) => l.span(),
706 }
707}
708
709struct LiteralForm {
715 width: Width,
716 digits: String,
719 sign: i128,
721 natural_scale: u32,
722 scale_qualifier: Option<(u32, Span)>,
723 rounded: bool,
724 radix_literal: bool,
729 value_span: Span,
730}
731
732enum Invocation {
733 Literal(LiteralForm),
734 Expression {
735 width: Width,
736 expr: Expr,
737 scale: u32,
738 scale_span: Span,
739 },
740}
741
742impl Invocation {
743 fn expand(self) -> TokenStream {
744 match self {
745 Invocation::Literal(form) => expand_literal(form),
746 Invocation::Expression {
747 width,
748 expr,
749 scale,
750 scale_span,
751 } => expand_expression(width, expr, scale, scale_span),
752 }
753 }
754}
755
756fn pick_radix(raw: &str, qualifier: Option<(u32, Span)>, _span: Span) -> Result<u32> {
762 let prefix_radix = [("0x", "0X", 16u32), ("0o", "0O", 8), ("0b", "0B", 2)]
763 .into_iter()
764 .find_map(|(lower, upper, radix)| {
765 raw.strip_prefix(lower)
766 .or_else(|| raw.strip_prefix(upper))
767 .map(|stripped| (radix, stripped))
768 });
769 match (prefix_radix, qualifier) {
770 (None, None) => Ok(10),
771 (None, Some((r, _))) => Ok(r),
772 (Some((p, _)), None) => Ok(p),
773 (Some((p, _)), Some((r, _sp))) if p == r => Ok(r),
774 (Some((p, _)), Some((r, sp))) => Err(syn::Error::new(
775 sp,
776 format!("radix qualifier ({r}) disagrees with literal prefix (radix {p})"),
777 )),
778 }
779}
780
781fn expand_literal(form: LiteralForm) -> TokenStream {
784 let LiteralForm {
785 width,
786 digits,
787 sign,
788 natural_scale,
789 scale_qualifier,
790 rounded,
791 radix_literal,
792 value_span,
793 } = form;
794 let (target_scale, scale_span) = match scale_qualifier {
795 Some((n, sp)) => (n, sp),
796 None => (natural_scale, value_span),
797 };
798
799 if target_scale > width.max_scale {
800 return error(
801 scale_span,
802 format!(
803 "scale {target_scale} exceeds max for {} (max = {})",
804 width.name.to_uppercase(),
805 width.max_scale
806 ),
807 );
808 }
809
810 if radix_literal {
814 let _ = rounded;
815 if width.wide {
816 return emit_wide(width, target_scale, sign, &digits);
817 } else {
818 return emit_narrow(width, target_scale, sign, &digits, value_span);
819 }
820 }
821
822 let shifted_digits: String;
830 let final_digits: &str;
831
832 if target_scale == natural_scale {
833 final_digits = &digits;
834 } else if target_scale > natural_scale {
835 let pad = target_scale - natural_scale;
836 shifted_digits = pad_with_zeros(&digits, pad as usize);
837 final_digits = &shifted_digits;
838 } else {
839 let shift = natural_scale - target_scale;
840 let padded = if (digits.len() as u32) <= shift {
844 pad_leading_zeros(&digits, (shift + 1) as usize - digits.len())
845 } else {
846 digits.clone()
847 };
848 let split = padded.len() - shift as usize;
849 let (kept, dropped) = padded.split_at(split);
850 let exact = dropped.bytes().all(|b| b == b'0');
851 if !exact && !rounded {
852 return error(
853 value_span,
854 format!(
855 "literal has {natural_scale} fractional digits, target scale {target_scale} would lose precision; pass `rounded` to opt into half-to-even rounding"
856 ),
857 );
858 }
859 if exact {
860 shifted_digits = if kept.is_empty() {
861 "0".to_string()
862 } else {
863 kept.to_string()
864 };
865 } else {
866 shifted_digits = round_half_to_even(kept, dropped, sign < 0);
868 }
869 final_digits = &shifted_digits;
870 }
871
872 if width.wide {
873 emit_wide(width, target_scale, sign, final_digits)
874 } else {
875 emit_narrow(width, target_scale, sign, final_digits, value_span)
876 }
877}
878
879fn emit_narrow(
880 width: Width,
881 target_scale: u32,
882 sign: i128,
883 digits: &str,
884 value_span: Span,
885) -> TokenStream {
886 let magnitude: i128 = match digits.parse::<i128>() {
887 Ok(v) => v,
888 Err(_) => {
889 return error(
890 value_span,
891 format!(
892 "scaled value overflows i128 before narrowing to {}'s storage",
893 width.name.to_uppercase()
894 ),
895 );
896 }
897 };
898 let signed = match magnitude.checked_mul(sign) {
899 Some(v) => v,
900 None => {
901 return error(
902 value_span,
903 "scaled value overflows i128 after applying sign".to_string(),
904 );
905 }
906 };
907 let (min, max): (i128, i128) = match width.storage_path {
909 "i32" => (i32::MIN as i128, i32::MAX as i128),
910 "i64" => (i64::MIN as i128, i64::MAX as i128),
911 "i128" => (i128::MIN, i128::MAX),
912 _ => unreachable!("narrow path called with non-narrow storage"),
913 };
914 if signed < min || signed > max {
915 return error(
916 value_span,
917 format!(
918 "scaled value {signed} overflows {}'s storage ({})",
919 width.name.to_uppercase(),
920 width.storage_path
921 ),
922 );
923 }
924 let bits_tokens: proc_macro2::TokenStream = match width.storage_path {
925 "i32" => {
926 let v = signed as i32;
927 quote! { #v }
928 }
929 "i64" => {
930 let v = signed as i64;
931 quote! { #v }
932 }
933 "i128" => quote! { #signed },
934 _ => unreachable!(),
935 };
936 let tp = type_path(width);
937 let out = quote! {
938 #tp :: <#target_scale> :: from_bits(#bits_tokens)
939 };
940 out.into()
941}
942
943fn emit_wide(width: Width, target_scale: u32, sign: i128, digits: &str) -> TokenStream {
944 let signed_str = if sign < 0 {
945 format!("-{digits}")
946 } else {
947 digits.to_string()
948 };
949 let tp = type_path(width);
950 let sp = storage_path_tokens(width);
951 let err_msg = format!("{}! bits parse failed", width.name);
952 let out = quote! {
953 #tp :: <#target_scale> :: from_bits({
954 const BITS: #sp = match <#sp>::from_str_radix(#signed_str, 10) {
955 ::core::result::Result::Ok(v) => v,
956 ::core::result::Result::Err(_) => panic!(#err_msg),
957 };
958 BITS
959 })
960 };
961 out.into()
962}
963
964fn expand_expression(width: Width, expr: Expr, scale: u32, scale_span: Span) -> TokenStream {
967 if scale > width.max_scale {
968 return error(
969 scale_span,
970 format!(
971 "scale {scale} exceeds max for {} (max = {})",
972 width.name.to_uppercase(),
973 width.max_scale
974 ),
975 );
976 }
977 let tp = type_path(width);
978 let sp = storage_path_tokens(width);
979 let err_msg = format!(
980 "{}! overflow: expression * 10^SCALE exceeds storage range",
981 width.name
982 );
983 let out = if width.wide && (width.storage_path == "Int::<2>" || width.storage_path == "Int::<1>") {
984 let bridged = if width.storage_path == "Int::<1>" {
989 quote! { <#sp as ::core::convert::From<i64>>::from((#expr) as i64) }
990 } else {
991 quote! { <#sp as ::core::convert::TryFrom<i128>>::try_from((#expr) as i128).unwrap() }
992 };
993 quote! {
994 #tp :: <#scale> :: from_bits({
995 let _v: #sp = #bridged;
996 let mult: #sp = <#sp>::from_str_radix("10", 10)
997 .expect("dNN! mult literal")
998 .pow(#scale);
999 _v.checked_mul(mult).expect(#err_msg)
1000 })
1001 }
1002 } else if width.wide {
1003 quote! {
1004 #tp :: <#scale> :: from_bits({
1005 let _v: #sp = (#expr);
1006 let mult: #sp = <#sp>::from_str_radix("10", 10)
1007 .expect("d{}! mult literal")
1008 .pow(#scale);
1009 _v.checked_mul(mult).expect(#err_msg)
1010 })
1011 }
1012 } else if scale == 0 {
1013 quote! {
1014 #tp :: <0> :: from_bits({
1015 let _v: #sp = (#expr);
1016 _v
1017 })
1018 }
1019 } else {
1020 let mult_lit: proc_macro2::TokenStream = match width.storage_path {
1021 "i32" => {
1022 let v = 10i32.pow(scale);
1023 quote! { #v }
1024 }
1025 "i64" => {
1026 let v = 10i64.pow(scale);
1027 quote! { #v }
1028 }
1029 "i128" => {
1030 let v = 10i128.pow(scale);
1031 quote! { #v }
1032 }
1033 _ => unreachable!(),
1034 };
1035 quote! {
1036 #tp :: <#scale> :: from_bits({
1037 let _v: #sp = (#expr);
1038 _v.checked_mul(#mult_lit).expect(#err_msg)
1039 })
1040 }
1041 };
1042 out.into()
1043}
1044
1045fn pad_with_zeros(digits: &str, pad: usize) -> String {
1048 let mut out = String::with_capacity(digits.len() + pad);
1049 out.push_str(digits);
1050 for _ in 0..pad {
1051 out.push('0');
1052 }
1053 out
1054}
1055
1056fn pad_leading_zeros(digits: &str, pad: usize) -> String {
1057 let mut out = String::with_capacity(digits.len() + pad);
1058 for _ in 0..pad {
1059 out.push('0');
1060 }
1061 out.push_str(digits);
1062 out
1063}
1064
1065fn round_half_to_even(kept: &str, dropped: &str, _negative: bool) -> String {
1069 debug_assert!(!dropped.is_empty());
1070 let first = dropped.as_bytes()[0];
1072 let rest_nonzero = dropped.bytes().skip(1).any(|b| b != b'0');
1073 let round_up = match first.cmp(&b'5') {
1074 std::cmp::Ordering::Less => false,
1075 std::cmp::Ordering::Greater => true,
1076 std::cmp::Ordering::Equal => {
1077 if rest_nonzero {
1081 true
1082 } else {
1083 let last_kept = kept.bytes().last().unwrap_or(b'0');
1084 (last_kept - b'0') & 1 == 1
1085 }
1086 }
1087 };
1088 let kept_or_zero = if kept.is_empty() { "0" } else { kept };
1089 if !round_up {
1090 kept_or_zero.to_string()
1091 } else {
1092 add_one_to_digits(kept_or_zero)
1093 }
1094}
1095
1096fn add_one_to_digits(digits: &str) -> String {
1098 let mut bytes: Vec<u8> = digits.as_bytes().to_vec();
1099 let mut i = bytes.len();
1100 let mut carry = 1u8;
1101 while i > 0 && carry > 0 {
1102 i -= 1;
1103 let d = bytes[i] - b'0' + carry;
1104 if d >= 10 {
1105 bytes[i] = b'0';
1106 carry = 1;
1107 } else {
1108 bytes[i] = b'0' + d;
1109 carry = 0;
1110 }
1111 }
1112 if carry > 0 {
1113 bytes.insert(0, b'1');
1114 }
1115 String::from_utf8(bytes).expect("ascii digits")
1116}
1117
1118fn try_decimal_literal(expr: &Expr) -> Option<(i128, String, Span)> {
1121 match expr {
1122 Expr::Lit(ExprLit { lit, .. }) => match lit {
1123 Lit::Float(f) => Some((1, f.to_string(), f.span())),
1124 Lit::Int(i) => Some((1, i.to_string(), i.span())),
1125 _ => None,
1126 },
1127 Expr::Unary(ExprUnary {
1128 op: UnOp::Neg(_),
1129 expr: inner,
1130 ..
1131 }) => {
1132 if let Expr::Lit(ExprLit { lit, .. }) = inner.as_ref() {
1133 match lit {
1134 Lit::Float(f) => Some((-1, f.to_string(), f.span())),
1135 Lit::Int(i) => Some((-1, i.to_string(), i.span())),
1136 _ => None,
1137 }
1138 } else {
1139 None
1140 }
1141 }
1142 _ => None,
1143 }
1144}
1145
1146fn expr_span(expr: &Expr) -> Span {
1147 use syn::spanned::Spanned;
1148 expr.span()
1149}
1150
1151fn parse_value_token(raw: &str, span: Span, radix: u32) -> Result<(String, u32)> {
1163 for (i, c) in raw.char_indices() {
1165 if (c == 'i' || c == 'u')
1166 || (c == 'f'
1167 && i > 0
1168 && !raw[..i].contains('.')
1169 && !raw[..i]
1170 .chars()
1171 .last()
1172 .is_some_and(|x| x.is_ascii_digit()))
1173 {
1174 let _ = i;
1176 }
1177 }
1178 if let Some(idx) = raw.find('f') {
1179 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1181 return Err(syn::Error::new(
1182 span,
1183 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1184 ));
1185 }
1186 }
1187 if let Some(idx) = raw.rfind(['i', 'u']) {
1188 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1192 return Err(syn::Error::new(
1193 span,
1194 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1195 ));
1196 }
1197 }
1198
1199 if radix == 10 {
1200 return parse_decimal_token(raw, span);
1201 }
1202
1203 let digits_part = strip_radix_prefix(raw)
1206 .map(|(p, rest)| {
1207 let _ = p;
1210 rest
1211 })
1212 .unwrap_or(raw);
1213
1214 if digits_part.contains('.') {
1215 return Err(syn::Error::new(
1216 span,
1217 "fractional non-decimal literals are not supported (use an explicit `scale N` with an integer-only digit string instead)",
1218 ));
1219 }
1220
1221 let cleaned: String = digits_part.chars().filter(|c| *c != '_').collect();
1223 if cleaned.is_empty() {
1224 return Err(syn::Error::new(span, "empty digit string"));
1225 }
1226
1227 let magnitude = match i128::from_str_radix(&cleaned, radix) {
1232 Ok(v) => v,
1233 Err(_) => {
1234 return Err(syn::Error::new(
1235 span,
1236 format!("digit string `{cleaned}` is not valid in radix {radix} or overflows i128"),
1237 ));
1238 }
1239 };
1240 Ok((magnitude.to_string(), 0))
1241}
1242
1243fn strip_radix_prefix(raw: &str) -> Option<(u32, &str)> {
1244 if let Some(rest) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
1245 Some((16, rest))
1246 } else if let Some(rest) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
1247 Some((8, rest))
1248 } else if let Some(rest) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
1249 Some((2, rest))
1250 } else {
1251 None
1252 }
1253}
1254
1255fn parse_decimal_token(raw: &str, span: Span) -> Result<(String, u32)> {
1256 let (mantissa, sci_exp) = match raw.find(['e', 'E']) {
1257 Some(idx) => {
1258 let m = &raw[..idx];
1259 let e: i32 = raw[idx + 1..].parse().map_err(|_| {
1260 syn::Error::new(
1261 span,
1262 format!("invalid scientific exponent: `{}`", &raw[idx + 1..]),
1263 )
1264 })?;
1265 (m, e)
1266 }
1267 None => (raw, 0_i32),
1268 };
1269 let (int_part, frac_part) = match mantissa.find('.') {
1270 Some(idx) => {
1271 let int_part = &mantissa[..idx];
1272 let frac_part = &mantissa[idx + 1..];
1273 if int_part.is_empty() {
1274 return Err(syn::Error::new(
1275 span,
1276 "decimal literals require a digit on each side of the dot (write `0.5` not `.5`)",
1277 ));
1278 }
1279 if frac_part.is_empty() {
1280 return Err(syn::Error::new(
1281 span,
1282 "decimal literals require a digit on each side of the dot (write `1.0` not `1.`)",
1283 ));
1284 }
1285 (int_part, frac_part)
1286 }
1287 None => (mantissa, ""),
1288 };
1289 let cleaned_int: String = int_part.chars().filter(|c| *c != '_').collect();
1290 let cleaned_frac: String = frac_part.chars().filter(|c| *c != '_').collect();
1291 for c in cleaned_int.chars().chain(cleaned_frac.chars()) {
1292 if !c.is_ascii_digit() {
1293 return Err(syn::Error::new(
1294 span,
1295 format!("invalid digit `{c}` in decimal literal"),
1296 ));
1297 }
1298 }
1299 let mantissa_scale = cleaned_frac.len() as u32;
1300 let mut digits = cleaned_int;
1301 digits.push_str(&cleaned_frac);
1302 let trimmed = digits.trim_start_matches('0');
1305 let digits = if trimmed.is_empty() {
1306 "0".to_string()
1307 } else {
1308 trimmed.to_string()
1309 };
1310
1311 let signed_natural = (mantissa_scale as i64) - (sci_exp as i64);
1313 if signed_natural >= 0 {
1314 Ok((digits, signed_natural as u32))
1315 } else {
1316 let pad = (-signed_natural) as usize;
1318 Ok((pad_with_zeros(&digits, pad), 0))
1319 }
1320}
1321
1322fn error(span: Span, msg: String) -> TokenStream {
1323 syn::Error::new(span, msg).into_compile_error().into()
1324}