1use proc_macro::TokenStream;
30use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
31use quote::quote;
32use syn::{
33 Expr, ExprLit, ExprUnary, Lit, Result, UnOp,
34};
35
36#[derive(Clone, Copy)]
42struct Width {
43 name: &'static str,
46 max_scale: u32,
49 type_path: &'static str,
52 storage_path: &'static str,
55 wide: bool,
58}
59
60const D9: Width = Width {
61 name: "d9",
62 max_scale: 9,
63 type_path: "::decimal_scaled::D9",
64 storage_path: "i32",
65 wide: false,
66};
67const D18: Width = Width {
68 name: "d18",
69 max_scale: 18,
70 type_path: "::decimal_scaled::D18",
71 storage_path: "i64",
72 wide: false,
73};
74const D38: Width = Width {
75 name: "d38",
76 max_scale: 38,
77 type_path: "::decimal_scaled::D38",
78 storage_path: "i128",
79 wide: false,
80};
81const D76: Width = Width {
82 name: "d76",
83 max_scale: 76,
84 type_path: "::decimal_scaled::D76",
85 storage_path: "::decimal_scaled::Int256",
86 wide: true,
87};
88const D153: Width = Width {
89 name: "d153",
90 max_scale: 153,
91 type_path: "::decimal_scaled::D153",
92 storage_path: "::decimal_scaled::Int512",
93 wide: true,
94};
95const D307: Width = Width {
96 name: "d307",
97 max_scale: 307,
98 type_path: "::decimal_scaled::D307",
99 storage_path: "::decimal_scaled::Int1024",
100 wide: true,
101};
102
103#[proc_macro]
108pub fn d9(input: TokenStream) -> TokenStream {
109 expand_for(D9, input)
110}
111
112#[proc_macro]
115pub fn d18(input: TokenStream) -> TokenStream {
116 expand_for(D18, input)
117}
118
119#[proc_macro]
122pub fn d38(input: TokenStream) -> TokenStream {
123 expand_for(D38, input)
124}
125
126#[proc_macro]
129pub fn d76(input: TokenStream) -> TokenStream {
130 expand_for(D76, input)
131}
132
133#[proc_macro]
136pub fn d153(input: TokenStream) -> TokenStream {
137 expand_for(D153, input)
138}
139
140#[proc_macro]
144pub fn d307(input: TokenStream) -> TokenStream {
145 expand_for(D307, input)
146}
147
148fn expand_for(width: Width, input: TokenStream) -> TokenStream {
149 let tokens: TokenStream2 = input.into();
154 match parse_invocation(tokens, width) {
155 Ok(inv) => inv.expand(),
156 Err(e) => e.into_compile_error().into(),
157 }
158}
159
160fn parse_invocation(tokens: TokenStream2, width: Width) -> Result<Invocation> {
167 let segments = split_top_commas(tokens);
168 if segments.is_empty() || segments[0].is_empty() {
169 return Err(syn::Error::new(
170 Span::call_site(),
171 format!("{}!() requires a value argument", width.name),
172 ));
173 }
174
175 let mut explicit_radix: Option<(u32, Span)> = None;
179 for seg in &segments[1..] {
180 if let Some((kw, _)) = seg.first().zip(seg.get(1)) {
181 if let TokenTree::Ident(id) = kw {
182 if id.to_string() == "radix" {
183 if let TokenTree::Literal(lit) = &seg[1] {
184 if let Ok(r) = lit.to_string().parse::<u32>() {
185 explicit_radix = Some((r, lit.span()));
186 }
187 }
188 }
189 }
190 }
191 }
192
193 let value_segment = &segments[0];
197 let value_parse = try_radix_fractional(value_segment, explicit_radix)?;
198
199 if let Some((digits, sign, natural_scale, value_span)) = value_parse {
200 let (scale_qualifier, _radix_q, rounded) =
204 parse_qualifier_segments(&segments[1..], width)?;
205 return Ok(Invocation::Literal {
206 width,
207 digits,
208 sign,
209 natural_scale,
210 scale_qualifier,
211 rounded,
212 radix_literal: true,
213 value_span,
214 });
215 }
216
217 let value_ts: TokenStream2 = value_segment.iter().cloned().collect();
220 let value_expr: Expr = syn::parse2(value_ts)?;
221 let value_span = expr_span(&value_expr);
222
223 let (scale_qualifier, radix_qualifier, rounded) =
224 parse_qualifier_segments(&segments[1..], width)?;
225
226 if let Some((sign, raw_str, lit_span)) = try_decimal_literal(&value_expr) {
227 let radix = pick_radix(&raw_str, radix_qualifier, lit_span)?;
228 let (digits, natural_scale) =
229 parse_value_token(&raw_str, lit_span, radix)?;
230 Ok(Invocation::Literal {
231 width,
232 digits,
233 sign,
234 natural_scale,
235 scale_qualifier,
236 rounded,
237 radix_literal: radix != 10,
238 value_span,
239 })
240 } else {
241 if let Some((_, radix_span)) = radix_qualifier {
242 return Err(syn::Error::new(
243 radix_span,
244 "`radix` qualifier is only valid with a literal value",
245 ));
246 }
247 let (scale, scale_span) = scale_qualifier.ok_or_else(|| {
248 syn::Error::new(
249 value_span,
250 format!(
251 "scale must be specified for an expression value: `{}!(expr, scale N)`",
252 width.name
253 ),
254 )
255 })?;
256 let _ = rounded;
257 Ok(Invocation::Expression {
258 width,
259 expr: value_expr,
260 scale,
261 scale_span,
262 })
263 }
264}
265
266fn split_top_commas(tokens: TokenStream2) -> Vec<Vec<TokenTree>> {
270 let mut out: Vec<Vec<TokenTree>> = vec![Vec::new()];
271 for tt in tokens {
272 match &tt {
273 TokenTree::Punct(p) if p.as_char() == ',' && p.spacing() == proc_macro2::Spacing::Alone => {
274 out.push(Vec::new());
275 }
276 _ => out.last_mut().unwrap().push(tt),
277 }
278 }
279 out
280}
281
282fn try_radix_fractional(
289 segment: &[TokenTree],
290 explicit_radix: Option<(u32, Span)>,
291) -> Result<Option<(String, i128, u32, Span)>> {
292 let Some((radix, _radix_span)) = explicit_radix else {
293 return Ok(None);
294 };
295 if radix == 10 {
296 return Ok(None);
297 }
298
299 let mut i = 0;
300 let mut sign: i128 = 1;
301 if let Some(TokenTree::Punct(p)) = segment.first() {
302 if p.as_char() == '-' {
303 sign = -1;
304 i += 1;
305 } else if p.as_char() == '+' {
306 i += 1;
307 }
308 }
309
310 let int_tok = segment.get(i);
319 let dot_tok = segment.get(i + 1);
320 let frac_tok = segment.get(i + 2);
321 let extra = segment.get(i + 3);
322
323 let int_lit = match int_tok {
324 Some(TokenTree::Literal(lit)) => lit.to_string(),
325 Some(TokenTree::Ident(id)) => id.to_string(),
326 _ => return Ok(None),
327 };
328
329 let (int_part, frac_part, span) = if int_lit.contains('.') && dot_tok.is_none() {
330 let span = match int_tok.unwrap() {
332 TokenTree::Literal(lit) => lit.span(),
333 _ => Span::call_site(),
334 };
335 let (head, tail) = int_lit.split_once('.').unwrap();
336 (head.to_string(), tail.to_string(), span)
337 } else {
338 match (dot_tok, frac_tok, extra) {
339 (Some(TokenTree::Punct(p)), Some(frac), None) if p.as_char() == '.' => {
340 let frac_str = match frac {
341 TokenTree::Literal(lit) => lit.to_string(),
342 TokenTree::Ident(id) => id.to_string(),
343 _ => return Ok(None),
344 };
345 let span = match int_tok.unwrap() {
346 TokenTree::Literal(lit) => lit.span(),
347 TokenTree::Ident(id) => id.span(),
348 _ => Span::call_site(),
349 };
350 (int_lit, frac_str, span)
351 }
352 (None, None, _) => {
353 let span = match int_tok.unwrap() {
354 TokenTree::Literal(lit) => lit.span(),
355 TokenTree::Ident(id) => id.span(),
356 _ => Span::call_site(),
357 };
358 (int_lit, String::new(), span)
359 }
360 _ => return Ok(None),
361 }
362 };
363
364 let cleaned_int = if let Some((prefix_r, rest)) = strip_radix_prefix(&int_part) {
368 if prefix_r != radix {
369 return Err(syn::Error::new(
370 span,
371 format!(
372 "radix qualifier ({radix}) disagrees with integer-part prefix (radix {prefix_r})"
373 ),
374 ));
375 }
376 rest.to_string()
377 } else {
378 int_part
379 };
380
381 let int_cleaned: String =
382 cleaned_int.chars().filter(|c| *c != '_').collect();
383 let frac_cleaned: String =
384 frac_part.chars().filter(|c| *c != '_').collect();
385
386 if int_cleaned.is_empty() {
387 return Err(syn::Error::new(
388 span,
389 "decimal literals require a digit on each side of the dot (write `0.A3` not `.A3`)",
390 ));
391 }
392 for c in int_cleaned.chars().chain(frac_cleaned.chars()) {
393 if !is_radix_digit(c, radix) {
394 return Err(syn::Error::new(
395 span,
396 format!("digit `{c}` not valid for radix {radix}"),
397 ));
398 }
399 }
400
401 let combined = format!("{int_cleaned}{frac_cleaned}");
407 let magnitude = match i128::from_str_radix(&combined, radix) {
408 Ok(v) => v,
409 Err(_) => {
410 return Err(syn::Error::new(
411 span,
412 format!(
413 "digit string `{combined}` overflows i128 when parsed in radix {radix}"
414 ),
415 ));
416 }
417 };
418 Ok(Some((
419 magnitude.to_string(),
420 sign,
421 frac_cleaned.len() as u32,
422 span,
423 )))
424}
425
426fn is_radix_digit(c: char, radix: u32) -> bool {
427 c.to_digit(radix).is_some()
428}
429
430fn parse_qualifier_segments(
434 segments: &[Vec<TokenTree>],
435 width: Width,
436) -> Result<(Option<(u32, Span)>, Option<(u32, Span)>, bool)> {
437 let _ = width;
438 let mut scale: Option<(u32, Span)> = None;
439 let mut radix: Option<(u32, Span)> = None;
440 let mut rounded = false;
441 for seg in segments {
442 if seg.is_empty() {
443 continue;
444 }
445 let TokenTree::Ident(kw) = &seg[0] else {
446 return Err(syn::Error::new(
447 tt_span(&seg[0]),
448 "expected qualifier identifier (scale | radix | rounded)",
449 ));
450 };
451 match kw.to_string().as_str() {
452 "scale" => {
453 let lit = seg.get(1).and_then(|t| match t {
454 TokenTree::Literal(l) => Some(l),
455 _ => None,
456 });
457 let lit = lit.ok_or_else(|| {
458 syn::Error::new(kw.span(), "`scale` requires an integer literal: `scale N`")
459 })?;
460 let n: u32 = lit.to_string().parse().map_err(|_| {
461 syn::Error::new(lit.span(), "scale must be a non-negative integer")
462 })?;
463 if scale.is_some() {
464 return Err(syn::Error::new(kw.span(), "duplicate `scale` qualifier"));
465 }
466 scale = Some((n, lit.span()));
467 }
468 "radix" => {
469 let lit = seg.get(1).and_then(|t| match t {
470 TokenTree::Literal(l) => Some(l),
471 _ => None,
472 });
473 let lit = lit.ok_or_else(|| {
474 syn::Error::new(kw.span(), "`radix` requires an integer literal: `radix N`")
475 })?;
476 let r: u32 = lit.to_string().parse().map_err(|_| {
477 syn::Error::new(lit.span(), "radix must be one of 2, 8, 10, 16")
478 })?;
479 if !matches!(r, 2 | 8 | 10 | 16) {
480 return Err(syn::Error::new(
481 lit.span(),
482 format!("radix must be one of 2, 8, 10, 16 (got {r})"),
483 ));
484 }
485 if radix.is_some() {
486 return Err(syn::Error::new(kw.span(), "duplicate `radix` qualifier"));
487 }
488 radix = Some((r, lit.span()));
489 }
490 "rounded" => {
491 if rounded {
492 return Err(syn::Error::new(kw.span(), "duplicate `rounded` qualifier"));
493 }
494 if seg.len() > 1 {
495 return Err(syn::Error::new(
496 tt_span(&seg[1]),
497 "`rounded` takes no argument",
498 ));
499 }
500 rounded = true;
501 }
502 other => {
503 return Err(syn::Error::new(
504 kw.span(),
505 format!(
506 "unknown qualifier `{other}`; expected one of: scale, radix, rounded"
507 ),
508 ));
509 }
510 }
511 }
512 Ok((scale, radix, rounded))
513}
514
515fn tt_span(tt: &TokenTree) -> Span {
516 match tt {
517 TokenTree::Group(g) => g.span(),
518 TokenTree::Ident(i) => i.span(),
519 TokenTree::Punct(p) => p.span(),
520 TokenTree::Literal(l) => l.span(),
521 }
522}
523
524enum Invocation {
527 Literal {
528 width: Width,
529 digits: String,
532 sign: i128,
534 natural_scale: u32,
535 scale_qualifier: Option<(u32, Span)>,
536 rounded: bool,
537 radix_literal: bool,
542 value_span: Span,
543 },
544 Expression {
545 width: Width,
546 expr: Expr,
547 scale: u32,
548 scale_span: Span,
549 },
550}
551
552impl Invocation {
553 fn expand(self) -> TokenStream {
554 match self {
555 Invocation::Literal {
556 width,
557 digits,
558 sign,
559 natural_scale,
560 scale_qualifier,
561 rounded,
562 radix_literal,
563 value_span,
564 } => expand_literal(
565 width,
566 digits,
567 sign,
568 natural_scale,
569 scale_qualifier,
570 rounded,
571 radix_literal,
572 value_span,
573 ),
574 Invocation::Expression {
575 width,
576 expr,
577 scale,
578 scale_span,
579 } => expand_expression(width, expr, scale, scale_span),
580 }
581 }
582}
583
584fn pick_radix(
590 raw: &str,
591 qualifier: Option<(u32, Span)>,
592 span: Span,
593) -> Result<u32> {
594 let prefix_radix = if let Some(stripped) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
595 Some((16, stripped))
596 } else if let Some(stripped) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
597 Some((8, stripped))
598 } else if let Some(stripped) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
599 Some((2, stripped))
600 } else {
601 None
602 };
603 match (prefix_radix, qualifier) {
604 (None, None) => Ok(10),
605 (None, Some((r, _))) => Ok(r),
606 (Some((p, _)), None) => Ok(p),
607 (Some((p, _)), Some((r, _sp))) if p == r => Ok(r),
608 (Some((p, _)), Some((r, sp))) => Err(syn::Error::new(
609 sp,
610 format!("radix qualifier ({r}) disagrees with literal prefix (radix {p})"),
611 )),
612 }.map_err(|e: syn::Error| {
613 let _ = span;
617 e
618 })
619}
620
621fn expand_literal(
624 width: Width,
625 digits: String,
626 sign: i128,
627 natural_scale: u32,
628 scale_qualifier: Option<(u32, Span)>,
629 rounded: bool,
630 radix_literal: bool,
631 value_span: Span,
632) -> TokenStream {
633 let (target_scale, scale_span) = match scale_qualifier {
634 Some((n, sp)) => (n, sp),
635 None => (natural_scale, value_span),
636 };
637
638 if target_scale > width.max_scale {
639 return error(
640 scale_span,
641 format!(
642 "scale {target_scale} exceeds max for {} (max = {})",
643 width.name.to_uppercase(),
644 width.max_scale
645 ),
646 );
647 }
648
649 if radix_literal {
653 let _ = rounded;
654 if width.wide {
655 return emit_wide(width, target_scale, sign, &digits);
656 } else {
657 return emit_narrow(width, target_scale, sign, &digits, value_span);
658 }
659 }
660
661 let shifted_digits: String;
669 let final_digits: &str;
670
671 if target_scale == natural_scale {
672 final_digits = &digits;
673 } else if target_scale > natural_scale {
674 let pad = target_scale - natural_scale;
675 shifted_digits = pad_with_zeros(&digits, pad as usize);
676 final_digits = &shifted_digits;
677 } else {
678 let shift = natural_scale - target_scale;
679 let padded = if (digits.len() as u32) <= shift {
683 pad_leading_zeros(&digits, (shift + 1) as usize - digits.len())
684 } else {
685 digits.clone()
686 };
687 let split = padded.len() - shift as usize;
688 let (kept, dropped) = padded.split_at(split);
689 let exact = dropped.bytes().all(|b| b == b'0');
690 if !exact && !rounded {
691 return error(
692 value_span,
693 format!(
694 "literal has {natural_scale} fractional digits, target scale {target_scale} would lose precision; pass `rounded` to opt into half-to-even rounding"
695 ),
696 );
697 }
698 if exact {
699 shifted_digits = if kept.is_empty() { "0".to_string() } else { kept.to_string() };
700 } else {
701 shifted_digits = round_half_to_even(kept, dropped, sign < 0);
703 }
704 final_digits = &shifted_digits;
705 }
706
707 if width.wide {
708 emit_wide(width, target_scale, sign, final_digits)
709 } else {
710 emit_narrow(width, target_scale, sign, final_digits, value_span)
711 }
712}
713
714fn emit_narrow(
715 width: Width,
716 target_scale: u32,
717 sign: i128,
718 digits: &str,
719 value_span: Span,
720) -> TokenStream {
721 let magnitude: i128 = match digits.parse::<i128>() {
722 Ok(v) => v,
723 Err(_) => {
724 return error(
725 value_span,
726 format!("scaled value overflows i128 before narrowing to {}'s storage", width.name.to_uppercase()),
727 );
728 }
729 };
730 let signed = match magnitude.checked_mul(sign) {
731 Some(v) => v,
732 None => {
733 return error(
734 value_span,
735 "scaled value overflows i128 after applying sign".to_string(),
736 );
737 }
738 };
739 let (min, max): (i128, i128) = match width.storage_path {
741 "i32" => (i32::MIN as i128, i32::MAX as i128),
742 "i64" => (i64::MIN as i128, i64::MAX as i128),
743 "i128" => (i128::MIN, i128::MAX),
744 _ => unreachable!("narrow path called with non-narrow storage"),
745 };
746 if signed < min || signed > max {
747 return error(
748 value_span,
749 format!(
750 "scaled value {signed} overflows {}'s storage ({})",
751 width.name.to_uppercase(),
752 width.storage_path
753 ),
754 );
755 }
756 let bits_tokens: proc_macro2::TokenStream = match width.storage_path {
757 "i32" => {
758 let v = signed as i32;
759 quote! { #v }
760 }
761 "i64" => {
762 let v = signed as i64;
763 quote! { #v }
764 }
765 "i128" => quote! { #signed },
766 _ => unreachable!(),
767 };
768 let type_path: proc_macro2::TokenStream = width.type_path.parse().unwrap();
769 let out = quote! {
770 #type_path :: <#target_scale> :: from_bits(#bits_tokens)
771 };
772 out.into()
773}
774
775fn emit_wide(
776 width: Width,
777 target_scale: u32,
778 sign: i128,
779 digits: &str,
780) -> TokenStream {
781 let signed_str = if sign < 0 {
784 format!("-{digits}")
785 } else {
786 digits.to_string()
787 };
788 let type_path: proc_macro2::TokenStream = width.type_path.parse().unwrap();
789 let storage_path: proc_macro2::TokenStream = width.storage_path.parse().unwrap();
790 let err_msg = format!("{}! bits parse failed", width.name);
791 let out = quote! {
792 #type_path :: <#target_scale> :: from_bits({
793 const BITS: #storage_path = match <#storage_path>::from_str_radix(#signed_str, 10) {
794 ::core::result::Result::Ok(v) => v,
795 ::core::result::Result::Err(_) => panic!(#err_msg),
796 };
797 BITS
798 })
799 };
800 out.into()
801}
802
803fn expand_expression(
806 width: Width,
807 expr: Expr,
808 scale: u32,
809 scale_span: Span,
810) -> TokenStream {
811 if scale > width.max_scale {
812 return error(
813 scale_span,
814 format!(
815 "scale {scale} exceeds max for {} (max = {})",
816 width.name.to_uppercase(),
817 width.max_scale
818 ),
819 );
820 }
821 let type_path: proc_macro2::TokenStream = width.type_path.parse().unwrap();
822 let storage_path: proc_macro2::TokenStream = width.storage_path.parse().unwrap();
823 let err_msg = format!("{}! overflow: expression * 10^SCALE exceeds storage range", width.name);
824 let out = if width.wide {
825 quote! {
828 #type_path :: <#scale> :: from_bits({
829 let _v: #storage_path = (#expr);
830 let mult: #storage_path = <#storage_path>::from_str_radix("10", 10)
831 .expect("d{}! mult literal")
832 .pow(#scale);
833 _v.checked_mul(mult).expect(#err_msg)
834 })
835 }
836 } else if scale == 0 {
837 quote! {
838 #type_path :: <0> :: from_bits({
839 let _v: #storage_path = (#expr);
840 _v
841 })
842 }
843 } else {
844 let mult_lit: proc_macro2::TokenStream = match width.storage_path {
846 "i32" => {
847 let v = 10i32.pow(scale);
848 quote! { #v }
849 }
850 "i64" => {
851 let v = 10i64.pow(scale);
852 quote! { #v }
853 }
854 "i128" => {
855 let v = 10i128.pow(scale);
856 quote! { #v }
857 }
858 _ => unreachable!(),
859 };
860 quote! {
861 #type_path :: <#scale> :: from_bits({
862 let _v: #storage_path = (#expr);
863 _v.checked_mul(#mult_lit).expect(#err_msg)
864 })
865 }
866 };
867 out.into()
868}
869
870fn pad_with_zeros(digits: &str, pad: usize) -> String {
873 let mut out = String::with_capacity(digits.len() + pad);
874 out.push_str(digits);
875 for _ in 0..pad {
876 out.push('0');
877 }
878 out
879}
880
881fn pad_leading_zeros(digits: &str, pad: usize) -> String {
882 let mut out = String::with_capacity(digits.len() + pad);
883 for _ in 0..pad {
884 out.push('0');
885 }
886 out.push_str(digits);
887 out
888}
889
890fn round_half_to_even(kept: &str, dropped: &str, _negative: bool) -> String {
894 debug_assert!(!dropped.is_empty());
895 let first = dropped.as_bytes()[0];
897 let rest_nonzero = dropped.bytes().skip(1).any(|b| b != b'0');
898 let round_up = match first.cmp(&b'5') {
899 std::cmp::Ordering::Less => false,
900 std::cmp::Ordering::Greater => true,
901 std::cmp::Ordering::Equal => {
902 if rest_nonzero {
906 true
907 } else {
908 let last_kept = kept.bytes().last().unwrap_or(b'0');
909 (last_kept - b'0') & 1 == 1
910 }
911 }
912 };
913 let kept_or_zero = if kept.is_empty() { "0" } else { kept };
914 if !round_up {
915 kept_or_zero.to_string()
916 } else {
917 add_one_to_digits(kept_or_zero)
918 }
919}
920
921fn add_one_to_digits(digits: &str) -> String {
923 let mut bytes: Vec<u8> = digits.as_bytes().to_vec();
924 let mut i = bytes.len();
925 let mut carry = 1u8;
926 while i > 0 && carry > 0 {
927 i -= 1;
928 let d = bytes[i] - b'0' + carry;
929 if d >= 10 {
930 bytes[i] = b'0';
931 carry = 1;
932 } else {
933 bytes[i] = b'0' + d;
934 carry = 0;
935 }
936 }
937 if carry > 0 {
938 bytes.insert(0, b'1');
939 }
940 String::from_utf8(bytes).expect("ascii digits")
941}
942
943fn try_decimal_literal(expr: &Expr) -> Option<(i128, String, Span)> {
946 match expr {
947 Expr::Lit(ExprLit { lit, .. }) => match lit {
948 Lit::Float(f) => Some((1, f.to_string(), f.span())),
949 Lit::Int(i) => Some((1, i.to_string(), i.span())),
950 _ => None,
951 },
952 Expr::Unary(ExprUnary {
953 op: UnOp::Neg(_),
954 expr: inner,
955 ..
956 }) => {
957 if let Expr::Lit(ExprLit { lit, .. }) = inner.as_ref() {
958 match lit {
959 Lit::Float(f) => Some((-1, f.to_string(), f.span())),
960 Lit::Int(i) => Some((-1, i.to_string(), i.span())),
961 _ => None,
962 }
963 } else {
964 None
965 }
966 }
967 _ => None,
968 }
969}
970
971fn expr_span(expr: &Expr) -> Span {
972 use syn::spanned::Spanned;
973 expr.span()
974}
975
976fn parse_value_token(
988 raw: &str,
989 span: Span,
990 radix: u32,
991) -> Result<(String, u32)> {
992 for (i, c) in raw.char_indices() {
994 if (c == 'i' || c == 'u') || (c == 'f' && i > 0 && !raw[..i].contains('.') && !raw[..i].chars().last().map_or(false, |x| x.is_ascii_digit())) {
995 let _ = i;
997 }
998 }
999 if let Some(idx) = raw.find(|c: char| c == 'f') {
1000 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1002 return Err(syn::Error::new(
1003 span,
1004 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1005 ));
1006 }
1007 }
1008 if let Some(idx) = raw.rfind(|c: char| c == 'i' || c == 'u') {
1009 if idx > 0 && raw.as_bytes()[idx - 1] == b'_' {
1013 return Err(syn::Error::new(
1014 span,
1015 "type suffixes (e.g. _i64, _f32) are not accepted in decimal-scaled literals",
1016 ));
1017 }
1018 }
1019
1020 if radix == 10 {
1021 return parse_decimal_token(raw, span);
1022 }
1023
1024 let digits_part = strip_radix_prefix(raw).map(|(p, rest)| {
1027 let _ = p;
1030 rest
1031 }).unwrap_or(raw);
1032
1033 if digits_part.contains('.') {
1034 return Err(syn::Error::new(
1035 span,
1036 "fractional non-decimal literals are not supported (use an explicit `scale N` with an integer-only digit string instead)",
1037 ));
1038 }
1039
1040 let cleaned: String = digits_part.chars().filter(|c| *c != '_').collect();
1042 if cleaned.is_empty() {
1043 return Err(syn::Error::new(span, "empty digit string"));
1044 }
1045
1046 let magnitude = match i128::from_str_radix(&cleaned, radix) {
1051 Ok(v) => v,
1052 Err(_) => {
1053 return Err(syn::Error::new(
1054 span,
1055 format!(
1056 "digit string `{cleaned}` is not valid in radix {radix} or overflows i128"
1057 ),
1058 ));
1059 }
1060 };
1061 Ok((magnitude.to_string(), 0))
1062}
1063
1064fn strip_radix_prefix(raw: &str) -> Option<(u32, &str)> {
1065 if let Some(rest) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
1066 Some((16, rest))
1067 } else if let Some(rest) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
1068 Some((8, rest))
1069 } else if let Some(rest) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
1070 Some((2, rest))
1071 } else {
1072 None
1073 }
1074}
1075
1076fn parse_decimal_token(raw: &str, span: Span) -> Result<(String, u32)> {
1077 let (mantissa, sci_exp) = match raw.find(['e', 'E']) {
1078 Some(idx) => {
1079 let m = &raw[..idx];
1080 let e: i32 = raw[idx + 1..].parse().map_err(|_| {
1081 syn::Error::new(
1082 span,
1083 format!("invalid scientific exponent: `{}`", &raw[idx + 1..]),
1084 )
1085 })?;
1086 (m, e)
1087 }
1088 None => (raw, 0_i32),
1089 };
1090 let (int_part, frac_part) = match mantissa.find('.') {
1091 Some(idx) => {
1092 let int_part = &mantissa[..idx];
1093 let frac_part = &mantissa[idx + 1..];
1094 if int_part.is_empty() {
1095 return Err(syn::Error::new(
1096 span,
1097 "decimal literals require a digit on each side of the dot (write `0.5` not `.5`)",
1098 ));
1099 }
1100 if frac_part.is_empty() {
1101 return Err(syn::Error::new(
1102 span,
1103 "decimal literals require a digit on each side of the dot (write `1.0` not `1.`)",
1104 ));
1105 }
1106 (int_part, frac_part)
1107 }
1108 None => (mantissa, ""),
1109 };
1110 let cleaned_int: String = int_part.chars().filter(|c| *c != '_').collect();
1111 let cleaned_frac: String = frac_part.chars().filter(|c| *c != '_').collect();
1112 for c in cleaned_int.chars().chain(cleaned_frac.chars()) {
1113 if !c.is_ascii_digit() {
1114 return Err(syn::Error::new(
1115 span,
1116 format!("invalid digit `{c}` in decimal literal"),
1117 ));
1118 }
1119 }
1120 let mantissa_scale = cleaned_frac.len() as u32;
1121 let mut digits = cleaned_int;
1122 digits.push_str(&cleaned_frac);
1123 let trimmed = digits.trim_start_matches('0');
1126 let digits = if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() };
1127
1128 let signed_natural = (mantissa_scale as i64) - (sci_exp as i64);
1130 if signed_natural >= 0 {
1131 Ok((digits, signed_natural as u32))
1132 } else {
1133 let pad = (-signed_natural) as usize;
1135 Ok((pad_with_zeros(&digits, pad), 0))
1136 }
1137}
1138
1139fn error(span: Span, msg: String) -> TokenStream {
1140 syn::Error::new(span, msg).into_compile_error().into()
1141}