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: Add checked_transmute #6262

Merged
merged 6 commits into from
Oct 9, 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
5 changes: 5 additions & 0 deletions compiler/noirc_frontend/src/monomorphization/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum MonomorphizationError {
InterpreterError(InterpreterError),
ComptimeFnInRuntimeCode { name: String, location: Location },
ComptimeTypeInRuntimeCode { typ: String, location: Location },
CheckedTransmuteFailed { actual: Type, expected: Type, location: Location },
}

impl MonomorphizationError {
Expand All @@ -21,6 +22,7 @@ impl MonomorphizationError {
| MonomorphizationError::InternalError { location, .. }
| MonomorphizationError::ComptimeFnInRuntimeCode { location, .. }
| MonomorphizationError::ComptimeTypeInRuntimeCode { location, .. }
| MonomorphizationError::CheckedTransmuteFailed { location, .. }
| MonomorphizationError::NoDefaultType { location, .. } => *location,
MonomorphizationError::InterpreterError(error) => error.get_location(),
}
Expand All @@ -45,6 +47,9 @@ impl MonomorphizationError {
MonomorphizationError::UnknownConstant { .. } => {
"Could not resolve constant".to_string()
}
MonomorphizationError::CheckedTransmuteFailed { actual, expected, .. } => {
format!("checked_transmute failed: `{actual}` != `{expected}`")
}
MonomorphizationError::NoDefaultType { location } => {
let message = "Type annotation needed".into();
let secondary = "Could not determine type of generic argument".into();
Expand Down
36 changes: 31 additions & 5 deletions compiler/noirc_frontend/src/monomorphization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ impl<'interner> Monomorphizer<'interner> {
};

let call = self
.try_evaluate_call(&func, &id, &return_type)
.try_evaluate_call(&func, &id, &call.arguments, &arguments, &return_type)?
.unwrap_or(ast::Expression::Call(ast::Call { func, arguments, return_type, location }));

if !block_expressions.is_empty() {
Expand Down Expand Up @@ -1371,13 +1371,15 @@ impl<'interner> Monomorphizer<'interner> {
&mut self,
func: &ast::Expression,
expr_id: &node_interner::ExprId,
arguments: &[node_interner::ExprId],
argument_values: &[ast::Expression],
result_type: &ast::Type,
) -> Option<ast::Expression> {
) -> Result<Option<ast::Expression>, MonomorphizationError> {
if let ast::Expression::Ident(ident) = func {
if let Definition::Builtin(opcode) = &ident.definition {
// TODO(#1736): Move this builtin to the SSA pass
let location = self.interner.expr_location(expr_id);
return match opcode.as_str() {
return Ok(match opcode.as_str() {
"modulus_num_bits" => {
let bits = (FieldElement::max_num_bits() as u128).into();
let typ =
Expand Down Expand Up @@ -1406,11 +1408,35 @@ impl<'interner> Monomorphizer<'interner> {
let bytes = FieldElement::modulus().to_bytes_le();
Some(self.modulus_slice_literal(bytes, IntegerBitSize::Eight, location))
}
"checked_transmute" => {
Some(self.checked_transmute(*expr_id, arguments, argument_values)?)
}
_ => None,
};
});
}
}
None
Ok(None)
}

fn checked_transmute(
&mut self,
expr_id: node_interner::ExprId,
arguments: &[node_interner::ExprId],
argument_values: &[ast::Expression],
) -> Result<ast::Expression, MonomorphizationError> {
let location = self.interner.expr_location(&expr_id);
let actual = self.interner.id_type(arguments[0]).follow_bindings();
let expected = self.interner.id_type(expr_id).follow_bindings();

if actual.unify(&expected).is_err() {
Err(MonomorphizationError::CheckedTransmuteFailed { actual, expected, location })
} else {
// Evaluate `checked_transmute(arg)` to `{ arg }`
// in case the user did `&mut checked_transmute(arg)`. Wrapping the
// arg in a block prevents mutating the original argument.
let argument = argument_values[0].clone();
Ok(ast::Expression::Block(vec![argument]))
}
}

fn modulus_slice_literal(
Expand Down
52 changes: 52 additions & 0 deletions docs/docs/noir/standard_library/mem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Memory Module
description:
This module contains functions which manipulate memory in a low-level way
keywords:
[
mem, memory, zeroed, transmute, checked_transmute
]
---

# `std::mem::zeroed`

```rust
fn zeroed<T>() -> T
```

Returns a zeroed value of any type.
This function is generally unsafe to use as the zeroed bit pattern is not guaranteed to be valid for all types.
It can however, be useful in cases when the value is guaranteed not to be used such as in a BoundedVec library implementing a growable vector, up to a certain length, backed by an array.
The array can be initialized with zeroed values which are guaranteed to be inaccessible until the vector is pushed to.
Similarly, enumerations in noir can be implemented using this method by providing zeroed values for the unused variants.

This function currently supports the following types:

- Field
- Bool
- Uint
- Array
- Slice
- String
- Tuple
- Functions

Using it on other types could result in unexpected behavior.

# `std::mem::checked_transmute`

```rust
fn checked_transmute<T, U>(value: T) -> U
```

Transmutes a value of one type into the same value but with a new type `U`.

This function is safe to use since both types are asserted to be equal later during compilation after the concrete values for generic types become known.
This function is useful for cases where the compiler may fails a type check that is expected to pass where
a user knows the two types to be equal. For example, when using arithmetic generics there are cases the compiler
does not see as equal, such as `[Field; N*(A + B)]` and `[Field; N*A + N*B]`, which users may know to be equal.
In these cases, `checked_transmute` can be used to cast the value to the desired type while also preserving safety
by checking this equality once `N`, `A`, `B` are fully resolved.

Note that since this safety check is performed after type checking rather than during, no error is issued if the function
containing `checked_transmute` is never called.
26 changes: 0 additions & 26 deletions docs/docs/noir/standard_library/zeroed.md

This file was deleted.

11 changes: 11 additions & 0 deletions noir_stdlib/src/mem.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@
#[builtin(zeroed)]
pub fn zeroed<T>() -> T {}

/// Transmutes a value of type T to a value of type U.
///
/// Both types are asserted to be equal during compilation but after type checking.
/// If not, a compilation error is issued.
///
/// This function is useful for types using arithmetic generics in cases
/// which the compiler otherwise cannot prove equal during type checking.
/// You can use this to obtain a value of the correct type while still asserting
/// that it is equal to the previous.
#[builtin(checked_transmute)]
pub fn checked_transmute<T, U>(value: T) -> U {}
7 changes: 7 additions & 0 deletions test_programs/compile_failure/checked_transmute/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "checked_transmute"
type = "bin"
authors = [""]
compiler_version = ">=0.35.0"

[dependencies]
9 changes: 9 additions & 0 deletions test_programs/compile_failure/checked_transmute/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use std::mem::checked_transmute;

fn main() {
let _: [Field; 2] = transmute_fail([1]);
}

pub fn transmute_fail<let N: u32>(x: [Field; N]) -> [Field; N + 1] {
checked_transmute(x)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "checked_transmute"
type = "bin"
authors = [""]
compiler_version = ">=0.35.0"

[dependencies]
15 changes: 15 additions & 0 deletions test_programs/compile_success_empty/checked_transmute/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::mem::checked_transmute;

fn main() {
// 1*(2 + 3) = 1*2 + 1*3 = 5
let _: [Field; 5] = distribute::<1, 2, 3>([1, 2, 3, 4, 5]);
}

pub fn distribute<let N: u32, let A: u32, let B: u32>(x: [Field; N * (A + B)]) -> [Field; N * A + N * B] {
// asserts: [Field; N * (A + B)] = [Field; N * A + N * B]
// -> N * A + B = N * A + N * B
//
// This assert occurs during monomorphization when the actual values for N, A, and B
// become known. This also means if this function is not called, the assert will not trigger.
checked_transmute(x)
}
Loading