From 1a08c7a08f4b39dbe1c17079eeca6d0d167ab347 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Sun, 24 May 2026 20:16:15 -0300 Subject: [PATCH 1/6] Check ref mut mutability for method call arguments The mutability check for `ref mut` parameters was only performed on free function and associated function calls, not on method calls via dot syntax. This allowed passing immutable variables to `ref mut` method parameters without any compiler error. Extend `gather_mutability()` to recurse through struct field access and tuple element access, and add the same mutability check to `type_check_method_application` that `function_application` already has. Closes #7621 --- .../src/language/ty/expression/expression.rs | 3 +++ .../typed_expression/method_application.rs | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sway-core/src/language/ty/expression/expression.rs b/sway-core/src/language/ty/expression/expression.rs index fd1367f5ece..7122a9d456f 100644 --- a/sway-core/src/language/ty/expression/expression.rs +++ b/sway-core/src/language/ty/expression/expression.rs @@ -633,6 +633,9 @@ impl TyExpression { pub(crate) fn gather_mutability(&self) -> VariableMutability { match &self.expression { TyExpressionVariant::VariableExpression { mutability, .. } => *mutability, + TyExpressionVariant::ConstantExpression { .. } => VariableMutability::Immutable, + TyExpressionVariant::StructFieldAccess { prefix, .. } + | TyExpressionVariant::TupleElemAccess { prefix, .. } => prefix.gather_mutability(), _ => VariableMutability::Immutable, } } diff --git a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs index b15c620e47c..d861927c817 100644 --- a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs +++ b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs @@ -195,6 +195,30 @@ pub(crate) fn type_check_method_application( ); } + // check for matching mutability of arguments + for (index, arg) in args_buf.iter().enumerate() { + let param_index = if method.is_contract_call { + if index == 0 { + continue; + } + index - 1 + } else { + index + }; + if let Some(param) = method.parameters.get(param_index) { + if param.is_self() { + continue; + } + let param_mutability = + ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); + if param_mutability.is_mutable() && arg.gather_mutability().is_immutable() { + handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { + span: arg.span.clone(), + }); + } + } + } + // check the method visibility if span.source_id() != method.span.source_id() && method.visibility.is_private() { return Err(handler.emit_err(CompileError::CallingPrivateLibraryMethod { @@ -1043,3 +1067,4 @@ pub(crate) fn monomorphize_method( Ok(decl_ref) } + From 009c6ca57fb24e9a8211c20d64efbb22b46b45a5 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Sun, 24 May 2026 20:16:29 -0300 Subject: [PATCH 2/6] Fix immutable arguments passed to ref mut method parameters Hasher::write passed its immutable `bytes` parameter directly to `Bytes::append` which requires `ref mut`. Rebind as mutable local before the call. Also fix the same pattern in the reexport_paths test. --- sway-lib-std/src/hash.sw | 3 ++- .../should_pass/language/reexport/reexport_paths/src/tests.sw | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sway-lib-std/src/hash.sw b/sway-lib-std/src/hash.sw index 7e4277ca19f..10c85baf277 100644 --- a/sway-lib-std/src/hash.sw +++ b/sway-lib-std/src/hash.sw @@ -31,7 +31,8 @@ impl Hasher { /// Note that the length of `bytes` is not appended to the /// `Hasher`, just the content. pub fn write(ref mut self, bytes: Bytes) { - self.bytes.append(bytes); + let mut b = bytes; + self.bytes.append(b); } /// Appends bytes from the `slice` to this `Hasher`. diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/reexport/reexport_paths/src/tests.sw b/test/src/e2e_vm_tests/test_programs/should_pass/language/reexport/reexport_paths/src/tests.sw index bbce7b1cf47..29f71b41f0b 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/language/reexport/reexport_paths/src/tests.sw +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/reexport/reexport_paths/src/tests.sw @@ -121,7 +121,7 @@ pub fn run_all_tests() -> u64 { assert(items_1_trait_teststruct_1_res); - let hasher = mk_hasher(); + let mut hasher = mk_hasher(); teststruct_1.hash(hasher); From da57ea7a41b4f2e3946828192b57ec85cf8bf21e Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Mon, 25 May 2026 09:09:14 -0300 Subject: [PATCH 3/6] Address review: use __addr_of for pointer semantics, add mcpi test --- .../ref_mut_method_mismatch/Forc.lock | 3 ++ .../ref_mut_method_mismatch/Forc.toml | 6 ++++ .../ref_mut_method_mismatch/src/main.sw | 33 +++++++++++++++++++ .../ref_mut_method_mismatch/test.toml | 17 ++++++++++ 4 files changed, 59 insertions(+) create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/test.toml diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.lock new file mode 100644 index 00000000000..acee34c9291 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.lock @@ -0,0 +1,3 @@ +[[package]] +name = "ref_mut_method_mismatch" +source = "member" diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.toml new file mode 100644 index 00000000000..217a0a23487 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "ref_mut_method_mismatch" +implicit-std = false diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/src/main.sw new file mode 100644 index 00000000000..316904a199d --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/src/main.sw @@ -0,0 +1,33 @@ +script; + +struct Point { + x: u64, + y: u64, +} + +impl Point { + fn mutate(self, ref mut _v: u64) { } +} + +const MY_CONST: u64 = 10; + +fn main() { + // immutable variable + let x = 42u64; + Point { x: 1, y: 2 }.mutate(x); + + // struct field access on immutable binding + let p = Point { x: 1, y: 2 }; + Point { x: 1, y: 2 }.mutate(p.x); + + // tuple element access on immutable binding + let t = (1u64, 2u64); + Point { x: 1, y: 2 }.mutate(t.0); + + // constant + Point { x: 1, y: 2 }.mutate(MY_CONST); + + // mutable variable — should NOT error + let mut m = 99u64; + Point { x: 1, y: 2 }.mutate(m); +} diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/test.toml b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/test.toml new file mode 100644 index 00000000000..8458107f1e1 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/ref_mut_method_mismatch/test.toml @@ -0,0 +1,17 @@ +category = "fail" + +# immutable variable +# check: $()Point { x: 1, y: 2 }.mutate(x); +# nextln: $()Cannot pass immutable argument to mutable parameter. + +# struct field access +# check: $()Point { x: 1, y: 2 }.mutate(p.x); +# nextln: $()Cannot pass immutable argument to mutable parameter. + +# tuple element access +# check: $()Point { x: 1, y: 2 }.mutate(t.0); +# nextln: $()Cannot pass immutable argument to mutable parameter. + +# constant +# check: $()Point { x: 1, y: 2 }.mutate(MY_CONST); +# nextln: $()Cannot pass immutable argument to mutable parameter. From 5cff9daaf126daa889e0d2965b9ce43f80ae4236 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Thu, 4 Jun 2026 16:05:11 -0300 Subject: [PATCH 4/6] Fix contract-call index skew in ref mut mutability check The mutability check ran in a separate loop that assumed args_buf[0] was always the contract-call receiver. The type-check loop only pushes that receiver when its first-pass type is present, so when it is absent args_buf is shorter and the index-1 mapping pairs arguments with the wrong parameters. Fold the check into the type-check loop so it reuses the same source-argument-to-parameter mapping and stays correct. --- .../typed_expression/method_application.rs | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs index 170174ac810..eeed2a06609 100644 --- a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs +++ b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs @@ -162,61 +162,57 @@ pub(crate) fn type_check_method_application( index }; - if let (Some(arg), _, false) = arg_opt { - if let Some(param) = method.parameters.get(param_index) { - if coercion_check.check(arg.return_type, param.type_argument.type_id) { - // If argument type coerces to resolved method parameter type skip second type_check. - args_buf.push_back(arg); - continue; - } - } else { - args_buf.push_back(arg); - continue; - } - } - - // We type check the argument expression again this time throwing out the error. - let ctx = if let Some(param) = method.parameters.get(param_index) { - // We now try to type check it again, this time with the type annotation. - ctx.by_ref() - .with_help_text( - "Function application argument type must match function parameter type.", - ) - .with_type_annotation(param.type_argument.type_id) + let param = method.parameters.get(param_index); + + // Reuse the first-pass typed argument when it already coerces to the + // resolved parameter type (or there is no matching parameter), otherwise + // type check the argument expression again, this time with the type + // annotation and throwing out the error. + let (arg_typed, _, arg_had_err) = arg_opt; + let reuse = !arg_had_err + && arg_typed.is_some() + && param + .map(|param| { + coercion_check + .check(arg_typed.as_ref().unwrap().return_type, param.type_argument.type_id) + }) + .unwrap_or(true); + + let typed_arg = if reuse { + arg_typed.unwrap() } else { - ctx.by_ref() - .with_help_text("") - .with_type_annotation(type_engine.new_unknown()) - }; - - args_buf.push_back( + let ctx = if let Some(param) = param { + ctx.by_ref() + .with_help_text( + "Function application argument type must match function parameter type.", + ) + .with_type_annotation(param.type_argument.type_id) + } else { + ctx.by_ref() + .with_help_text("") + .with_type_annotation(type_engine.new_unknown()) + }; ty::TyExpression::type_check(handler, ctx, arg) - .unwrap_or_else(|err| ty::TyExpression::error(err, span.clone(), engines)), - ); - } - - // check for matching mutability of arguments - for (index, arg) in args_buf.iter().enumerate() { - let param_index = if method.is_contract_call { - if index == 0 { - continue; - } - index - 1 - } else { - index + .unwrap_or_else(|err| ty::TyExpression::error(err, span.clone(), engines)) }; - if let Some(param) = method.parameters.get(param_index) { - if param.is_self() { - continue; - } - let param_mutability = - ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); - if param_mutability.is_mutable() && arg.gather_mutability().is_immutable() { - handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { - span: arg.span.clone(), - }); + + // Check that an immutable argument is not passed to a `ref mut` parameter. + // This keys off the same source-argument-to-parameter mapping used above, + // so it stays correct even when the contract-call receiver is not pushed + // into `args_buf`. + if let Some(param) = param { + if !param.is_self() { + let param_mutability = + ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); + if param_mutability.is_mutable() && typed_arg.gather_mutability().is_immutable() { + handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { + span: typed_arg.span.clone(), + }); + } } } + + args_buf.push_back(typed_arg); } // check the method visibility From 9126cafbf67dd2c6486dce84164fcad9949cc754 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Thu, 4 Jun 2026 16:27:27 -0300 Subject: [PATCH 5/6] Extract shared check_arg_mutability helper Both function and method application emitted the same new_from_ref_mut + gather_mutability + ImmutableArgumentToMutableParameter check inline. Pull it into one check_arg_mutability helper so the two paths can't drift. Also rename the misleading arg_had_err binding to needs_recheck, since it tracks needs_second_pass (errors or numerics). --- .../typed_expression/function_application.rs | 23 +++++++++++++------ .../typed_expression/method_application.rs | 14 ++++------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/function_application.rs b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/function_application.rs index 288b068983e..a1af7e2b28f 100644 --- a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/function_application.rs +++ b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/function_application.rs @@ -216,13 +216,7 @@ fn unify_arguments_and_parameters( } // check for matching mutability - let param_mutability = - ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); - if arg.gather_mutability().is_immutable() && param_mutability.is_mutable() { - handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { - span: arg.span.clone(), - }); - } + check_arg_mutability(handler, param, &arg); typed_arguments_and_names.push((param.name.clone(), arg)); } @@ -231,6 +225,21 @@ fn unify_arguments_and_parameters( }) } +/// Emits an error if an immutable `arg` is passed to a `ref mut` `param`. +pub(crate) fn check_arg_mutability( + handler: &Handler, + param: &ty::TyFunctionParameter, + arg: &ty::TyExpression, +) { + let param_mutability = + ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); + if param_mutability.is_mutable() && arg.gather_mutability().is_immutable() { + handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { + span: arg.span.clone(), + }); + } +} + pub(crate) fn check_function_arguments_arity( handler: &Handler, arguments_len: usize, diff --git a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs index eeed2a06609..734688cd092 100644 --- a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs +++ b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs @@ -15,7 +15,7 @@ use ast_elements::{ type_argument::GenericTypeArgument, type_parameter::{ConstGenericExpr, GenericTypeParameter}, }; -use ast_node::typed_expression::check_function_arguments_arity; +use ast_node::typed_expression::{check_arg_mutability, check_function_arguments_arity}; use indexmap::IndexMap; use itertools::izip; use std::collections::{BTreeMap, HashMap, VecDeque}; @@ -168,8 +168,8 @@ pub(crate) fn type_check_method_application( // resolved parameter type (or there is no matching parameter), otherwise // type check the argument expression again, this time with the type // annotation and throwing out the error. - let (arg_typed, _, arg_had_err) = arg_opt; - let reuse = !arg_had_err + let (arg_typed, _, needs_recheck) = arg_opt; + let reuse = !needs_recheck && arg_typed.is_some() && param .map(|param| { @@ -202,13 +202,7 @@ pub(crate) fn type_check_method_application( // into `args_buf`. if let Some(param) = param { if !param.is_self() { - let param_mutability = - ty::VariableMutability::new_from_ref_mut(param.is_reference, param.is_mutable); - if param_mutability.is_mutable() && typed_arg.gather_mutability().is_immutable() { - handler.emit_err(CompileError::ImmutableArgumentToMutableParameter { - span: typed_arg.span.clone(), - }); - } + check_arg_mutability(handler, param, &typed_arg); } } From 6367ec9b0884160e74a09361a22a1b3c08670ef4 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Thu, 4 Jun 2026 16:45:54 -0300 Subject: [PATCH 6/6] Apply rustfmt to method application argument loop --- .../expression/typed_expression/method_application.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs index 734688cd092..b9e13d7193a 100644 --- a/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs +++ b/sway-core/src/semantic_analysis/ast_node/expression/typed_expression/method_application.rs @@ -173,8 +173,10 @@ pub(crate) fn type_check_method_application( && arg_typed.is_some() && param .map(|param| { - coercion_check - .check(arg_typed.as_ref().unwrap().return_type, param.type_argument.type_id) + coercion_check.check( + arg_typed.as_ref().unwrap().return_type, + param.type_argument.type_id, + ) }) .unwrap_or(true); @@ -1063,4 +1065,3 @@ pub(crate) fn monomorphize_method( Ok(decl_ref) } -