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

feat: warn on unnecessary unsafe blocks #6867

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions compiler/noirc_frontend/src/elaborator/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
Kind, QuotedType, Shared, StructType, Type,
};

use super::{Elaborator, LambdaContext};
use super::{Elaborator, LambdaContext, UnsafeBlockStatus};

impl<'context> Elaborator<'context> {
pub(crate) fn elaborate_expression(&mut self, expr: Expression) -> (ExprId, Type) {
Expand All @@ -59,7 +59,7 @@ impl<'context> Elaborator<'context> {
return self.elaborate_comptime_block(comptime, expr.span)
}
ExpressionKind::Unsafe(block_expression, _) => {
self.elaborate_unsafe_block(block_expression)
self.elaborate_unsafe_block(block_expression, expr.span)
}
ExpressionKind::Resolved(id) => return (id, self.interner.id_type(id)),
ExpressionKind::Interned(id) => {
Expand Down Expand Up @@ -140,15 +140,36 @@ impl<'context> Elaborator<'context> {
(HirBlockExpression { statements }, block_type)
}

fn elaborate_unsafe_block(&mut self, block: BlockExpression) -> (HirExpression, Type) {
fn elaborate_unsafe_block(
&mut self,
block: BlockExpression,
span: Span,
) -> (HirExpression, Type) {
// Before entering the block we cache the old value of `in_unsafe_block` so it can be restored.
let old_in_unsafe_block = self.in_unsafe_block;
self.in_unsafe_block = true;
let old_in_unsafe_block = self.unsafe_block_status;
let is_nested_unsafe_block =
!matches!(old_in_unsafe_block, UnsafeBlockStatus::NotInUnsafeBlock);
if is_nested_unsafe_block {
let span = Span::from(span.start()..span.start() + 6); // Only highlight the `unsafe` keyword
self.push_err(TypeCheckError::NestedUnsafeBlock { span });
}

self.unsafe_block_status = UnsafeBlockStatus::InUnsafeBlockWithoutUnconstrainedCalls;

let (hir_block_expression, typ) = self.elaborate_block_expression(block);

// Finally, we restore the original value of `self.in_unsafe_block`.
self.in_unsafe_block = old_in_unsafe_block;
if let UnsafeBlockStatus::InUnsafeBlockWithoutUnconstrainedCalls = self.unsafe_block_status
{
let span = Span::from(span.start()..span.start() + 6); // Only highlight the `unsafe` keyword
self.push_err(TypeCheckError::UnnecessaryUnsafeBlock { span });
}

// Finally, we restore the original value of `self.in_unsafe_block`,
// but only if this isn't a nested unsafe block (that way if we found an unconstrained call
// for this unsafe block we'll also consider the outer one as finding one, and we don't double error)
if !is_nested_unsafe_block {
self.unsafe_block_status = old_in_unsafe_block;
}

(HirExpression::Unsafe(hir_block_expression), typ)
}
Expand Down
14 changes: 12 additions & 2 deletions compiler/noirc_frontend/src/elaborator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ pub struct LambdaContext {
pub scope_index: usize,
}

/// Determines whether we are in an unsafe block and, if so, whether
/// any unconstrained calls were found in it (because if not we'll warn
/// that the unsafe block is not needed).
#[derive(Copy, Clone)]
enum UnsafeBlockStatus {
NotInUnsafeBlock,
InUnsafeBlockWithoutUnconstrainedCalls,
InUnsafeBlockWithConstrainedCalls,
}

pub struct Elaborator<'context> {
scopes: ScopeForest,

Expand All @@ -90,7 +100,7 @@ pub struct Elaborator<'context> {

pub(crate) file: FileId,

in_unsafe_block: bool,
unsafe_block_status: UnsafeBlockStatus,
nested_loops: usize,

/// Contains a mapping of the current struct or functions's generics to
Expand Down Expand Up @@ -202,7 +212,7 @@ impl<'context> Elaborator<'context> {
def_maps,
usage_tracker,
file: FileId::dummy(),
in_unsafe_block: false,
unsafe_block_status: UnsafeBlockStatus::NotInUnsafeBlock,
nested_loops: 0,
generics: Vec::new(),
lambda_stack: Vec::new(),
Expand Down
12 changes: 9 additions & 3 deletions compiler/noirc_frontend/src/elaborator/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::{
Generics, Kind, ResolvedGeneric, Type, TypeBinding, TypeBindings, UnificationError,
};

use super::{lints, path_resolution::PathResolutionItem, Elaborator};
use super::{lints, path_resolution::PathResolutionItem, Elaborator, UnsafeBlockStatus};

pub const SELF_TYPE_NAME: &str = "Self";

Expand Down Expand Up @@ -1483,8 +1483,14 @@ impl<'context> Elaborator<'context> {
func_type_is_unconstrained || self.is_unconstrained_call(call.func);
let crossing_runtime_boundary = is_current_func_constrained && is_unconstrained_call;
if crossing_runtime_boundary {
if !self.in_unsafe_block {
self.push_err(TypeCheckError::Unsafe { span });
match self.unsafe_block_status {
UnsafeBlockStatus::NotInUnsafeBlock => {
self.push_err(TypeCheckError::Unsafe { span });
}
UnsafeBlockStatus::InUnsafeBlockWithoutUnconstrainedCalls => {
self.unsafe_block_status = UnsafeBlockStatus::InUnsafeBlockWithConstrainedCalls;
}
UnsafeBlockStatus::InUnsafeBlockWithConstrainedCalls => (),
}

if let Some(called_func_id) = self.interner.lookup_function_from_expr(&call.func) {
Expand Down
18 changes: 18 additions & 0 deletions compiler/noirc_frontend/src/hir/type_check/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ pub enum TypeCheckError {
CyclicType { typ: Type, span: Span },
#[error("Type annotations required before indexing this array or slice")]
TypeAnnotationsNeededForIndex { span: Span },
#[error("Unnecessary `unsafe` block")]
UnnecessaryUnsafeBlock { span: Span },
#[error("Unnecessary `unsafe` block")]
NestedUnsafeBlock { span: Span },
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -517,6 +521,20 @@ impl<'a> From<&'a TypeCheckError> for Diagnostic {
*span,
)
},
TypeCheckError::UnnecessaryUnsafeBlock { span } => {
Diagnostic::simple_warning(
"Unnecessary `unsafe` block".into(),
"".into(),
*span,
)
},
TypeCheckError::NestedUnsafeBlock { span } => {
Diagnostic::simple_warning(
"Unnecessary `unsafe` block".into(),
"Because it's nested inside another `unsafe` block".into(),
*span,
)
},
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions compiler/noirc_frontend/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3170,7 +3170,7 @@
}

#[test]
fn dont_infer_globals_to_u32_from_type_use() {

Check warning on line 3173 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 @@ -3200,7 +3200,7 @@
}

#[test]
fn dont_infer_partial_global_types() {

Check warning on line 3203 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 @@ -3438,7 +3438,7 @@

#[test]
fn unconditional_recursion_fail() {
let srcs = vec![

Check warning on line 3441 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 @@ -3500,7 +3500,7 @@
"#,
];

for src in srcs {

Check warning on line 3503 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 All @@ -3519,7 +3519,7 @@

#[test]
fn unconditional_recursion_pass() {
let srcs = vec![

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

View workflow job for this annotation

GitHub Actions / Code

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

for src in srcs {

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

View workflow job for this annotation

GitHub Actions / Code

Unknown word (srcs)
assert_no_errors(src);
}
}
Expand Down Expand Up @@ -3875,3 +3875,43 @@
CompilationError::ResolverError(ResolverError::DependencyCycle { .. })
)));
}

#[test]
fn warns_on_unneeded_unsafe() {
let src = r#"
fn main() {
unsafe {
foo()
}
}

fn foo() {}
"#;
let errors = get_program_errors(src);
assert_eq!(errors.len(), 1);
assert!(matches!(
&errors[0].0,
CompilationError::TypeError(TypeCheckError::UnnecessaryUnsafeBlock { .. })
));
}

#[test]
fn warns_on_nested_unsafe() {
let src = r#"
fn main() {
unsafe {
unsafe {
foo()
}
}
}

unconstrained fn foo() {}
"#;
let errors = get_program_errors(src);
assert_eq!(errors.len(), 1);
assert!(matches!(
&errors[0].0,
CompilationError::TypeError(TypeCheckError::NestedUnsafeBlock { .. })
));
}
2 changes: 1 addition & 1 deletion noir_stdlib/src/collections/map.nr
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ impl<K, V, let N: u32, B> HashMap<K, V, N, B> {

for slot in self._table {
if slot.is_valid() {
let (_, value) = unsafe { slot.key_value_unchecked() };
let (_, value) = slot.key_value_unchecked();
values.push(value);
}
}
Expand Down
2 changes: 1 addition & 1 deletion noir_stdlib/src/meta/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub comptime fn derive(s: StructDefinition, traits: [TraitDefinition]) -> Quoted
let mut result = quote {};

for trait_to_derive in traits {
let handler = unsafe { HANDLERS.get(trait_to_derive) };
let handler = HANDLERS.get(trait_to_derive);
assert(handler.is_some(), f"No derive function registered for `{trait_to_derive}`");

let trait_impl = handler.unwrap()(s);
Expand Down
Loading