Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(frontend)!: Restrict capturing mutable variable in lambdas #7488

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
15 changes: 14 additions & 1 deletion compiler/noirc_frontend/src/elaborator/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,20 @@ impl<'context> Elaborator<'context> {
});

let captured_vars = vecmap(&lambda_context.captures, |capture| {
self.interner.definition_type(capture.ident.id)
let typ = self.interner.definition_type(capture.ident.id);
if let Some(definition) = self.interner.try_definition(capture.ident.id) {
if definition.mutable && !typ.is_mutable_ref() {
let location = definition.location;
self.push_err(
TypeCheckError::MutableCaptureWithoutRef {
name: definition.name.clone(),
span: location.span,
},
location.file,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to issue an error here? The original intent for lambda captures was not to prevent capturing a mutable variable completely, but rather that any mutable variables should be captured by value and thus be immutable within the lambda. E.g. the equivalent of let mut x = Foo(); bar(x) where fn bar(y: Foo) { ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. This is what we currently allow. Taking the example from #4795 (comment):

fn main() {
    let mut x = 3;

    let f = || {
        x += 2;
    };

    f();
    assert(x == 5);
}

This example will fail on master. assert(x == 3); passes.

Copy link
Contributor Author

@vezenovm vezenovm Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we desired to have an error, but I could just have this be a warning.

Otherwise, lambdas are working as expected and we just need to provide greater documentation/education.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to capture a mutable variable immutably it would require devs to reassign their variable before capturing in a lambda. This is a bit cumbersome in my opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of the original issue, but maybe I was misunderstanding this check. I was suggesting that the original intent was to allow captures of x still but capture it immutably by value instead. So if you just did a print(x) in f that'd be allowed even if x was declared as let mut x = 3;

Can you add a test to ensure

fn main() {
        let mut x = 3;
        let f = || x;
        let _x2 = f();
        assert(x == 3);
    }

(or similar) is valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. I misunderstood as I thought from the original issue you desired this restrictive of a check. I'll update and add this test.

}
}
typ
});

let env_type =
Expand Down
7 changes: 7 additions & 0 deletions compiler/noirc_frontend/src/hir/type_check/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ pub enum TypeCheckError {
VariableMustBeMutable { name: String, span: Span },
#[error("Cannot mutate immutable variable `{name}`")]
CannotMutateImmutableVariable { name: String, span: Span },
#[error("Variable {name} captured in lambda must be a mutable reference")]
MutableCaptureWithoutRef { name: String, span: Span },
#[error("No method named '{method_name}' found for type '{object_type}'")]
UnresolvedMethodCall { method_name: String, object_type: Type, span: Span },
#[error("Cannot invoke function field '{method_name}' on type '{object_type}' as a method")]
Expand Down Expand Up @@ -370,6 +372,11 @@ impl<'a> From<&'a TypeCheckError> for Diagnostic {
| TypeCheckError::InvalidShiftSize { span } => {
Diagnostic::simple_error(error.to_string(), String::new(), *span)
}
TypeCheckError::MutableCaptureWithoutRef { name, span } => Diagnostic::simple_error(
format!("Mutable variable {name} captured in lambda must be a mutable reference"),
"Use '&mut' instead of 'mut' to capture a mutable variable.".to_string(),
*span,
),
TypeCheckError::PublicReturnType { typ, span } => Diagnostic::simple_error(
"Functions cannot declare a public return type".to_string(),
format!("return type is {typ}"),
Expand Down
4 changes: 4 additions & 0 deletions compiler/noirc_frontend/src/hir_def/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,10 @@
}
}

pub(crate) fn is_mutable_ref(&self) -> bool {
matches!(self.follow_bindings_shallow().as_ref(), Type::MutableReference(_))
}

/// True if this type can be used as a parameter to `main` or a contract function.
/// This is only false for unsized types like slices or slices that do not make sense
/// as a program input such as named generics or mutable references.
Expand Down Expand Up @@ -2347,7 +2351,7 @@
}

let recur_on_binding = |id, replacement: &Type| {
// Prevent recuring forever if there's a `T := T` binding

Check warning on line 2354 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (recuring)
if replacement.type_variable_id() == Some(id) {
replacement.clone()
} else {
Expand Down Expand Up @@ -2433,7 +2437,7 @@
Type::Tuple(fields)
}
Type::Forall(typevars, typ) => {
// Trying to substitute_helper a variable de, substitute_bound_typevarsfined within a nested Forall

Check warning on line 2440 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (typevarsfined)
// is usually impossible and indicative of an error in the type checker somewhere.
for var in typevars {
assert!(!type_bindings.contains_key(&var.id()));
Expand Down Expand Up @@ -2599,7 +2603,7 @@
}
}

/// Follow bindings if this is a type variable or generic to the first non-typevariable

Check warning on line 2606 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (typevariable)
/// type. Unlike `follow_bindings`, this won't recursively follow any bindings on any
/// fields or arguments of this type.
pub fn follow_bindings_shallow(&self) -> Cow<Type> {
Expand All @@ -2624,7 +2628,7 @@

/// Replace any `Type::NamedGeneric` in this type with a `Type::TypeVariable`
/// using to the same inner `TypeVariable`. This is used during monomorphization
/// to bind to named generics since they are unbindable during type checking.

Check warning on line 2631 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (unbindable)
pub fn replace_named_generics_with_type_variables(&mut self) {
match self {
Type::FieldElement
Expand Down Expand Up @@ -3095,7 +3099,7 @@
len.hash(state);
env.hash(state);
}
Type::Tuple(elems) => elems.hash(state),

Check warning on line 3102 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (elems)

Check warning on line 3102 in compiler/noirc_frontend/src/hir_def/types.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (elems)
Type::DataType(def, args) => {
def.hash(state);
args.hash(state);
Expand Down
56 changes: 56 additions & 0 deletions compiler/noirc_frontend/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3197,7 +3197,7 @@
}

#[test]
fn dont_infer_globals_to_u32_from_type_use() {

Check warning on line 3200 in compiler/noirc_frontend/src/tests.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (dont)
let src = r#"
global ARRAY_LEN = 3;
global STR_LEN: _ = 2;
Expand Down Expand Up @@ -3227,7 +3227,7 @@
}

#[test]
fn dont_infer_partial_global_types() {

Check warning on line 3230 in compiler/noirc_frontend/src/tests.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (dont)
let src = r#"
pub global ARRAY: [Field; _] = [0; 3];
pub global NESTED_ARRAY: [[Field; _]; 3] = [[]; 3];
Expand Down Expand Up @@ -3470,7 +3470,7 @@
// wouldn't panic due to infinite recursion, but the errors asserted here
// come from the compilation checks, which does static analysis to catch the
// problem before it even has a chance to cause a panic.
let srcs = vec![

Check warning on line 3473 in compiler/noirc_frontend/src/tests.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (srcs)
r#"
fn main() {
main()
Expand Down Expand Up @@ -3532,7 +3532,7 @@
"#,
];

for src in srcs {

Check warning on line 3535 in compiler/noirc_frontend/src/tests.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (srcs)
let errors = get_program_errors(src);
assert!(
!errors.is_empty(),
Expand Down Expand Up @@ -4475,3 +4475,59 @@

assert!(matches!(error.reason(), Some(ParserErrorReason::ExperimentalFeature(_))));
}

#[test]
fn mutate_with_reference_in_lambda() {
let src = r#"
fn main() {
let x = &mut 3;
let f = || {
*x += 2;
};
f();
assert(*x == 5);
}
"#;

let errors = get_program_errors(src);
assert_eq!(errors.len(), 0);
}

#[test]
fn mutate_with_reference_marked_mutable_in_lambda() {
let src = r#"
fn main() {
let mut x = &mut 3;
let f = || {
*x += 2;
};
f();
assert(*x == 5);
}
"#;

let errors = get_program_errors(src);
assert_eq!(errors.len(), 0);
}

#[test]
fn deny_capturing_mut_variable_without_reference_in_lambda() {
let src = r#"
fn main() {
let mut x = 3;
let f = || {
x += 2;
};
f();
assert(x == 5);
}
"#;

let errors = get_program_errors(src);
assert_eq!(errors.len(), 1);

assert!(matches!(
errors[0].0,
CompilationError::TypeError(TypeCheckError::MutableCaptureWithoutRef { .. })
));
}
Loading