diff --git a/cmd/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index a12d5821b..9745c25b3 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -266,6 +266,17 @@ impl Spec { let v = value_type.as_ref().clone(); return self.from_string(s, &v); } + // Integer types accept `_` as a digit separator, mirroring Rust numeric + // literals (e.g. `1_000_000`). Strip separators before parsing, gated to + // integer types only: `_` is a legal character in Symbol/String args and + // must be preserved there. See #2447. + let stripped; + let s = if is_integer_type(t) && s.contains('_') { + stripped = s.replace('_', ""); + stripped.as_str() + } else { + s + }; // Parse as string and for special types assume Value::String serde_json::from_str(s) .map_or_else( @@ -805,6 +816,21 @@ impl Spec { /// # Errors /// /// Might return an error +/// Integer contract-argument types that accept `_` as a digit separator. +fn is_integer_type(t: &ScType) -> bool { + matches!( + t, + ScType::U32 + | ScType::I32 + | ScType::U64 + | ScType::I64 + | ScType::U128 + | ScType::I128 + | ScType::U256 + | ScType::I256 + ) +} + pub fn from_string_primitive(s: &str, t: &ScType) -> Result { Spec::from_string_primitive(s, t) } @@ -1853,6 +1879,45 @@ mod tests { assert_eq!(to_string(&parsed).unwrap(), format!("\"{as_str}\"")); } + /// Assert that a value with `_` digit separators parses identically to the + /// same value without them, generating one named `#[test]` per row. + macro_rules! numeric_separator_tests { + ($($name:ident: $ty:expr, $with:expr, $without:expr;)*) => { + $( + #[test] + fn $name() { + assert_eq!( + from_string_primitive($with, &$ty).unwrap(), + from_string_primitive($without, &$ty).unwrap() + ); + } + )* + }; + } + + numeric_separator_tests! { + test_u32_numeric_separator: ScType::U32, "1_000", "1000"; + test_i64_numeric_separator: ScType::I64, "1_000_000", "1000000"; + // The exact #2447 example. + test_i128_numeric_separator: ScType::I128, "100_0000000", "1000000000"; + test_u128_numeric_separator: ScType::U128, "1_000", "1000"; + test_u256_numeric_separator: ScType::U256, "1_000", "1000"; + test_i256_numeric_separator: ScType::I256, "1_000", "1000"; + } + + #[test] + fn test_symbol_underscore_preserved() { + // `_` is a legal Soroban symbol char and must NOT be stripped. + let parsed = from_string_primitive("hello_world", &ScType::Symbol).unwrap(); + assert_eq!(parsed, ScVal::Symbol("hello_world".try_into().unwrap())); + } + + #[test] + fn test_string_underscore_preserved() { + let parsed = from_string_primitive("a_b_c", &ScType::String).unwrap(); + assert_eq!(parsed, sc_string("a_b_c")); + } + #[test] fn test_symbol_conversion() { let as_str = "hello";