From 2b4249fc62eb06b3a1624be29b5dd16e80211b1e Mon Sep 17 00:00:00 2001 From: Jake Fecher Date: Wed, 9 Oct 2024 17:01:10 +0100 Subject: [PATCH 1/6] Add checked_transmute --- .../src/monomorphization/errors.rs | 5 +++ .../src/monomorphization/mod.rs | 36 ++++++++++++++++--- noir_stdlib/src/mem.nr | 11 ++++++ .../checked_transmute/Nargo.toml | 7 ++++ .../checked_transmute/src/main.nr | 9 +++++ .../checked_transmute/Nargo.toml | 7 ++++ .../checked_transmute/src/main.nr | 15 ++++++++ 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 test_programs/compile_failure/checked_transmute/Nargo.toml create mode 100644 test_programs/compile_failure/checked_transmute/src/main.nr create mode 100644 test_programs/compile_success_empty/checked_transmute/Nargo.toml create mode 100644 test_programs/compile_success_empty/checked_transmute/src/main.nr diff --git a/compiler/noirc_frontend/src/monomorphization/errors.rs b/compiler/noirc_frontend/src/monomorphization/errors.rs index 4b1951a1aac..e2ff94f521b 100644 --- a/compiler/noirc_frontend/src/monomorphization/errors.rs +++ b/compiler/noirc_frontend/src/monomorphization/errors.rs @@ -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 { @@ -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(), } @@ -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(); diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs index 295aa9056b6..3406575b0f5 100644 --- a/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -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() { @@ -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 { + ) -> Result, 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 = @@ -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 { + 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.try_unify(&expected, &mut TypeBindings::new()).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( diff --git a/noir_stdlib/src/mem.nr b/noir_stdlib/src/mem.nr index 88d17e20ee3..c0bfa60e4a1 100644 --- a/noir_stdlib/src/mem.nr +++ b/noir_stdlib/src/mem.nr @@ -4,3 +4,14 @@ #[builtin(zeroed)] pub fn zeroed() -> T {} +/// Transmutes a value of type T to a value of type U. +/// +/// This operation is checked in that in a later stage of compilation +/// both types are asserted to be equal. If not, a compile error is issued. +/// +/// This function is useful for types using arithmetic generics for cases +/// which the compiler otherwise cannot prove as 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(value: T) -> U {} diff --git a/test_programs/compile_failure/checked_transmute/Nargo.toml b/test_programs/compile_failure/checked_transmute/Nargo.toml new file mode 100644 index 00000000000..9d01c873b03 --- /dev/null +++ b/test_programs/compile_failure/checked_transmute/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "checked_transmute" +type = "bin" +authors = [""] +compiler_version = ">=0.35.0" + +[dependencies] \ No newline at end of file diff --git a/test_programs/compile_failure/checked_transmute/src/main.nr b/test_programs/compile_failure/checked_transmute/src/main.nr new file mode 100644 index 00000000000..058fa0ec911 --- /dev/null +++ b/test_programs/compile_failure/checked_transmute/src/main.nr @@ -0,0 +1,9 @@ +use std::mem::checked_transmute; + +fn main() { + let _: [Field; 2] = transmute_fail([1]); +} + +pub fn transmute_fail(x: [Field; N]) -> [Field; N + 1] { + checked_transmute(x) +} diff --git a/test_programs/compile_success_empty/checked_transmute/Nargo.toml b/test_programs/compile_success_empty/checked_transmute/Nargo.toml new file mode 100644 index 00000000000..f3392ec79bb --- /dev/null +++ b/test_programs/compile_success_empty/checked_transmute/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "checked_transmute" +type = "bin" +authors = [""] +compiler_version = ">=0.35.0" + +[dependencies] diff --git a/test_programs/compile_success_empty/checked_transmute/src/main.nr b/test_programs/compile_success_empty/checked_transmute/src/main.nr new file mode 100644 index 00000000000..fa6240fb43a --- /dev/null +++ b/test_programs/compile_success_empty/checked_transmute/src/main.nr @@ -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(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) +} From 28a5fb1aef09b15ef3e6df0742129ddaa7d6a1b0 Mon Sep 17 00:00:00 2001 From: Jake Fecher Date: Wed, 9 Oct 2024 17:09:09 +0100 Subject: [PATCH 2/6] Add checked_transmute to docs --- docs/docs/noir/standard_library/mem.md | 52 +++++++++++++++++++++++ docs/docs/noir/standard_library/zeroed.md | 26 ------------ 2 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 docs/docs/noir/standard_library/mem.md delete mode 100644 docs/docs/noir/standard_library/zeroed.md diff --git a/docs/docs/noir/standard_library/mem.md b/docs/docs/noir/standard_library/mem.md new file mode 100644 index 00000000000..8aac2bb0017 --- /dev/null +++ b/docs/docs/noir/standard_library/mem.md @@ -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 +``` + +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(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. +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 later on. + +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. diff --git a/docs/docs/noir/standard_library/zeroed.md b/docs/docs/noir/standard_library/zeroed.md deleted file mode 100644 index f450fecdd36..00000000000 --- a/docs/docs/noir/standard_library/zeroed.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Zeroed Function -description: - The zeroed function returns a zeroed value of any type. -keywords: - [ - zeroed - ] ---- - -Implements `fn zeroed() -> T` to return 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. - -You can access the function at `std::unsafe::zeroed`. - -This function currently supports the following types: - -- Field -- Bool -- Uint -- Array -- Slice -- String -- Tuple -- Function - -Using it on other types could result in unexpected behavior. From 8bd1609307c6cd3fd7dcf78dbdf96b859e47427c Mon Sep 17 00:00:00 2001 From: jfecher Date: Wed, 9 Oct 2024 17:52:04 +0100 Subject: [PATCH 3/6] Update noir_stdlib/src/mem.nr Co-authored-by: Michael J Klein --- noir_stdlib/src/mem.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir_stdlib/src/mem.nr b/noir_stdlib/src/mem.nr index c0bfa60e4a1..7edd8217d39 100644 --- a/noir_stdlib/src/mem.nr +++ b/noir_stdlib/src/mem.nr @@ -7,7 +7,7 @@ pub fn zeroed() -> T {} /// Transmutes a value of type T to a value of type U. /// /// This operation is checked in that in a later stage of compilation -/// both types are asserted to be equal. If not, a compile error is issued. +/// both types are asserted to be equal. If not, a compilation error is issued. /// /// This function is useful for types using arithmetic generics for cases /// which the compiler otherwise cannot prove as equal during type checking. From 4060c2a6d9dcef86e658695239e7507ea063a678 Mon Sep 17 00:00:00 2001 From: jfecher Date: Wed, 9 Oct 2024 17:52:26 +0100 Subject: [PATCH 4/6] Update noir_stdlib/src/mem.nr Co-authored-by: Michael J Klein --- noir_stdlib/src/mem.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir_stdlib/src/mem.nr b/noir_stdlib/src/mem.nr index 7edd8217d39..6bc392cba23 100644 --- a/noir_stdlib/src/mem.nr +++ b/noir_stdlib/src/mem.nr @@ -10,7 +10,7 @@ pub fn zeroed() -> T {} /// both types are asserted to be equal. If not, a compilation error is issued. /// /// This function is useful for types using arithmetic generics for cases -/// which the compiler otherwise cannot prove as equal during type checking. +/// 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)] From d264b1d149cb863ebd8c7c7e1cafc89818cb75b7 Mon Sep 17 00:00:00 2001 From: jfecher Date: Wed, 9 Oct 2024 17:52:57 +0100 Subject: [PATCH 5/6] Update docs/docs/noir/standard_library/mem.md Co-authored-by: Michael J Klein --- docs/docs/noir/standard_library/mem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/noir/standard_library/mem.md b/docs/docs/noir/standard_library/mem.md index 8aac2bb0017..74ea41ab5ee 100644 --- a/docs/docs/noir/standard_library/mem.md +++ b/docs/docs/noir/standard_library/mem.md @@ -46,7 +46,7 @@ This function is useful for cases where the compiler may fails a type check that 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 later on. +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. From bfed3ea9d3480a478fe65d46bc97a125701af17d Mon Sep 17 00:00:00 2001 From: Jake Fecher Date: Wed, 9 Oct 2024 17:58:20 +0100 Subject: [PATCH 6/6] Docs suggestions --- compiler/noirc_frontend/src/monomorphization/mod.rs | 2 +- docs/docs/noir/standard_library/mem.md | 2 +- noir_stdlib/src/mem.nr | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs index 3406575b0f5..a397763955b 100644 --- a/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -1428,7 +1428,7 @@ impl<'interner> Monomorphizer<'interner> { let actual = self.interner.id_type(arguments[0]).follow_bindings(); let expected = self.interner.id_type(expr_id).follow_bindings(); - if actual.try_unify(&expected, &mut TypeBindings::new()).is_err() { + if actual.unify(&expected).is_err() { Err(MonomorphizationError::CheckedTransmuteFailed { actual, expected, location }) } else { // Evaluate `checked_transmute(arg)` to `{ arg }` diff --git a/docs/docs/noir/standard_library/mem.md b/docs/docs/noir/standard_library/mem.md index 74ea41ab5ee..95d36ac2a72 100644 --- a/docs/docs/noir/standard_library/mem.md +++ b/docs/docs/noir/standard_library/mem.md @@ -41,7 +41,7 @@ fn checked_transmute(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. +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. diff --git a/noir_stdlib/src/mem.nr b/noir_stdlib/src/mem.nr index 6bc392cba23..0d47a21b50d 100644 --- a/noir_stdlib/src/mem.nr +++ b/noir_stdlib/src/mem.nr @@ -6,10 +6,10 @@ pub fn zeroed() -> T {} /// Transmutes a value of type T to a value of type U. /// -/// This operation is checked in that in a later stage of compilation -/// both types are asserted to be equal. If not, a compilation error is issued. +/// 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 for cases +/// 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.