Jelajahi Sumber

Implement Soroban integer width rounding feature (#1805) (#1810)

## Description
This PR adds automatic rounding of integer widths to Soroban-compatible
sizes when developers use unsupported integer types like `int56` or
`uint72`.

### Key Changes
Automatic Rounding - int56 → int64, uint72 → uint128, etc.
Warning Messages - Shows exact warnings for non-compatible widths
Strict Mode Flag - `--strict-soroban-types` turns warnings into errors
Soroban-Only - Only applies when targeting Soroban runtime

Fixes #1805

---------

Signed-off-by: Pratyksh Gupta <pratykshgupta9999@gmail.com>
Co-authored-by: salaheldinsoliman <49910731+salaheldinsoliman@users.noreply.github.com>
Pratyksh Gupta 2 bulan lalu
induk
melakukan
c63cfcc29f

+ 6 - 0
src/bin/cli/mod.rs

@@ -349,6 +349,10 @@ pub struct DebugFeatures {
     #[arg(name = "RELEASE", help = "Disable all debugging features such as prints, logging runtime errors, and logging api return codes", long = "release", action = ArgAction::SetTrue)]
     #[serde(default)]
     pub release: bool,
+
+    #[arg(name = "STRICTSOROBANTYPES", help = "Turn Soroban integer width warnings into errors for stricter type safety", long = "strict-soroban-types", action = ArgAction::SetTrue)]
+    #[serde(default)]
+    pub strict_soroban_types: bool,
 }
 
 impl Default for DebugFeatures {
@@ -358,6 +362,7 @@ impl Default for DebugFeatures {
             log_prints: true,
             generate_debug_info: false,
             release: false,
+            strict_soroban_types: false,
         }
     }
 }
