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 = if target_scale == natural_scale {
831 &digits
832 } else if target_scale > natural_scale {
833 let pad = target_scale - natural_scale;
834 shifted_digits = pad_with_zeros(&digits, pad as usize);
835 &shifted_digits
836 } else {
837 let shift = natural_scale - target_scale;
838 let padded = if (digits.len() as u32) <= shift {
842 pad_leading_zeros(&digits, (shift + 1) as usize - digits.len())
843 } else {
844 digits.clone()
845 };
846 let split = padded.len() - shift as usize;
847 let (kept, dropped) = padded.split_at(split);
848 let exact = dropped.bytes().all(|b| b == b'0');
849 if !exact && !rounded {
850 return error(
851 value_span,
852 format!(
853 "literal has {natural_scale} fractional digits, target scale {target_scale} would lose precision; pass `rounded` to opt into half-to-even rounding"
854 ),
855 );
856 }
857 if exact {
858 shifted_digits = if kept.is_empty() {
859 "0".to_string()
860 } else {
861 kept.to_string()
862 };
863 } else {
864 shifted_digits = round_half_to_even(kept, dropped, sign < 0);
866 }
867 &shifted_digits
868 };
869
870 if width.wide {
871 emit_wide(width, target_scale, sign, final_digits)
872 } else {
873 emit_narrow(width, target_scale, sign, final_digits, value_span)
874 }
875}
876
877fn emit_narrow(
878 width: Width,
879 target_scale: u32,
880 sign: i128,
881 digits: &str,
882 value_span: Span,
883) -> TokenStream {
884 let magnitude: i128 = match digits.parse::<i128>() {
885 Ok(v) => v,
886 Err(_) => {
887 return error(
888 value_span,
889 format!(
890 "scaled value overflows i128 before narrowing to {}'s storage",
891 width.name.to_uppercase()
892 ),
893 );
894 }
895 };
896 let signed = match magnitude.checked_mul(sign) {
897 Some(v) => v,
898 None => {
899 return error(
900 value_span,
901 "scaled value overflows i128 after applying sign".to_string(),
902 );
903 }
904 };
905 let (min, max): (i128, i128) = match width.storage_path {
907 "i32" => (i32::MIN as i128, i32::MAX as i128),
908 "i64" => (i64::MIN as i128, i64::MAX as i128),
909 "i128" => (i128::MIN, i128::MAX),
910 _ => unreachable!("narrow path called with non-narrow storage"),
911 };
912 if signed < min || signed > max {
913 return error(
914 value_span,
915 format!(
916 "scaled value {signed} overflows {}'s storage ({})",
917 width.name.to_uppercase(),
918 width.storage_path
919 ),
920 );
921 }
922 let bits_tokens: proc_macro2::TokenStream = match width.storage_path {
923 "i32" => {
924 let v = signed as i32;
925 quote! { #v }
926 }
927 "i64" => {
928 let v = signed as i64;
929 quote! { #v }
930 }
931 "i128" => quote! { #signed },
932 _ => unreachable!(),
933 };
934 let tp = type_path(width);
935 let out = quote! {
936 #tp :: <#target_scale> :: from_bits(#bits_tokens)
937 };
938 out.into()
939}
940
941fn emit_wide(width: Width, target_scale: u32, sign: i128, digits: &str) -> TokenStream {
942 let signed_str = if sign < 0 {
943 format!("-{digits}")
944 } else {
945 digits.to_string()
946 };
947 let tp = type_path(width);
948 let sp = storage_path_tokens(width);
949 let err_msg = format!("{}! bits parse failed", width.name);
950 let out = quote! {
951 #tp :: <#target_scale> :: from_bits({
952 const BITS: #sp = match <#sp>::from_str_radix(#signed_str, 10) {
953 ::core::result::Result::Ok(v) => v,
954 ::core::result::Result::Err(_) => panic!(#err_msg),
955 };
956 BITS
957 })
958 };
959 out.into()
960}
961
962fn expand_expression(width: Width, expr: Expr, scale: u32, scale_span: Span) -> TokenStream {
965 if scale > width.max_scale {
966 return error(
967 scale_span,
968 format!(
969 "scale {scale} exceeds max for {} (max = {})",
970 width.name.to_uppercase(),
971 width.max_scale
972 ),
973 );
974 }
975 let tp = type_path(width);
976 let sp = storage_path_tokens(width);
977 let err_msg = format!(
978 "{}! overflow: expression * 10^SCALE exceeds storage range",
979 width.name
980 );
981 let out = if width.wide && (width.storage_path == "Int::<2>" || width.storage_path == "Int::<1>") {
982 let bridged = if width.storage_path == "Int::<1>" {
987 quote! { <#sp as ::core::convert::From<i64>>::from((#expr) as i64) }
988 } else {
989 quote! { <#sp as ::core::convert::TryFrom<i128>>::try_from((#expr) as i128).unwrap() }
990 };
991 quote! {
992 #tp :: <#scale> :: from_bits({
993 let _v: #sp = #bridged;
994 let mult: #sp = <#sp>::from_str_radix("10", 10)
995 .expect("dNN! mult literal")
996 .pow(#scale);
997 _v.checked_mul(mult).expect(#err_msg)
998 })
999 }
1000 } else if width.wide {
1001 quote! {
1002 #tp :: <#scale> :: from_bits({
1003 let _v: #sp = (#expr);
1004 let mult: #sp = <#sp>::from_str_radix("10", 10)
1005 .expect("d{}! mult literal")
1006 .pow(#scale);
1007 _v.checked_mul(mult).expect(#err_msg)
1008 })
1009 }
1010 } else if scale == 0 {
1011 quote! {
1012 #tp :: <0> :: from_bits({
1013 let _v: #sp = (#expr);
1014 _v
1015 })
1016 }
1017 } else {
1018 let mult_lit: proc_macro2::TokenStream = match width.storage_path {
1019 "i32" => {
1020 let v = 10i32.pow(scale);
1021 quote! { #v }
1022 }
1023 "i64" => {
1024 let v = 10i64.pow(scale);
1025 quote! { #v }
1026 }
1027 "i128" => {
1028 let v = 10i128.pow(scale);
1029 quote! { #v }
1030 }
1031 _ => unreachable!(),
1032 };
1033 quote! {
1034 #tp :: <#scale> :: from_bits({
1035 let _v: #sp = (#expr);
1036 _v.checked_mul(#mult_lit).expect(#err_msg)
1037 })
1038 }
1039 };
1040 out.into()
1041}
1042
1043fn pad_with_zeros(digits: &str, pad: usize) -> String {
1046 let mut out = String::with_capacity(digits.len() + pad);
1047 out.push_str(digits);
1048 for _ in 0..pad {
1049 out.push('0');
1050 }
1051 out
1052}
1053
1054fn pad_leading_zeros(digits: &str, pad: usize) -> String {
1055 let mut out = String::with_capacity(digits.len() + pad);
1056 for _ in 0..pad {
1057 out.push('0');
1058 }
1059 out.push_str(digits);
1060 out
1061}
1062
1063fn round_half_to_even(kept: &str, dropped: &str, _negative: bool) -> String {
1067 debug_assert!(!dropped.is_empty());
1068 let first = dropped.as_bytes()[0];
1070 let rest_nonzero = dropped.bytes().skip(1).any(|b| b != b'0');
1071 let round_up = match first.cmp(&b'5') {
1072 std::cmp::Ordering::Less => false,
1073 std::cmp::Ordering::Greater => true,
1074 std::cmp::Ordering::Equal => {
1075 if rest_nonzero {
1079 true
1080 } else {
1081 let last_kept = kept.bytes().last().unwrap_or(b'0');
1082 (last_kept - b'0') & 1 == 1
1083 }
1084 }
1085 };
1086 let kept_or_zero = if kept.is_empty() { "0" } else { kept };
1087 if !round_up {
1088 kept_or_zero.to_string()
1089 } else {
1090 add_one_to_digits(kept_or_zero)
1091 }
1092}
1093
1094fn add_one_to_digits(digits: &str) -> String {
1096 let mut bytes: Vec<u8> = digits.as_bytes().to_vec();
1097 let mut i = bytes.len();
1098 let mut carry = 1u8;
1099 while i > 0 && carry > 0 {
1100 i -= 1;
1101 let d = bytes[i] - b'0' + carry;
1102 if d >= 10 {
1103 bytes[i] = b'0';
1104 carry = 1;
1105 } else {
1106 bytes[i] = b'0' + d;
1107 carry = 0;
1108 }
1109 }
1110 if carry > 0 {
1111 bytes.insert(0, b'1');
1112 }
1113 String::from_utf8(bytes).expect("ascii digits")
1114}
1115
1116fn try_decimal_literal(expr: &Expr) -> Option<(i128, String, Span)> {
1119 match expr {
1120 Expr::Lit(ExprLit { lit, .. }) => match lit {
1121 Lit::Float(f) => Some((1, f.to_string(), f.span())),
1122 Lit::Int(i) => Some((1, i.to_string(), i.span())),
1123 _ => None,
1124 },
1125 Expr::Unary(ExprUnary {
1126 op: UnOp::Neg(_),
1127 expr: inner,
1128 ..
1129 }) => {
1130 if let Expr::Lit(ExprLit { lit, .. }) = inner.as_ref() {
1131 match lit {
1132 Lit::Float(f) => Some((-1, f.to_string(), f.span())),
1133 Lit::Int(i) => Some((-1, i.to_string(), i.span())),
1134 _ => None,
1135 }
1136 } else {
1137 None
1138 }
1139 }
1140 _ => None,
1141 }
1142}
1143
1144fn expr_span(expr: &Expr) -> Span {
1145 use syn::spanned::Spanned;
1146 expr.span()
1147}
1148
1149fn parse_value_token(raw: &str, span: Span, radix: u32) -> Result<(String, u32)> {
1161 for (i, c) in raw.char_indices() {
1163 if (c == 'i' || c == 'u')
1164 || (c == 'f'
1165 && i > 0
1166 && !raw[..i].contains('.')
1167 && !raw[..i]
1168 .chars()
1169 .last()
1170 .is_some_and(|x| x.is_ascii_digit()))
1171 {
1172 let _ = i;
1174 }
1175 }
1176 if let Some(idx) = raw.find('f') {
1177 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1179 return Err(syn::Error::new(
1180 span,
1181 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1182 ));
1183 }
1184 }
1185 if let Some(idx) = raw.rfind(['i', 'u']) {
1186 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1190 return Err(syn::Error::new(
1191 span,
1192 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1193 ));
1194 }
1195 }
1196
1197 if radix == 10 {
1198 return parse_decimal_token(raw, span);
1199 }
1200
1201 let digits_part = strip_radix_prefix(raw)
1204 .map(|(p, rest)| {
1205 let _ = p;
1208 rest
1209 })
1210 .unwrap_or(raw);
1211
1212 if digits_part.contains('.') {
1213 return Err(syn::Error::new(
1214 span,
1215 "fractional non-decimal literals are not supported (use an explicit `scale N` with an integer-only digit string instead)",
1216 ));
1217 }
1218
1219 let cleaned: String = digits_part.chars().filter(|c| *c != '_').collect();
1221 if cleaned.is_empty() {
1222 return Err(syn::Error::new(span, "empty digit string"));
1223 }
1224
1225 let magnitude = match i128::from_str_radix(&cleaned, radix) {
1230 Ok(v) => v,
1231 Err(_) => {
1232 return Err(syn::Error::new(
1233 span,
1234 format!("digit string `{cleaned}` is not valid in radix {radix} or overflows i128"),
1235 ));
1236 }
1237 };
1238 Ok((magnitude.to_string(), 0))
1239}
1240
1241fn strip_radix_prefix(raw: &str) -> Option<(u32, &str)> {
1242 if let Some(rest) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
1243 Some((16, rest))
1244 } else if let Some(rest) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
1245 Some((8, rest))
1246 } else if let Some(rest) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
1247 Some((2, rest))
1248 } else {
1249 None
1250 }
1251}
1252
1253fn parse_decimal_token(raw: &str, span: Span) -> Result<(String, u32)> {
1254 let (mantissa, sci_exp) = match raw.find(['e', 'E']) {
1255 Some(idx) => {
1256 let m = &raw[..idx];
1257 let e: i32 = raw[idx + 1..].parse().map_err(|_| {
1258 syn::Error::new(
1259 span,
1260 format!("invalid scientific exponent: `{}`", &raw[idx + 1..]),
1261 )
1262 })?;
1263 (m, e)
1264 }
1265 None => (raw, 0_i32),
1266 };
1267 let (int_part, frac_part) = match mantissa.find('.') {
1268 Some(idx) => {
1269 let int_part = &mantissa[..idx];
1270 let frac_part = &mantissa[idx + 1..];
1271 if int_part.is_empty() {
1272 return Err(syn::Error::new(
1273 span,
1274 "decimal literals require a digit on each side of the dot (write `0.5` not `.5`)",
1275 ));
1276 }
1277 if frac_part.is_empty() {
1278 return Err(syn::Error::new(
1279 span,
1280 "decimal literals require a digit on each side of the dot (write `1.0` not `1.`)",
1281 ));
1282 }
1283 (int_part, frac_part)
1284 }
1285 None => (mantissa, ""),
1286 };
1287 let cleaned_int: String = int_part.chars().filter(|c| *c != '_').collect();
1288 let cleaned_frac: String = frac_part.chars().filter(|c| *c != '_').collect();
1289 for c in cleaned_int.chars().chain(cleaned_frac.chars()) {
1290 if !c.is_ascii_digit() {
1291 return Err(syn::Error::new(
1292 span,
1293 format!("invalid digit `{c}` in decimal literal"),
1294 ));
1295 }
1296 }
1297 let mantissa_scale = cleaned_frac.len() as u32;
1298 let mut digits = cleaned_int;
1299 digits.push_str(&cleaned_frac);
1300 let trimmed = digits.trim_start_matches('0');
1303 let digits = if trimmed.is_empty() {
1304 "0".to_string()
1305 } else {
1306 trimmed.to_string()
1307 };
1308
1309 let signed_natural = (mantissa_scale as i64) - (sci_exp as i64);
1311 if signed_natural >= 0 {
1312 Ok((digits, signed_natural as u32))
1313 } else {
1314 let pad = (-signed_natural) as usize;
1316 Ok((pad_with_zeros(&digits, pad), 0))
1317 }
1318}
1319
1320fn error(span: Span, msg: String) -> TokenStream {
1321 syn::Error::new(span, msg).into_compile_error().into()
1322}