Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,12 @@ impl FunctionContext<'_> {
))
}
UnaryOp::Reference { mutable: _ } => {
Ok(self.codegen_reference(&unary.rhs)?.map(|rhs| {
let rhs = self.codegen_reference(&unary.rhs)?;
// If skip is set then `rhs` is a member access expression which is already a reference
if unary.skip {
return Ok(rhs);
}
Ok(rhs.map(|rhs| {
match rhs {
value::Value::Normal(value) => {
let rhs_type = self.builder.current_function.dfg.type_of_value(value);
Expand Down
91 changes: 71 additions & 20 deletions compiler/noirc_frontend/src/elaborator/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ impl Elaborator<'_> {
ExpressionKind::Constrain(constrain) => self.elaborate_constrain(constrain),
ExpressionKind::Constructor(constructor) => self.elaborate_constructor(*constructor),
ExpressionKind::MemberAccess(access) => {
return self.elaborate_member_access(*access, expr.location);
let (expr_id, typ, _) = self.elaborate_member_access(*access, expr.location, false);
return (expr_id, typ);
}
ExpressionKind::Cast(cast) => self.elaborate_cast(*cast, expr.location),
ExpressionKind::Infix(infix) => return self.elaborate_infix(*infix, expr.location),
Expand Down Expand Up @@ -118,6 +119,23 @@ impl Elaborator<'_> {
(id, typ)
}

/// Elaborate a member access expression without adding the automatic dereferencing operations
/// to it, treating it as an offset instead. This also returns a boolean indicating whether the
/// result skipped any required auto-derefs (and thus needs dereferencing to be used as a value
/// instead of a reference). This flag is used when `&mut foo.bar.baz` is used to cancel out
/// the `&mut`.
fn elaborate_reference_expression(&mut self, expr: Expression) -> (ExprId, Type, bool) {
match expr.kind {
ExpressionKind::MemberAccess(access) => {
self.elaborate_member_access(*access, expr.location, true)
}
_ => {
let (expr_id, typ) = self.elaborate_expression(expr);
(expr_id, typ, false)
}
}
}

fn elaborate_interned_statement_as_expr(
&mut self,
id: InternedStatementKind,
Expand Down Expand Up @@ -403,40 +421,60 @@ impl Elaborator<'_> {

fn elaborate_prefix(&mut self, prefix: PrefixExpression, location: Location) -> (ExprId, Type) {
let rhs_location = prefix.rhs.location;
let operator = prefix.operator;

let (rhs, rhs_type) = self.elaborate_expression(prefix.rhs);
let trait_method_id = self.interner.get_prefix_operator_trait_method(&prefix.operator);
let (rhs, rhs_type, skip_op) = if matches!(operator, UnaryOp::Reference { .. }) {
let (rhs, rhs_type, needs_deref) = self.elaborate_reference_expression(prefix.rhs);

let operator = prefix.operator;
// If the reference expression delayed a needed deref, we can skip the `&mut _`
// operation since the expression is already a reference.
(rhs, rhs_type, needs_deref)
} else {
let (rhs, rhs_type) = self.elaborate_expression(prefix.rhs);
(rhs, rhs_type, false)
};

let trait_method_id = self.interner.get_prefix_operator_trait_method(&operator);

if let UnaryOp::Reference { mutable } = operator {
if mutable {
self.check_can_mutate(rhs, rhs_location);
// If skip_op is set we already know we have a mutable reference
if !skip_op {
self.check_can_mutate(rhs, rhs_location);
}
} else {
self.use_unstable_feature(UnstableFeature::Ownership, location);
}
}

let expr = HirExpression::Prefix(HirPrefixExpression { operator, rhs, trait_method_id });
let expr = HirExpression::Prefix(HirPrefixExpression {
operator,
rhs,
trait_method_id,
skip: skip_op,
});
let expr_id = self.interner.push_expr(expr);
self.interner.push_expr_location(expr_id, location);

let result = self.prefix_operand_type_rules(&operator, &rhs_type, location);
let typ = self.handle_operand_type_rules_result(
result,
&rhs_type,
trait_method_id,
expr_id,
location,
);
let typ = if skip_op {
rhs_type
} else {
let result = self.prefix_operand_type_rules(&operator, &rhs_type, location);
self.handle_operand_type_rules_result(
result,
&rhs_type,
trait_method_id,
expr_id,
location,
)
};

self.interner.push_expr_type(expr_id, typ.clone());
(expr_id, typ)
}

pub(super) fn check_can_mutate(&mut self, expr_id: ExprId, location: Location) {
let expr = self.interner.expression(&expr_id);
match expr {
match self.interner.expression(&expr_id) {
HirExpression::Ident(hir_ident, _) => {
if let Some(definition) = self.interner.try_definition(hir_ident.id) {
let name = definition.name.clone();
Expand Down Expand Up @@ -1014,20 +1052,33 @@ impl Elaborator<'_> {
ret
}

/// This method also returns whether or not its lhs still needs to be dereferenced depending on
/// `is_offset`:
/// - `is_offset = false`: Auto-dereferencing will occur, and this will always return false
/// - `is_offset = true`: Auto-dereferencing is disabled, and this will return true if the lhs
/// is a reference.
fn elaborate_member_access(
&mut self,
access: MemberAccessExpression,
location: Location,
) -> (ExprId, Type) {
let (lhs, lhs_type) = self.elaborate_expression(access.lhs);
is_offset: bool,
) -> (ExprId, Type, bool) {
// We don't need the boolean 'skipped auto-derefs' from elaborate_reference_expression
// since if we have skipped any then `lhs_type` will be a reference and we will need to
// skip the deref (if is_offset is true) here anyway to extract the field out of the reference.
// This is more reliable than using the boolean return value here since the return value
// doesn't account for reference variables which we need to account for.
let (lhs, lhs_type, _) = self.elaborate_reference_expression(access.lhs);
let is_reference = lhs_type.is_ref();

let rhs = access.rhs;
let rhs_location = rhs.location();
// `is_offset` is only used when lhs is a reference and we want to return a reference to rhs
let access = HirMemberAccess { lhs, rhs, is_offset: false };
let access = HirMemberAccess { lhs, rhs, is_offset };
let expr_id = self.intern_expr(HirExpression::MemberAccess(access.clone()), location);
let typ = self.type_check_member_access(access, expr_id, lhs_type, rhs_location);
self.interner.push_expr_type(expr_id, typ.clone());
(expr_id, typ)
(expr_id, typ, is_offset && is_reference)
}

pub fn intern_expr(&mut self, expr: HirExpression, location: Location) -> ExprId {
Expand Down
27 changes: 11 additions & 16 deletions compiler/noirc_frontend/src/elaborator/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,11 +999,10 @@ impl Elaborator<'_> {
if let Type::Reference(element, _mut) = typ.follow_bindings() {
let location = self.interner.id_location(object);

let object = self.interner.push_expr(HirExpression::Prefix(HirPrefixExpression {
operator: UnaryOp::Dereference { implicitly_added: true },
rhs: object,
trait_method_id: None,
}));
let object = self.interner.push_expr(HirExpression::Prefix(HirPrefixExpression::new(
UnaryOp::Dereference { implicitly_added: true },
object,
)));
self.interner.push_expr_type(object, element.as_ref().clone());
self.interner.push_expr_location(object, location);

Expand Down Expand Up @@ -1591,11 +1590,10 @@ impl Elaborator<'_> {

let dereference_lhs = |this: &mut Self, lhs_type, element| {
let old_lhs = *access_lhs;
*access_lhs = this.interner.push_expr(HirExpression::Prefix(HirPrefixExpression {
operator: crate::ast::UnaryOp::Dereference { implicitly_added: true },
rhs: old_lhs,
trait_method_id: None,
}));
*access_lhs = this.interner.push_expr(HirExpression::Prefix(HirPrefixExpression::new(
crate::ast::UnaryOp::Dereference { implicitly_added: true },
old_lhs,
)));
this.interner.push_expr_type(old_lhs, lhs_type);
this.interner.push_expr_type(*access_lhs, element);

Expand Down Expand Up @@ -2069,12 +2067,9 @@ impl Elaborator<'_> {

// If that didn't work, then wrap the whole expression in an `&mut`
*object = new_object.unwrap_or_else(|| {
let new_object =
self.interner.push_expr(HirExpression::Prefix(HirPrefixExpression {
operator: UnaryOp::Reference { mutable },
rhs: *object,
trait_method_id: None,
}));
let new_object = self.interner.push_expr(HirExpression::Prefix(
HirPrefixExpression::new(UnaryOp::Reference { mutable }, *object),
));
self.interner.push_expr_type(new_object, new_type);
self.interner.push_expr_location(new_object, location);
new_object
Expand Down
35 changes: 25 additions & 10 deletions compiler/noirc_frontend/src/hir/comptime/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use crate::{
};

use super::errors::{IResult, InterpreterError};
use super::value::{Closure, Value, unwrap_rc};
use super::value::{Closure, StructFields, Value, unwrap_rc};

mod builtin;
mod cast;
Expand Down Expand Up @@ -826,6 +826,10 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
_ => self.evaluate(prefix.rhs)?,
};

if prefix.skip {
return Ok(rhs);
}

if self.elaborator.interner.get_selected_impl_for_expression(id).is_some() {
self.evaluate_overloaded_prefix(prefix, rhs, id)
} else {
Expand Down Expand Up @@ -962,11 +966,11 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
Ok(Value::Enum(constructor.variant_index, fields, typ))
}

fn evaluate_access(&mut self, access: HirMemberAccess, id: ExprId) -> IResult<Value> {
let (fields, struct_type) = match self.evaluate(access.lhs)? {
Value::Struct(fields, typ) => (fields, typ),
fn get_fields(&mut self, value: Value, id: ExprId) -> IResult<(StructFields, Type)> {
match value {
Value::Struct(fields, typ) => Ok((fields, typ)),
Value::Tuple(fields) => {
let (fields, field_types): (HashMap<Rc<String>, Shared<Value>>, Vec<Type>) = fields
let (fields, field_types) = fields
.into_iter()
.enumerate()
.map(|(i, field)| {
Expand All @@ -975,14 +979,21 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
(key_val_pair, field_type)
})
.unzip();
(fields, Type::Tuple(field_types))
Ok((fields, Type::Tuple(field_types)))
}
Value::Pointer(element, ..) => self.get_fields(element.unwrap_or_clone(), id),
value => {
let location = self.elaborator.interner.expr_location(&id);
let typ = value.get_type().into_owned();
return Err(InterpreterError::NonTupleOrStructInMemberAccess { typ, location });
Err(InterpreterError::NonTupleOrStructInMemberAccess { typ, location })
}
};
}
}

fn evaluate_access(&mut self, access: HirMemberAccess, id: ExprId) -> IResult<Value> {
let lhs = self.evaluate(access.lhs)?;
let is_offset = access.is_offset && lhs.get_type().is_ref();
let (fields, struct_type) = self.get_fields(lhs, id)?;

let field = fields.get(access.rhs.as_string()).cloned().ok_or_else(|| {
let location = self.elaborator.interner.expr_location(&id);
Expand All @@ -994,8 +1005,12 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {

// Return a reference to the field so that `&mut foo.bar.baz` can use this reference.
// We set auto_deref to true so that when it is used elsewhere it is dereferenced
// automatically.
Ok(Value::Pointer(field, true, false))
// automatically. In some cases in the frontend the leading `&mut` will cancel out
// with a field access which is expected to only offset into the struct and thus return
// a reference already. In this case we set auto_deref to false because the outer `&mut`
// will also be removed in that case so the pointer should be explicit.
let auto_deref = !is_offset;
Ok(Value::Pointer(field, auto_deref, false))
}

fn evaluate_call(&mut self, call: HirCallExpression, id: ExprId) -> IResult<Value> {
Expand Down
14 changes: 14 additions & 0 deletions compiler/noirc_frontend/src/hir_def/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ pub struct HirPrefixExpression {
/// The trait method id for the operator trait method that corresponds to this operator,
/// if such a trait exists (for example, there's no trait for the dereference operator).
pub trait_method_id: Option<TraitItemId>,

/// If this is true we should skip this operation and directly return `rhs` instead.
/// This is used for compiling `&mut foo.bar.baz` where `foo.bar.baz` already returns
/// a reference and we do not want to create a new reference. Additionally, this node
/// is kept so that `nargo expand` still expands into the full `&mut foo.bar.baz` instead
/// of removing the leading `&mut`.
pub skip: bool,
}

impl HirPrefixExpression {
/// Creates a basic HirPrefixExpression with `trait_method_id = None` and `skip = false`
pub fn new(operator: UnaryOp, rhs: ExprId) -> Self {
Self { operator, rhs, trait_method_id: None, skip: false }
}
}

#[derive(Debug, Clone)]
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 @@ -1299,6 +1299,10 @@ impl Type {
matches!(self.follow_bindings_shallow().as_ref(), Type::Reference(_, true))
}

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

/// 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
5 changes: 5 additions & 0 deletions compiler/noirc_frontend/src/monomorphization/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ pub struct Unary {
pub rhs: Box<Expression>,
pub result_type: Type,
pub location: Location,

/// Carried over from `HirPrefixExpression::skip`. This also flags whether we should directly
/// return `rhs` and skip performing this operation. Compared to replacing this node with rhs
/// directly, keeping this flag keeps `--show-monomorphized` output the same.
pub skip: bool,
}

pub type BinaryOp = BinaryOpKind;
Expand Down
10 changes: 9 additions & 1 deletion compiler/noirc_frontend/src/monomorphization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,13 @@ impl<'interner> Monomorphizer<'interner> {
let operator = prefix.operator;
let rhs = Box::new(rhs);
let result_type = Self::convert_type(&self.interner.id_type(expr), location)?;
ast::Expression::Unary(ast::Unary { operator, rhs, result_type, location })
ast::Expression::Unary(ast::Unary {
operator,
rhs,
result_type,
location,
skip: prefix.skip,
})
}
}

Expand Down Expand Up @@ -2274,6 +2280,7 @@ impl<'interner> Monomorphizer<'interner> {
result_type,
operator: Reference { mutable: *mutable },
location,
skip: false,
})
}
}
Expand Down Expand Up @@ -2363,6 +2370,7 @@ impl<'interner> Monomorphizer<'interner> {
rhs: Box::new(result),
result_type: ast::Type::Bool,
location,
skip: false,
});
}
// All the comparison operators require special handling since their `cmp` method
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "reference_cancelling"
type = "bin"
authors = [""]

[dependencies]
23 changes: 23 additions & 0 deletions test_programs/execution_success/reference_cancelling/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
fn main() {
assert(comptime { store_through_outer_pointer() });
assert(store_through_outer_pointer());

assert(comptime { store_through_middle_pointer() });
assert(store_through_middle_pointer());
}

fn store_through_outer_pointer() -> bool {
let a = &mut ((false,),);
let b = &mut a.0.0;
*a = ((true,),);
println(*b);
*b
}

fn store_through_middle_pointer() -> bool {
let a = &mut ((false,),);
let b = &mut a.0.0;
a.0 = (true,);
println(*b);
*b
}
Loading
Loading