diff --git a/compiler/noirc_evaluator/src/acir/generated_acir.rs b/compiler/noirc_evaluator/src/acir/generated_acir.rs index 14ceac62461..21c8bfe7d5c 100644 --- a/compiler/noirc_evaluator/src/acir/generated_acir.rs +++ b/compiler/noirc_evaluator/src/acir/generated_acir.rs @@ -367,7 +367,6 @@ impl GeneratedAcir { radix_big, "ICE: Radix must be a power of 2" ); - let limb_witnesses = self.brillig_to_radix(input_expr, radix, limb_count); let mut composed_limbs = Expression::default(); diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs index 3506b63919c..85e16440921 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs @@ -66,7 +66,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "array_as_str_unchecked" => array_as_str_unchecked(interner, arguments, location), "array_len" => array_len(interner, arguments, location), "array_refcount" => Ok(Value::U32(0)), - "assert_constant" => Ok(Value::Bool(true)), + "assert_constant" => Ok(Value::Unit), "as_slice" => as_slice(interner, arguments, location), "ctstring_eq" => ctstring_eq(arguments, location), "ctstring_hash" => ctstring_hash(arguments, location), @@ -175,6 +175,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "slice_push_front" => slice_push_front(interner, arguments, location), "slice_refcount" => Ok(Value::U32(0)), "slice_remove" => slice_remove(interner, arguments, location, call_stack), + "static_assert" => static_assert(interner, arguments, location, call_stack), "str_as_bytes" => str_as_bytes(interner, arguments, location), "str_as_ctstring" => str_as_ctstring(interner, arguments, location), "struct_def_add_attribute" => struct_def_add_attribute(interner, arguments, location), @@ -327,6 +328,28 @@ fn slice_push_back( Ok(Value::Slice(values, typ)) } +// static_assert(predicate: bool, message: str) +fn static_assert( + interner: &NodeInterner, + arguments: Vec<(Value, Location)>, + location: Location, + call_stack: &im::Vector, +) -> IResult { + let (predicate, message) = check_two_arguments(arguments, location)?; + let predicate = get_bool(predicate)?; + let message = get_str(interner, message)?; + + if predicate { + Ok(Value::Unit) + } else { + failing_constraint( + format!("static_assert failed: {}", message).clone(), + location, + call_stack, + ) + } +} + fn str_as_bytes( interner: &NodeInterner, arguments: Vec<(Value, Location)>, diff --git a/compiler/noirc_frontend/src/tests/metaprogramming.rs b/compiler/noirc_frontend/src/tests/metaprogramming.rs index 8256744e18f..93426c1348c 100644 --- a/compiler/noirc_frontend/src/tests/metaprogramming.rs +++ b/compiler/noirc_frontend/src/tests/metaprogramming.rs @@ -3,6 +3,7 @@ use noirc_errors::Spanned; use crate::{ ast::Ident, hir::{ + comptime::InterpreterError, def_collector::{ dc_crate::CompilationError, errors::{DefCollectorErrorKind, DuplicateType}, @@ -26,6 +27,27 @@ fn comptime_let() { assert_eq!(errors.len(), 0); } +#[test] +fn comptime_code_rejects_dynamic_variable() { + let src = r#"fn main(x: Field) { + comptime let my_var = (x - x) + 2; + assert_eq(my_var, 2); + }"#; + let errors = get_program_errors(src); + + + assert_eq!(errors.len(), 1); + match &errors[0].0 { + CompilationError::InterpreterError(InterpreterError::NonComptimeVarReferenced { + name, + .. + }) => { + assert_eq!(name, "x"); + } + _ => panic!("expected an InterpreterError"), + } +} + #[test] fn comptime_type_in_runtime_code() { let source = "pub fn foo(_f: FunctionDefinition) {}"; diff --git a/noir_stdlib/src/collections/bounded_vec.nr b/noir_stdlib/src/collections/bounded_vec.nr index 7aed5e6a0e4..8624233d398 100644 --- a/noir_stdlib/src/collections/bounded_vec.nr +++ b/noir_stdlib/src/collections/bounded_vec.nr @@ -1,4 +1,4 @@ -use crate::{cmp::Eq, convert::From}; +use crate::{cmp::Eq, convert::From, static_assert}; /// A `BoundedVec` is a growable storage similar to a `Vec` except that it /// is bounded with a maximum possible length. Unlike `Vec`, `BoundedVec` is not implemented @@ -339,7 +339,7 @@ impl BoundedVec { /// let bounded_vec: BoundedVec = BoundedVec::from_array([1, 2, 3]) /// ``` pub fn from_array(array: [T; Len]) -> Self { - assert(Len <= MaxLen, "from array out of bounds"); + static_assert(Len <= MaxLen, "from array out of bounds"); let mut vec: BoundedVec = BoundedVec::new(); vec.extend_from_array(array); vec diff --git a/noir_stdlib/src/field/mod.nr b/noir_stdlib/src/field/mod.nr index d0760447ff1..957c90ace25 100644 --- a/noir_stdlib/src/field/mod.nr +++ b/noir_stdlib/src/field/mod.nr @@ -1,5 +1,5 @@ pub mod bn254; -use crate::runtime::is_unconstrained; +use crate::{runtime::is_unconstrained, static_assert}; use bn254::lt as bn254_lt; impl Field { @@ -10,7 +10,10 @@ impl Field { // docs:start:assert_max_bit_size pub fn assert_max_bit_size(self) { // docs:end:assert_max_bit_size - assert(BIT_SIZE < modulus_num_bits() as u32); + static_assert( + BIT_SIZE < modulus_num_bits() as u32, + "BIT_SIZE must be less than modulus_num_bits", + ); self.__assert_max_bit_size(BIT_SIZE); } @@ -61,6 +64,10 @@ impl Field { // docs:start:to_le_bytes pub fn to_le_bytes(self: Self) -> [u8; N] { // docs:end:to_le_bytes + static_assert( + N <= modulus_le_bytes().len(), + "N must be less than or equal to modulus_le_bytes().len()", + ); // Compute the byte decomposition let bytes = self.to_le_radix(256); @@ -94,6 +101,10 @@ impl Field { // docs:start:to_be_bytes pub fn to_be_bytes(self: Self) -> [u8; N] { // docs:end:to_be_bytes + static_assert( + N <= modulus_le_bytes().len(), + "N must be less than or equal to modulus_le_bytes().len()", + ); // Compute the byte decomposition let bytes = self.to_be_radix(256); @@ -139,6 +150,7 @@ impl Field { #[builtin(to_le_radix)] fn __to_le_radix(self, radix: u32) -> [u8; N] {} + // `_radix` must be less than 256 #[builtin(to_be_radix)] fn __to_be_radix(self, radix: u32) -> [u8; N] {} @@ -172,6 +184,10 @@ impl Field { /// Convert a little endian byte array to a field element. /// If the provided byte array overflows the field modulus then the Field will silently wrap around. pub fn from_le_bytes(bytes: [u8; N]) -> Field { + static_assert( + N <= modulus_le_bytes().len(), + "N must be less than or equal to modulus_le_bytes().len()", + ); let mut v = 1; let mut result = 0; @@ -185,6 +201,10 @@ impl Field { /// Convert a big endian byte array to a field element. /// If the provided byte array overflows the field modulus then the Field will silently wrap around. pub fn from_be_bytes(bytes: [u8; N]) -> Field { + static_assert( + N <= modulus_le_bytes().len(), + "N must be less than or equal to modulus_le_bytes().len()", + ); let mut v = 1; let mut result = 0; diff --git a/noir_stdlib/src/meta/ctstring.nr b/noir_stdlib/src/meta/ctstring.nr index e23567ece7d..00b4f1fdb6f 100644 --- a/noir_stdlib/src/meta/ctstring.nr +++ b/noir_stdlib/src/meta/ctstring.nr @@ -7,7 +7,8 @@ impl CtString { "".as_ctstring() } - // Bug: using &mut self as the object results in this method not being found + // TODO(https://github.com/noir-lang/noir/issues/6980): Bug: using &mut self + // as the object results in this method not being found // docs:start:append_str pub comptime fn append_str(self, s: str) -> Self { // docs:end:append_str diff --git a/noir_stdlib/src/uint128.nr b/noir_stdlib/src/uint128.nr index 9aa01e1ea52..446ed4fb092 100644 --- a/noir_stdlib/src/uint128.nr +++ b/noir_stdlib/src/uint128.nr @@ -1,5 +1,6 @@ use crate::cmp::{Eq, Ord, Ordering}; use crate::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Not, Rem, Shl, Shr, Sub}; +use crate::static_assert; use super::{convert::AsPrimitive, default::Default}; global pow64: Field = 18446744073709551616; //2^64; @@ -67,11 +68,10 @@ impl U128 { } pub fn from_hex(hex: str) -> U128 { - let N = N as u32; let bytes = hex.as_bytes(); // string must starts with "0x" assert((bytes[0] == 48) & (bytes[1] == 120), "Invalid hexadecimal string"); - assert(N < 35, "Input does not fit into a U128"); + static_assert(N < 35, "Input does not fit into a U128"); let mut lo = 0; let mut hi = 0; diff --git a/test_programs/compile_failure/comptime_static_assert_failure/Nargo.toml b/test_programs/compile_failure/comptime_static_assert_failure/Nargo.toml new file mode 100644 index 00000000000..006fd9f7ffe --- /dev/null +++ b/test_programs/compile_failure/comptime_static_assert_failure/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "comptime_static_assert_failure" +type = "bin" +authors = [""] + +[dependencies] \ No newline at end of file diff --git a/test_programs/compile_failure/comptime_static_assert_failure/src/main.nr b/test_programs/compile_failure/comptime_static_assert_failure/src/main.nr new file mode 100644 index 00000000000..fcd757f4c94 --- /dev/null +++ b/test_programs/compile_failure/comptime_static_assert_failure/src/main.nr @@ -0,0 +1,13 @@ +use std::static_assert; + +comptime fn foo(x: Field) -> bool { + static_assert(x == 4, "x != 4"); + x == 4 +} + +fn main() { + comptime { + static_assert(foo(3), "expected message"); + } +} + diff --git a/test_programs/compile_success_empty/comptime_static_assert/Nargo.toml b/test_programs/compile_success_empty/comptime_static_assert/Nargo.toml new file mode 100644 index 00000000000..4c969fe7a79 --- /dev/null +++ b/test_programs/compile_success_empty/comptime_static_assert/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "comptime_static_assert" +type = "bin" +authors = [""] + +[dependencies] \ No newline at end of file diff --git a/test_programs/compile_success_empty/comptime_static_assert/src/main.nr b/test_programs/compile_success_empty/comptime_static_assert/src/main.nr new file mode 100644 index 00000000000..2ddbba7b0de --- /dev/null +++ b/test_programs/compile_success_empty/comptime_static_assert/src/main.nr @@ -0,0 +1,19 @@ +use std::static_assert; + +comptime fn foo(x: Field) -> bool { + static_assert(x == 4, "x != 4"); + x == 4 +} + +global C: bool = { + let out = foo(2 + 2); + static_assert(out, "foo did not pass in C"); + out +}; + +fn main() { + comptime { + static_assert(foo(4), "foo did not pass in main"); + static_assert(C, "C did not pass") + } +} diff --git a/tooling/nargo_cli/src/cli/compile_cmd.rs b/tooling/nargo_cli/src/cli/compile_cmd.rs index 0af05703c9a..3c55d8c2164 100644 --- a/tooling/nargo_cli/src/cli/compile_cmd.rs +++ b/tooling/nargo_cli/src/cli/compile_cmd.rs @@ -325,6 +325,7 @@ pub(crate) fn get_target_width( #[cfg(test)] mod tests { use std::{ + fmt::Write, path::{Path, PathBuf}, str::FromStr, }; @@ -335,6 +336,7 @@ mod tests { use noirc_driver::{CompileOptions, CrateName}; use crate::cli::compile_cmd::{get_target_width, parse_workspace, read_workspace}; + use crate::cli::test_cmd::formatters::diagnostic_to_string; /// Try to find the directory that Cargo sets when it is running; /// otherwise fallback to assuming the CWD is the root of the repository @@ -414,7 +416,18 @@ mod tests { &CompileOptions::default(), None, ) - .expect("failed to compile"); + .unwrap_or_else(|err| { + let error_string: String = + err.iter().fold(String::new(), |mut output, diagnostic| { + let _ = write!( + output, + "{}\n---\n", + diagnostic_to_string(diagnostic, &file_manager) + ); + output + }); + panic!("Failed to compile:\n\n{}", error_string) + }); let width = get_target_width(package.expression_width, None); diff --git a/tooling/nargo_cli/src/cli/test_cmd.rs b/tooling/nargo_cli/src/cli/test_cmd.rs index 9bf3ae9fedf..4ba734386fe 100644 --- a/tooling/nargo_cli/src/cli/test_cmd.rs +++ b/tooling/nargo_cli/src/cli/test_cmd.rs @@ -26,7 +26,7 @@ use crate::{cli::check_cmd::check_crate_and_report_errors, errors::CliError}; use super::{NargoConfig, PackageOptions}; -mod formatters; +pub(crate) mod formatters; /// Run the tests for this program #[derive(Debug, Clone, Args)] diff --git a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs index 75cf14ba120..bc4621c92ea 100644 --- a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs +++ b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs @@ -514,7 +514,10 @@ fn package_start(package_name: &str, test_count: usize) -> std::io::Result<()> { Ok(()) } -fn diagnostic_to_string(file_diagnostic: &FileDiagnostic, file_manager: &FileManager) -> String { +pub(crate) fn diagnostic_to_string( + file_diagnostic: &FileDiagnostic, + file_manager: &FileManager, +) -> String { let file_map = file_manager.as_file_map(); let custom_diagnostic = &file_diagnostic.diagnostic;