@@ -581,6 +586,7 @@ pub fn options_arg(
         opt_level,
         log_runtime_errors: debug.log_runtime_errors && !debug.release,
         log_prints: debug.log_prints && !debug.release,
+        strict_soroban_types: debug.strict_soroban_types,
         #[cfg(feature = "wasm_opt")]
         wasm_opt: optimizations.wasm_opt_passes.or(if debug.release {
             Some(OptimizationPasses::Z)

+ 4 - 2
src/bin/cli/test.rs

@@ -214,7 +214,8 @@ mod tests {
                     log_runtime_errors: true,
                     log_prints: true,
                     generate_debug_info: false,
-                    release: false
+                    release: false,
+                    strict_soroban_types: false,
                 },
                 optimizations: cli::Optimizations {
                     dead_storage: true,
@@ -269,7 +270,8 @@ mod tests {
                     log_runtime_errors: true,
                     log_prints: true,
                     generate_debug_info: false,
-                    release: false
+                    release: false,
+                    strict_soroban_types: false,
                 },
                 optimizations: cli::Optimizations {
                     dead_storage: false,

+ 8 - 2
src/bin/solang.rs

@@ -129,7 +129,12 @@ fn doc(doc_args: Doc) {
     let mut files = Vec::new();
 
     for filename in doc_args.package.input {
-        let ns = solang::parse_and_resolve(filename.as_os_str(), &mut resolver, target);
+        let ns = solang::parse_and_resolve_with_options(
+            filename.as_os_str(),
+            &mut resolver,
+            target,
+            None,
+        );
 
         ns.print_diagnostics(&resolver, verbose);
 
@@ -322,7 +327,8 @@ fn process_file(
     };
 
     // resolve phase
-    let mut ns = solang::parse_and_resolve(filepath.as_os_str(), resolver, target);
+    let mut ns =
+        solang::parse_and_resolve_with_options(filepath.as_os_str(), resolver, target, Some(opt));
 
     // codegen all the contracts; some additional errors/warnings will be detected here
     codegen(&mut ns, opt);

+ 2 - 0
src/codegen/mod.rs

@@ -170,6 +170,7 @@ pub struct Options {
     pub opt_level: OptimizationLevel,
     pub log_runtime_errors: bool,
     pub log_prints: bool,
+    pub strict_soroban_types: bool,
     #[cfg(feature = "wasm_opt")]
     pub wasm_opt: Option<OptimizationPasses>,
     pub soroban_version: Option<u64>,
@@ -187,6 +188,7 @@ impl Default for Options {
             opt_level: OptimizationLevel::Default,
             log_runtime_errors: false,
             log_prints: true,
+            strict_soroban_types: false,
             #[cfg(feature = "wasm_opt")]
             wasm_opt: None,
             soroban_version: None,

+ 11 - 1
src/lib.rs

@@ -127,7 +127,7 @@ pub fn compile(
     authors: Vec<String>,
     version: &str,
 ) -> (Vec<(Vec<u8>, String)>, sema::ast::Namespace) {
-    let mut ns = parse_and_resolve(filename, resolver, target);
+    let mut ns = parse_and_resolve_with_options(filename, resolver, target, Some(opts));
 
     if ns.diagnostics.any_errors() {
         return (Vec::new(), ns);
@@ -167,6 +167,16 @@ pub fn parse_and_resolve(
     filename: &OsStr,
     resolver: &mut FileResolver,
     target: Target,
+) -> sema::ast::Namespace {
+    parse_and_resolve_with_options(filename, resolver, target, None)
+}
+
+/// Parse and resolve the Solidity source code with options.
+pub fn parse_and_resolve_with_options(
+    filename: &OsStr,
+    resolver: &mut FileResolver,
+    target: Target,
+    _options: Option<&codegen::Options>,
 ) -> sema::ast::Namespace {
     let mut ns = sema::ast::Namespace::new(target);
 

+ 59 - 0
src/sema/ast.rs

@@ -143,6 +143,63 @@ impl Type {
             self
         }
     }
+
+    /// Round integer width to Soroban-compatible size and emit warning if needed
+    pub fn round_soroban_width(&self, ns: &mut Namespace, loc: pt::Loc) -> Type {
+        match self {
+            Type::Int(width) => {
+                let rounded_width = Self::get_soroban_int_width(*width);
+                if rounded_width != *width {
+                    let message = format!(
+                        "int{} is not supported by the Soroban runtime and will be rounded up to int{}",
+                        width, rounded_width
+                    );
+                    if ns.strict_soroban_types {
+                        ns.diagnostics.push(Diagnostic::error(loc, message));
+                    } else {
+                        ns.diagnostics.push(Diagnostic::warning(loc, message));
+                    }
+                    Type::Int(rounded_width)
+                } else {
+                    Type::Int(*width)
+                }
+            }
+            Type::Uint(width) => {
+                let rounded_width = Self::get_soroban_int_width(*width);
+                if rounded_width != *width {
+                    let message = format!(
+                        "uint{} is not supported by the Soroban runtime and will be rounded up to uint{}",
+                        width, rounded_width
+                    );
+                    if ns.strict_soroban_types {
+                        ns.diagnostics.push(Diagnostic::error(loc, message));
+                    } else {
+                        ns.diagnostics.push(Diagnostic::warning(loc, message));
+                    }
+                    Type::Uint(rounded_width)
+                } else {
+                    Type::Uint(*width)
+                }
+            }
+            _ => self.clone(),
+        }
+    }
+
+    /// Get the Soroban-compatible integer width by rounding up to the next supported size
+    pub fn get_soroban_int_width(width: u16) -> u16 {
+        match width {
+            1..=32 => 32,
+            33..=64 => 64,
+            65..=128 => 128,
+            129..=256 => 256,
+            _ => width, // Keep as-is if already 256+ or invalid
+        }
+    }
+
+    /// Check if an integer width is Soroban-compatible
+    pub fn is_soroban_compatible_width(width: u16) -> bool {
+        matches!(width, 32 | 64 | 128 | 256)
+    }
 }
 
 #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
@@ -703,6 +760,8 @@ pub struct Namespace {
     pub var_constants: HashMap<pt::Loc, codegen::Expression>,
     /// Overrides for hover in the language server
     pub hover_overrides: HashMap<pt::Loc, String>,
+    /// Strict mode for Soroban integer width checking
+    pub strict_soroban_types: bool,
 }
 
 #[derive(Debug)]

+ 9 - 1
src/sema/namespace.rs

@@ -66,6 +66,7 @@ impl Namespace {
             next_id: 0,
             var_constants: HashMap::new(),
             hover_overrides: HashMap::new(),
+            strict_soroban_types: false,
         };
 
         match target {
@@ -1191,7 +1192,14 @@ impl Namespace {
                         Type::Address(true)
                     }
                 }
-                _ => Type::from(ty),
+                _ => {
+                    let mut ty = Type::from(ty);
+                    // Apply Soroban integer width rounding if target is Soroban
+                    if self.target == Target::Soroban {
+                        ty = ty.round_soroban_width(self, id.loc());
+                    }
+                    ty
+                }
             };
 
             return if dimensions.is_empty() {

+ 0 - 2
tests/soroban.rs

@@ -1,8 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #[cfg(feature = "soroban")]
-pub mod soroban_testcases;
-
 use solang::codegen::Options;
 use solang::file_resolver::FileResolver;
 use solang::sema::ast::Namespace;

+ 81 - 0
tests/soroban_testcases/integer_width_rounding.rs

@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::{IntoVal, Val};
+
+#[test]
+fn test_int56_rounds_to_int64() {
+    let runtime = build_solidity(
+        r#"contract test {
+        function test_int56(int56 a) public returns (int64) {
+            return int64(a);
+        }
+    }"#,
+        |_| {},
+    );
+
+    // Check that the function compiles and works with the rounded type
+    let arg: Val = 42_i64.into_val(&runtime.env);
+    let addr = runtime.contracts.last().unwrap();
+    let res = runtime.invoke_contract(addr, "test_int56", vec![arg]);
+
+    let expected: Val = 42_i64.into_val(&runtime.env);
+    assert!(expected.shallow_eq(&res));
+}
+
+#[test]
+fn test_uint56_rounds_to_uint64() {
+    let runtime = build_solidity(
+        r#"contract test {
+        function test_uint56(uint56 a) public returns (uint64) {
+            return uint64(a);
+        }
+    }"#,
+        |_| {},
+    );
+
+    let arg: Val = 42_u64.into_val(&runtime.env);
+    let addr = runtime.contracts.last().unwrap();
+    let res = runtime.invoke_contract(addr, "test_uint56", vec![arg]);
+
+    let expected: Val = 42_u64.into_val(&runtime.env);
+    assert!(expected.shallow_eq(&res));
+}
+
+#[test]
+fn test_int96_rounds_to_int128() {
+    let runtime = build_solidity(
+        r#"contract test {
+        function test_int96(int96 a) public returns (int128) {
+            return int128(a);
+        }
+    }"#,
+        |_| {},
+    );
+
+    let arg: Val = 42_i128.into_val(&runtime.env);
+    let addr = runtime.contracts.last().unwrap();
+    let res = runtime.invoke_contract(addr, "test_int96", vec![arg]);
+
+    let expected: Val = 42_i128.into_val(&runtime.env);
+    assert!(expected.shallow_eq(&res));
+}
+
+#[test]
+fn test_uint96_rounds_to_uint128() {
+    let runtime = build_solidity(
+        r#"contract test {
+        function test_uint96(uint96 a) public returns (uint128) {
+            return uint128(a);
+        }
+    }"#,
+        |_| {},
+    );
+
+    let arg: Val = 42_u128.into_val(&runtime.env);
+    let addr = runtime.contracts.last().unwrap();
+    let res = runtime.invoke_contract(addr, "test_uint96", vec![arg]);
+
+    let expected: Val = 42_u128.into_val(&runtime.env);
+    assert!(expected.shallow_eq(&res));
+}

+ 223 - 0
tests/soroban_testcases/integer_width_warnings.rs

@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use solang::codegen::Options;
+use solang::file_resolver::FileResolver;
+use solang::sema::diagnostics::Diagnostics;
+use solang::{compile, Target};
+use std::ffi::OsStr;
+
+fn build_with_strict_soroban_types(src: &str, strict: bool) -> Diagnostics {
+    let tmp_file = OsStr::new("test.sol");
+    let mut cache = FileResolver::default();
+    cache.set_file_contents(tmp_file.to_str().unwrap(), src.to_string());
+    let opt = inkwell::OptimizationLevel::Default;
+    let target = Target::Soroban;
+    let (_, ns) = compile(
+        tmp_file,
+        &mut cache,
+        target,
+        &Options {
+            opt_level: opt.into(),
+            log_runtime_errors: true,
+            log_prints: true,
+            strict_soroban_types: strict,
+            #[cfg(feature = "wasm_opt")]
+            wasm_opt: Some(contract_build::OptimizationPasses::Z),
+            soroban_version: None,
+            ..Default::default()
+        },
+        std::vec!["unknown".to_string()],
+        "0.0.1",
+    );
+    ns.diagnostics
+}
+
+#[test]
+fn test_warning_for_int56_without_strict() {
+    let src = r#"contract test {
+        function test_int56(int56 a) public returns (int64) {
+            return int64(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, false);
+    
+    // Should have a warning about int56 being rounded to int64
+    let warnings: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Warning).collect();
+    assert!(!warnings.is_empty(), "Expected warnings for int56 rounding");
+    
+    let warning_messages: Vec<_> = warnings.iter().map(|w| w.message.as_str()).collect();
+    assert!(warning_messages.iter().any(|msg| msg.contains("int56") && msg.contains("int64")), 
+            "Expected warning about int56 being rounded to int64");
+}
+
+#[test]
+fn test_warning_for_uint56_without_strict() {
+    let src = r#"contract test {
+        function test_uint56(uint56 a) public returns (uint64) {
+            return uint64(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, false);
+    
+    // Should have a warning about uint56 being rounded to uint64
+    let warnings: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Warning).collect();
+    assert!(!warnings.is_empty(), "Expected warnings for uint56 rounding");
+    
+    let warning_messages: Vec<_> = warnings.iter().map(|w| w.message.as_str()).collect();
+    assert!(warning_messages.iter().any(|msg| msg.contains("uint56") && msg.contains("uint64")), 
+            "Expected warning about uint56 being rounded to uint64");
+}
+
+#[test]
+fn test_warning_for_int96_without_strict() {
+    let src = r#"contract test {
+        function test_int96(int96 a) public returns (int128) {
+            return int128(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, false);
+    
+    // Should have a warning about int96 being rounded to int128
+    let warnings: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Warning).collect();
+    assert!(!warnings.is_empty(), "Expected warnings for int96 rounding");
+    
+    let warning_messages: Vec<_> = warnings.iter().map(|w| w.message.as_str()).collect();
+    assert!(warning_messages.iter().any(|msg| msg.contains("int96") && msg.contains("int128")), 
+            "Expected warning about int96 being rounded to int128");
+}
+
+#[test]
+fn test_warning_for_uint96_without_strict() {
+    let src = r#"contract test {
+        function test_uint96(uint96 a) public returns (uint128) {
+            return uint128(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, false);
+    
+    // Should have a warning about uint96 being rounded to uint128
+    let warnings: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Warning).collect();
+    assert!(!warnings.is_empty(), "Expected warnings for uint96 rounding");
+    
+    let warning_messages: Vec<_> = warnings.iter().map(|w| w.message.as_str()).collect();
+    assert!(warning_messages.iter().any(|msg| msg.contains("uint96") && msg.contains("uint128")), 
+            "Expected warning about uint96 being rounded to uint128");
+}
+
+#[test]
+fn test_error_for_int56_with_strict() {
+    let src = r#"contract test {
+        function test_int56(int56 a) public returns (int64) {
+            return int64(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about int56 being rounded to int64
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for int56 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("int56") && msg.contains("int64")), 
+            "Expected error about int56 being rounded to int64");
+}
+
+#[test]
+fn test_error_for_uint56_with_strict() {
+    let src = r#"contract test {
+        function test_uint56(uint56 a) public returns (uint64) {
+            return uint64(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about uint56 being rounded to uint64
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for uint56 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("uint56") && msg.contains("uint64")), 
+            "Expected error about uint56 being rounded to uint64");
+}
+
+#[test]
+fn test_error_for_int96_with_strict() {
+    let src = r#"contract test {
+        function test_int96(int96 a) public returns (int128) {
+            return int128(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about int96 being rounded to int128
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for int96 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("int96") && msg.contains("int128")), 
+            "Expected error about int96 being rounded to int128");
+}
+
+#[test]
+fn test_error_for_uint96_with_strict() {
+    let src = r#"contract test {
+        function test_uint96(uint96 a) public returns (uint128) {
+            return uint128(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about uint96 being rounded to uint128
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for uint96 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("uint96") && msg.contains("uint128")), 
+            "Expected error about uint96 being rounded to uint128");
+}
+
+#[test]
+fn test_error_for_int200_with_strict() {
+    let src = r#"contract test {
+        function test_int200(int200 a) public returns (int256) {
+            return int256(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about int200 being rounded to int256
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for int200 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("int200") && msg.contains("int256")), 
+            "Expected error about int200 being rounded to int256");
+}
+
+#[test]
+fn test_error_for_uint200_with_strict() {
+    let src = r#"contract test {
+        function test_uint200(uint200 a) public returns (uint256) {
+            return uint256(a);
+        }
+    }"#;
+    
+    let diagnostics = build_with_strict_soroban_types(src, true);
+    
+    // Should have an error about uint200 being rounded to uint256
+    let errors: Vec<_> = diagnostics.iter().filter(|d| d.level == solang_parser::diagnostics::Level::Error).collect();
+    assert!(!errors.is_empty(), "Expected errors for uint200 rounding with strict mode");
+    
+    let error_messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
+    assert!(error_messages.iter().any(|msg| msg.contains("uint200") && msg.contains("uint256")), 
+            "Expected error about uint200 being rounded to uint256");
+}

+ 2 - 0
tests/soroban_testcases/mod.rs

@@ -2,6 +2,8 @@
 mod auth;
 mod constructor;
 mod cross_contract_calls;
+mod integer_width_rounding;
+mod integer_width_warnings;
 mod mappings;
 mod math;
 mod print;

+ 1 - 0
tests/undefined_variable_detection.rs

@@ -25,6 +25,7 @@ fn parse_and_codegen(src: &'static str) -> Namespace {
         generate_debug_information: false,
         log_runtime_errors: false,
         log_prints: true,
+        strict_soroban_types: false,
         #[cfg(feature = "wasm_opt")]
         wasm_opt: None,
         soroban_version: None,