diff --git a/Cargo.lock b/Cargo.lock index 4e1510c8df9..1b7a70b2063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1982,6 +1982,7 @@ dependencies = [ "noirc_abi", "noirc_driver", "noirc_errors", + "regex", "rustc_version", "serde", "serde_json", @@ -2128,6 +2129,7 @@ dependencies = [ "iter-extended", "noirc_abi", "noirc_errors", + "regex", "rustc-hash", "serde", "serde_json", diff --git a/crates/nargo/Cargo.toml b/crates/nargo/Cargo.toml index 6c053cba931..afbafdff931 100644 --- a/crates/nargo/Cargo.toml +++ b/crates/nargo/Cargo.toml @@ -20,4 +20,5 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true noirc_errors.workspace = true -base64.workspace = true \ No newline at end of file +base64.workspace = true +regex = "1.9.1" diff --git a/crates/nargo/src/ops/foreign_calls.rs b/crates/nargo/src/ops/foreign_calls.rs index 4bbd4eb58bc..2abc62b1032 100644 --- a/crates/nargo/src/ops/foreign_calls.rs +++ b/crates/nargo/src/ops/foreign_calls.rs @@ -4,6 +4,7 @@ use acvm::{ }; use iter_extended::vecmap; use noirc_abi::{decode_string_value, input_parser::InputValueDisplay, AbiType}; +use regex::{Captures, Regex}; use crate::errors::ForeignCallError; @@ -63,31 +64,89 @@ impl ForeignCall { } fn execute_println(foreign_call_inputs: &[Vec]) -> Result<(), ForeignCallError> { - let (abi_type, input_values) = fetch_abi_type(foreign_call_inputs)?; + let (is_fmt_str, foreign_call_inputs) = + foreign_call_inputs.split_last().ok_or(ForeignCallError::MissingForeignCallInputs)?; - // We must use a flat map here as each value in a struct will be in a separate input value - let mut input_values_as_fields = - input_values.iter().flat_map(|values| values.iter().map(|value| value.to_field())); - - let input_value_display = - InputValueDisplay::try_from_fields(&mut input_values_as_fields, abi_type)?; - - println!("{input_value_display}"); + let output_string = if is_fmt_str[0].to_field().is_one() { + convert_fmt_string_inputs(foreign_call_inputs)? + } else { + convert_string_inputs(foreign_call_inputs)? + }; + println!("{output_string}"); Ok(()) } } -/// Fetch the abi type from the foreign call input -/// The remaining input values should hold the values to be printed -fn fetch_abi_type( - foreign_call_inputs: &[Vec], -) -> Result<(AbiType, &[Vec]), ForeignCallError> { +fn convert_string_inputs(foreign_call_inputs: &[Vec]) -> Result { + // Fetch the abi type from the foreign call input + // The remaining input values should hold what is to be printed let (abi_type_as_values, input_values) = foreign_call_inputs.split_last().ok_or(ForeignCallError::MissingForeignCallInputs)?; + let abi_type = fetch_abi_type(abi_type_as_values)?; + + // We must use a flat map here as each value in a struct will be in a separate input value + let mut input_values_as_fields = + input_values.iter().flat_map(|values| vecmap(values, |value| value.to_field())); + + let input_value_display = + InputValueDisplay::try_from_fields(&mut input_values_as_fields, abi_type)?; + + Ok(input_value_display.to_string()) +} + +fn convert_fmt_string_inputs( + foreign_call_inputs: &[Vec], +) -> Result { + let (message_as_values, input_and_abi_values) = + foreign_call_inputs.split_first().ok_or(ForeignCallError::MissingForeignCallInputs)?; + + let message_as_fields = vecmap(message_as_values, |value| value.to_field()); + let message_as_string = decode_string_value(&message_as_fields); + + let (num_values, input_and_abi_values) = + input_and_abi_values.split_first().ok_or(ForeignCallError::MissingForeignCallInputs)?; + + let mut output_strings = Vec::new(); + let num_values = num_values[0].to_field().to_u128() as usize; + + let mut abi_types = Vec::new(); + for abi_values in input_and_abi_values.iter().skip(input_and_abi_values.len() - num_values) { + let abi_type = fetch_abi_type(abi_values)?; + abi_types.push(abi_type); + } + + for i in 0..num_values { + let abi_type = &abi_types[i]; + let type_size = abi_type.field_count() as usize; + + let mut input_values_as_fields = input_and_abi_values[i..(i + type_size)] + .iter() + .flat_map(|values| vecmap(values, |value| value.to_field())); + + let input_value_display = + InputValueDisplay::try_from_fields(&mut input_values_as_fields, abi_type.clone())?; + + output_strings.push(input_value_display.to_string()); + } + + let mut output_strings_iter = output_strings.into_iter(); + let re = Regex::new(r"\{([a-zA-Z0-9_]+)\}") + .expect("ICE: an invalid regex pattern was used for checking format strings"); + + let formatted_str = re.replace_all(&message_as_string, |_: &Captures| { + output_strings_iter + .next() + .expect("ICE: there are more regex matches than fields supplied to the format string") + }); + + Ok(formatted_str.into_owned()) +} + +fn fetch_abi_type(abi_type_as_values: &[Value]) -> Result { let abi_type_as_fields = vecmap(abi_type_as_values, |value| value.to_field()); let abi_type_as_string = decode_string_value(&abi_type_as_fields); let abi_type: AbiType = serde_json::from_str(&abi_type_as_string) .map_err(|err| ForeignCallError::InputParserError(err.into()))?; - Ok((abi_type, input_values)) + Ok(abi_type) } diff --git a/crates/nargo_cli/tests/test_data/debug_logs/src/main.nr b/crates/nargo_cli/tests/test_data/debug_logs/src/main.nr index 29386feb98c..c8d37a938c7 100644 --- a/crates/nargo_cli/tests/test_data/debug_logs/src/main.nr +++ b/crates/nargo_cli/tests/test_data/debug_logs/src/main.nr @@ -1,14 +1,56 @@ use dep::std; fn main(x : Field, y : pub Field) { + let string = "i: {i}, j: {j}"; + std::println(string); + + // A `fmtstr` lets you easily perform string interpolation. + let fmt_str: fmtstr<14, (Field, Field)> = f"i: {x}, j: {y}"; + let fmt_str = string_identity(fmt_str); + std::println(fmt_str); + + let fmt_str_no_type = f"i: {x}, j: {y}"; + std::println(fmt_str_no_type); + + let fmt_str_generic = string_with_generics(fmt_str_no_type); + std::println(fmt_str_generic); + + let s = myStruct { y: x, x: y }; + std::println(s); + + std::println(f"randomstring{x}{x}"); + + let fmt_str = string_with_partial_generics(f"i: {x}, s: {s}"); + std::println(fmt_str); - std::println("*** println ***"); std::println(x); std::println([x, y]); - let s = myStruct { y: x, x: y }; let foo = fooStruct { my_struct: s, foo: 15 }; - std::println(foo); + std::println(f"s: {s}, foo: {foo}"); + + std::println(f"x: 0, y: 1"); + + let s_2 = myStruct { x: 20, y: 30 }; + std::println(f"s1: {s}, s2: {s_2}"); + + let bar = fooStruct { my_struct: s_2, foo: 20 }; + std::println(f"foo1: {foo}, foo2: {bar}"); + + let struct_string = if x != 5 { f"{foo}" } else { f"{bar}" }; + std::println(struct_string); +} + +fn string_identity(string: fmtstr<14, (Field, Field)>) -> fmtstr<14, (Field, Field)> { + string +} + +fn string_with_generics(string: fmtstr) -> fmtstr { + string +} + +fn string_with_partial_generics(string: fmtstr) -> fmtstr { + string } struct myStruct { diff --git a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/context.rs b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/context.rs index 769ee6aa09f..c485200a53e 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/context.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/context.rs @@ -177,6 +177,15 @@ impl<'a> FunctionContext<'a> { ast::Type::MutableReference(element) => { Self::map_type_helper(element, &mut |_| f(Type::Reference)) } + ast::Type::FmtString(len, fields) => { + // A format string is represented by multiple values + // The message string, the number of fields to be formatted, and + // then the encapsulated fields themselves + let final_fmt_str_fields = + vec![ast::Type::String(*len), ast::Type::Field, *fields.clone()]; + let fmt_str_tuple = ast::Type::Tuple(final_fmt_str_fields); + Self::map_type_helper(&fmt_str_tuple, f) + } other => Tree::Leaf(f(Self::convert_non_tuple_type(other))), } } @@ -204,6 +213,9 @@ impl<'a> FunctionContext<'a> { ast::Type::Integer(Signedness::Unsigned, bits) => Type::unsigned(*bits), ast::Type::Bool => Type::unsigned(1), ast::Type::String(len) => Type::Array(Rc::new(vec![Type::char()]), *len as usize), + ast::Type::FmtString(_, _) => { + panic!("convert_non_tuple_type called on a fmt string: {typ}") + } ast::Type::Unit => panic!("convert_non_tuple_type called on a unit type"), ast::Type::Tuple(_) => panic!("convert_non_tuple_type called on a tuple: {typ}"), ast::Type::Function(_, _) => Type::Function, diff --git a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs index d6169dfd218..0c0dd35211b 100644 --- a/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs +++ b/crates/noirc_evaluator/src/ssa_refactor/ssa_gen/mod.rs @@ -135,6 +135,18 @@ impl<'a> FunctionContext<'a> { let typ = Self::convert_non_tuple_type(&ast::Type::String(elements.len() as u64)); self.codegen_array(elements, typ) } + ast::Literal::FmtStr(string, number_of_fields, fields) => { + // A caller needs multiple pieces of information to make use of a format string + // The message string, the number of fields to be formatted, and the fields themselves + let string = Expression::Literal(ast::Literal::Str(string.clone())); + let number_of_fields = Expression::Literal(ast::Literal::Integer( + (*number_of_fields as u128).into(), + ast::Type::Field, + )); + let fields = *fields.clone(); + let fmt_str_tuple = &[string, number_of_fields, fields]; + self.codegen_tuple(fmt_str_tuple) + } } } diff --git a/crates/noirc_frontend/Cargo.toml b/crates/noirc_frontend/Cargo.toml index a9a62673af6..1f902d2d399 100644 --- a/crates/noirc_frontend/Cargo.toml +++ b/crates/noirc_frontend/Cargo.toml @@ -20,6 +20,7 @@ serde.workspace = true serde_json.workspace = true rustc-hash = "1.1.0" small-ord-set = "0.1.3" +regex = "1.9.1" [dev-dependencies] strum = "0.24" diff --git a/crates/noirc_frontend/src/ast/expression.rs b/crates/noirc_frontend/src/ast/expression.rs index 1f1d226310f..b1829e8c1ee 100644 --- a/crates/noirc_frontend/src/ast/expression.rs +++ b/crates/noirc_frontend/src/ast/expression.rs @@ -72,6 +72,10 @@ impl ExpressionKind { ExpressionKind::Literal(Literal::Str(contents)) } + pub fn format_string(contents: String) -> ExpressionKind { + ExpressionKind::Literal(Literal::FmtStr(contents)) + } + pub fn constructor((type_name, fields): (Path, Vec<(Ident, Expression)>)) -> ExpressionKind { ExpressionKind::Constructor(Box::new(ConstructorExpression { type_name, fields })) } @@ -298,6 +302,7 @@ pub enum Literal { Bool(bool), Integer(FieldElement), Str(String), + FmtStr(String), Unit, } @@ -473,6 +478,7 @@ impl Display for Literal { Literal::Bool(boolean) => write!(f, "{}", if *boolean { "true" } else { "false" }), Literal::Integer(integer) => write!(f, "{}", integer.to_u128()), Literal::Str(string) => write!(f, "\"{string}\""), + Literal::FmtStr(string) => write!(f, "f\"{string}\""), Literal::Unit => write!(f, "()"), } } diff --git a/crates/noirc_frontend/src/ast/mod.rs b/crates/noirc_frontend/src/ast/mod.rs index ed73cce486a..b52c3e685d3 100644 --- a/crates/noirc_frontend/src/ast/mod.rs +++ b/crates/noirc_frontend/src/ast/mod.rs @@ -36,6 +36,7 @@ pub enum UnresolvedType { Bool(CompTime), Expression(UnresolvedTypeExpression), String(Option), + FormatString(UnresolvedTypeExpression, Box), Unit, /// A Named UnresolvedType can be a struct type or a type variable @@ -102,9 +103,10 @@ impl std::fmt::Display for UnresolvedType { Expression(expression) => expression.fmt(f), Bool(is_const) => write!(f, "{is_const}bool"), String(len) => match len { - None => write!(f, "str[]"), - Some(len) => write!(f, "str[{len}]"), + None => write!(f, "str<_>"), + Some(len) => write!(f, "str<{len}>"), }, + FormatString(len, elements) => write!(f, "fmt<{len}, {elements}"), Function(args, ret) => { let args = vecmap(args, ToString::to_string); write!(f, "fn({}) -> {ret}", args.join(", ")) diff --git a/crates/noirc_frontend/src/hir/resolution/errors.rs b/crates/noirc_frontend/src/hir/resolution/errors.rs index 82688928575..e9cf8f31393 100644 --- a/crates/noirc_frontend/src/hir/resolution/errors.rs +++ b/crates/noirc_frontend/src/hir/resolution/errors.rs @@ -74,6 +74,8 @@ pub enum ResolverError { MutableReferenceToArrayElement { span: Span }, #[error("Function is not defined in a contract yet sets is_internal")] ContractFunctionInternalInNormalFunction { span: Span }, + #[error("Numeric constants should be printed without formatting braces")] + NumericConstantInFormatString { name: String, span: Span }, } impl ResolverError { @@ -283,6 +285,11 @@ impl From for Diagnostic { "Non-contract functions cannot be 'internal'".into(), span, ), + ResolverError::NumericConstantInFormatString { name, span } => Diagnostic::simple_error( + format!("cannot find `{name}` in this scope "), + "Numeric constants should be printed without formatting braces".to_string(), + span, + ), } } } diff --git a/crates/noirc_frontend/src/hir/resolution/resolver.rs b/crates/noirc_frontend/src/hir/resolution/resolver.rs index 29b3cc485d5..fe19cb633e4 100644 --- a/crates/noirc_frontend/src/hir/resolution/resolver.rs +++ b/crates/noirc_frontend/src/hir/resolution/resolver.rs @@ -18,6 +18,7 @@ use crate::hir_def::expr::{ HirMethodCallExpression, HirPrefixExpression, }; use crate::token::Attribute; +use regex::Regex; use std::collections::{HashMap, HashSet}; use std::rc::Rc; @@ -347,6 +348,11 @@ impl<'a> Resolver<'a> { let resolved_size = self.resolve_array_size(size, new_variables); Type::String(Box::new(resolved_size)) } + UnresolvedType::FormatString(size, fields) => { + let resolved_size = self.convert_expression_type(size); + let fields = self.resolve_type_inner(*fields, new_variables); + Type::FmtString(Box::new(resolved_size), Box::new(fields)) + } UnresolvedType::Unit => Type::Unit, UnresolvedType::Unspecified => Type::Error, UnresolvedType::Error => Type::Error, @@ -775,7 +781,6 @@ impl<'a> Resolver<'a> { Type::FieldElement(_) | Type::Integer(_, _, _) | Type::Bool(_) - | Type::String(_) | Type::Unit | Type::Error | Type::TypeVariable(_, _) @@ -784,10 +789,11 @@ impl<'a> Resolver<'a> { | Type::NotConstant | Type::Forall(_, _) => (), - Type::Array(length, _) => { + Type::Array(length, element_type) => { if let Type::NamedGeneric(type_variable, name) = length.as_ref() { found.insert(name.to_string(), type_variable.clone()); } + Self::find_numeric_generics_in_type(element_type, found); } Type::Tuple(fields) => { @@ -813,6 +819,17 @@ impl<'a> Resolver<'a> { } } Type::MutableReference(element) => Self::find_numeric_generics_in_type(element, found), + Type::String(length) => { + if let Type::NamedGeneric(type_variable, name) = length.as_ref() { + found.insert(name.to_string(), type_variable.clone()); + } + } + Type::FmtString(length, fields) => { + if let Type::NamedGeneric(type_variable, name) = length.as_ref() { + found.insert(name.to_string(), type_variable.clone()); + } + Self::find_numeric_generics_in_type(fields, found); + } } } @@ -904,6 +921,7 @@ impl<'a> Resolver<'a> { } Literal::Integer(integer) => HirLiteral::Integer(integer), Literal::Str(str) => HirLiteral::Str(str), + Literal::FmtStr(str) => self.resolve_fmt_str_literal(str, expr.span), Literal::Unit => HirLiteral::Unit, }), ExpressionKind::Variable(path) => { @@ -939,6 +957,7 @@ impl<'a> Resolver<'a> { ExpressionKind::Call(call_expr) => { // Get the span and name of path for error reporting let func = self.resolve_expression(*call_expr.func); + let arguments = vecmap(call_expr.arguments, |arg| self.resolve_expression(arg)); let location = Location::new(expr.span, self.file); HirExpression::Call(HirCallExpression { func, arguments, location }) @@ -1288,6 +1307,36 @@ impl<'a> Resolver<'a> { let module_id = self.path_resolver.module_id(); module_id.module(self.def_maps).is_contract } + + fn resolve_fmt_str_literal(&mut self, str: String, call_expr_span: Span) -> HirLiteral { + let re = Regex::new(r"\{([a-zA-Z0-9_]+)\}") + .expect("ICE: an invalid regex pattern was used for checking format strings"); + let mut fmt_str_idents = Vec::new(); + for field in re.find_iter(&str) { + let matched_str = field.as_str(); + let ident_name = &matched_str[1..(matched_str.len() - 1)]; + + let scope_tree = self.scopes.current_scope_tree(); + let variable = scope_tree.find(ident_name); + if let Some((old_value, _)) = variable { + old_value.num_times_used += 1; + let expr_id = self.interner.push_expr(HirExpression::Ident(old_value.ident)); + self.interner.push_expr_location(expr_id, call_expr_span, self.file); + fmt_str_idents.push(expr_id); + } else if ident_name.parse::().is_ok() { + self.errors.push(ResolverError::NumericConstantInFormatString { + name: ident_name.to_owned(), + span: call_expr_span, + }); + } else { + self.errors.push(ResolverError::VariableNotDeclared { + name: ident_name.to_owned(), + span: call_expr_span, + }); + } + } + HirLiteral::FmtStr(str, fmt_str_idents) + } } /// Gives an error if a user tries to create a mutable reference @@ -1572,6 +1621,39 @@ mod test { assert!(errors.is_empty()); } + #[test] + fn resolve_fmt_strings() { + let src = r#" + fn main() { + let string = f"this is i: {i}"; + println(string); + + println(f"I want to print {0}"); + + let new_val = 10; + println(f"randomstring{new_val}{new_val}"); + } + fn println(x : T) -> T { + x + } + "#; + + let errors = resolve_src_code(src, vec!["main", "println"]); + assert!(errors.len() == 2, "Expected 2 errors, got: {:?}", errors); + + for err in errors { + match &err { + ResolverError::VariableNotDeclared { name, .. } => { + assert_eq!(name, "i"); + } + ResolverError::NumericConstantInFormatString { name, .. } => { + assert_eq!(name, "0"); + } + _ => unimplemented!(), + }; + } + } + fn path_unresolved_error(err: ResolverError, expected_unresolved_path: &str) { match err { ResolverError::PathResolutionError(PathResolutionError::Unresolved(name)) => { diff --git a/crates/noirc_frontend/src/hir/type_check/expr.rs b/crates/noirc_frontend/src/hir/type_check/expr.rs index b19833fb311..12c11bf20e1 100644 --- a/crates/noirc_frontend/src/hir/type_check/expr.rs +++ b/crates/noirc_frontend/src/hir/type_check/expr.rs @@ -111,6 +111,11 @@ impl<'interner> TypeChecker<'interner> { let len = Type::Constant(string.len() as u64); Type::String(Box::new(len)) } + HirLiteral::FmtStr(string, idents) => { + let len = Type::Constant(string.len() as u64); + let types = vecmap(&idents, |elem| self.check_expression(elem)); + Type::FmtString(Box::new(len), Box::new(Type::Tuple(types))) + } HirLiteral::Unit => Type::Unit, } } diff --git a/crates/noirc_frontend/src/hir_def/expr.rs b/crates/noirc_frontend/src/hir_def/expr.rs index 63b7e421dc3..5db9751591a 100644 --- a/crates/noirc_frontend/src/hir_def/expr.rs +++ b/crates/noirc_frontend/src/hir_def/expr.rs @@ -80,6 +80,7 @@ pub enum HirLiteral { Bool(bool), Integer(FieldElement), Str(String), + FmtStr(String, Vec), Unit, } diff --git a/crates/noirc_frontend/src/hir_def/types.rs b/crates/noirc_frontend/src/hir_def/types.rs index 6e1113345a8..4b4318f79d6 100644 --- a/crates/noirc_frontend/src/hir_def/types.rs +++ b/crates/noirc_frontend/src/hir_def/types.rs @@ -39,6 +39,10 @@ pub enum Type { /// is either a type variable of some kind or a Type::Constant. String(Box), + /// FmtString(N, Vec) is an array of characters of length N that contains + /// a list of fields specified inside the string by the following regular expression r"\{([\S]+)\}" + FmtString(Box, Box), + /// The unit type `()`. Unit, @@ -608,7 +612,6 @@ impl Type { Type::FieldElement(_) | Type::Integer(_, _, _) | Type::Bool(_) - | Type::String(_) | Type::Unit | Type::Error | Type::TypeVariable(_, _) @@ -638,6 +641,11 @@ impl Type { }) } Type::MutableReference(element) => element.contains_numeric_typevar(target_id), + Type::String(length) => named_generic_id_matches_target(length), + Type::FmtString(length, elements) => { + elements.contains_numeric_typevar(target_id) + || named_generic_id_matches_target(length) + } } } @@ -704,6 +712,9 @@ impl std::fmt::Display for Type { } Type::Bool(comp_time) => write!(f, "{comp_time}bool"), Type::String(len) => write!(f, "str<{len}>"), + Type::FmtString(len, elements) => { + write!(f, "fmtstr<{len}, {elements}>") + } Type::Unit => write!(f, "()"), Type::Error => write!(f, "error"), Type::NamedGeneric(binding, name) => match &*binding.borrow() { @@ -1057,6 +1068,13 @@ impl Type { elem_a.try_unify(elem_b, span) } + (String(len_a), String(len_b)) => len_a.try_unify(len_b, span), + + (FmtString(len_a, elements_a), FmtString(len_b, elements_b)) => { + len_a.try_unify(len_b, span)?; + elements_a.try_unify(elements_b, span) + } + (Tuple(elements_a), Tuple(elements_b)) => { if elements_a.len() != elements_b.len() { Err(SpanKind::None) @@ -1258,6 +1276,13 @@ impl Type { elem_a.is_subtype_of(elem_b, span) } + (String(len_a), String(len_b)) => len_a.is_subtype_of(len_b, span), + + (FmtString(len_a, elements_a), FmtString(len_b, elements_b)) => { + len_a.is_subtype_of(len_b, span)?; + elements_a.is_subtype_of(elements_b, span) + } + (Tuple(elements_a), Tuple(elements_b)) => { if elements_a.len() != elements_b.len() { Err(SpanKind::None) @@ -1396,6 +1421,7 @@ impl Type { .expect("Cannot have variable sized strings as a parameter to main"); AbiType::String { length: size } } + Type::FmtString(_, _) => unreachable!("format strings cannot be used in the abi"), Type::Error => unreachable!(), Type::Unit => unreachable!(), Type::Constant(_) => unreachable!(), @@ -1497,6 +1523,11 @@ impl Type { let size = Box::new(size.substitute(type_bindings)); Type::String(size) } + Type::FmtString(size, fields) => { + let size = Box::new(size.substitute(type_bindings)); + let fields = Box::new(fields.substitute(type_bindings)); + Type::FmtString(size, fields) + } Type::NamedGeneric(binding, _) | Type::TypeVariable(binding, _) => { substitute_binding(binding) } @@ -1543,6 +1574,11 @@ impl Type { match self { Type::Array(len, elem) => len.occurs(target_id) || elem.occurs(target_id), Type::String(len) => len.occurs(target_id), + Type::FmtString(len, fields) => { + let len_occurs = len.occurs(target_id); + let field_occurs = fields.occurs(target_id); + len_occurs || field_occurs + } Type::Struct(_, generic_args) => generic_args.iter().any(|arg| arg.occurs(target_id)), Type::Tuple(fields) => fields.iter().any(|field| field.occurs(target_id)), Type::NamedGeneric(binding, _) | Type::TypeVariable(binding, _) => { @@ -1582,6 +1618,11 @@ impl Type { Array(Box::new(size.follow_bindings()), Box::new(elem.follow_bindings())) } String(size) => String(Box::new(size.follow_bindings())), + FmtString(size, args) => { + let size = Box::new(size.follow_bindings()); + let args = Box::new(args.follow_bindings()); + FmtString(size, args) + } Struct(def, args) => { let args = vecmap(args, |arg| arg.follow_bindings()); Struct(def.clone(), args) diff --git a/crates/noirc_frontend/src/lexer/lexer.rs b/crates/noirc_frontend/src/lexer/lexer.rs index e376d85ddf0..8a98d5bfa3c 100644 --- a/crates/noirc_frontend/src/lexer/lexer.rs +++ b/crates/noirc_frontend/src/lexer/lexer.rs @@ -102,7 +102,8 @@ impl<'a> Lexer<'a> { Some('}') => self.single_char_token(Token::RightBrace), Some('[') => self.single_char_token(Token::LeftBracket), Some(']') => self.single_char_token(Token::RightBracket), - Some('"') => Ok(self.eat_string_literal()), + Some('"') => Ok(self.eat_string_literal(false)), + Some('f') => self.eat_format_string_or_alpha_numeric(), Some('#') => self.eat_attribute(), Some(ch) if ch.is_ascii_alphanumeric() || ch == '_' => self.eat_alpha_numeric(ch), Some(ch) => { @@ -307,13 +308,23 @@ impl<'a> Lexer<'a> { Ok(integer_token.into_span(start, end)) } - fn eat_string_literal(&mut self) -> SpannedToken { + fn eat_string_literal(&mut self, is_format_string: bool) -> SpannedToken { let (str_literal, start_span, end_span) = self.eat_while(None, |ch| ch != '"'); - let str_literal_token = Token::Str(str_literal); + let str_literal_token = + if is_format_string { Token::FmtStr(str_literal) } else { Token::Str(str_literal) }; self.next_char(); // Advance past the closing quote str_literal_token.into_span(start_span, end_span) } + fn eat_format_string_or_alpha_numeric(&mut self) -> SpannedTokenResult { + if self.peek_char_is('"') { + self.next_char(); + Ok(self.eat_string_literal(true)) + } else { + self.eat_alpha_numeric('f') + } + } + fn parse_comment(&mut self) -> SpannedTokenResult { let _ = self.eat_while(None, |ch| ch != '\n'); self.next_token() diff --git a/crates/noirc_frontend/src/lexer/token.rs b/crates/noirc_frontend/src/lexer/token.rs index b39d1640c57..3ef1d2a5dde 100644 --- a/crates/noirc_frontend/src/lexer/token.rs +++ b/crates/noirc_frontend/src/lexer/token.rs @@ -15,6 +15,7 @@ pub enum Token { Int(FieldElement), Bool(bool), Str(String), + FmtStr(String), Keyword(Keyword), IntType(IntType), Attribute(Attribute), @@ -145,6 +146,7 @@ impl fmt::Display for Token { Token::Int(n) => write!(f, "{}", n.to_u128()), Token::Bool(b) => write!(f, "{b}"), Token::Str(ref b) => write!(f, "{b}"), + Token::FmtStr(ref b) => write!(f, "f{b}"), Token::Keyword(k) => write!(f, "{k}"), Token::Attribute(ref a) => write!(f, "{a}"), Token::IntType(ref i) => write!(f, "{i}"), @@ -212,7 +214,7 @@ impl Token { pub fn kind(&self) -> TokenKind { match *self { Token::Ident(_) => TokenKind::Ident, - Token::Int(_) | Token::Bool(_) | Token::Str(_) => TokenKind::Literal, + Token::Int(_) | Token::Bool(_) | Token::Str(_) | Token::FmtStr(_) => TokenKind::Literal, Token::Keyword(_) => TokenKind::Keyword, Token::Attribute(_) => TokenKind::Attribute, ref tok => TokenKind::Token(tok.clone()), @@ -451,6 +453,7 @@ pub enum Keyword { Field, Fn, For, + FormatString, Global, If, Impl, @@ -489,6 +492,7 @@ impl fmt::Display for Keyword { Keyword::Field => write!(f, "Field"), Keyword::Fn => write!(f, "fn"), Keyword::For => write!(f, "for"), + Keyword::FormatString => write!(f, "fmtstr"), Keyword::Global => write!(f, "global"), Keyword::If => write!(f, "if"), Keyword::Impl => write!(f, "impl"), @@ -530,6 +534,7 @@ impl Keyword { "Field" => Keyword::Field, "fn" => Keyword::Fn, "for" => Keyword::For, + "fmtstr" => Keyword::FormatString, "global" => Keyword::Global, "if" => Keyword::If, "impl" => Keyword::Impl, diff --git a/crates/noirc_frontend/src/monomorphization/ast.rs b/crates/noirc_frontend/src/monomorphization/ast.rs index 488d05c6509..7ad05f09231 100644 --- a/crates/noirc_frontend/src/monomorphization/ast.rs +++ b/crates/noirc_frontend/src/monomorphization/ast.rs @@ -83,6 +83,7 @@ pub enum Literal { Integer(FieldElement, Type), Bool(bool), Str(String), + FmtStr(String, u64, Box), } #[derive(Debug, Clone)] @@ -207,6 +208,7 @@ pub enum Type { Integer(Signedness, /*bits:*/ u32), // u32 = Integer(unsigned, 32) Bool, String(/*len:*/ u64), // String(4) = str[4] + FmtString(/*len:*/ u64, Box), Unit, Tuple(Vec), Slice(Box), @@ -313,7 +315,10 @@ impl std::fmt::Display for Type { Signedness::Signed => write!(f, "i{bits}"), }, Type::Bool => write!(f, "bool"), - Type::String(len) => write!(f, "str[{len}]"), + Type::String(len) => write!(f, "str<{len}>"), + Type::FmtString(len, elements) => { + write!(f, "fmtstr<{len}, {elements}>") + } Type::Unit => write!(f, "()"), Type::Tuple(elements) => { let elements = vecmap(elements, ToString::to_string); diff --git a/crates/noirc_frontend/src/monomorphization/mod.rs b/crates/noirc_frontend/src/monomorphization/mod.rs index bb0228091da..963d16a311c 100644 --- a/crates/noirc_frontend/src/monomorphization/mod.rs +++ b/crates/noirc_frontend/src/monomorphization/mod.rs @@ -22,7 +22,7 @@ use crate::{ }, node_interner::{self, DefinitionKind, NodeInterner, StmtId}, token::Attribute, - ContractFunctionType, FunctionKind, TypeBinding, TypeBindings, TypeVariableKind, + ContractFunctionType, FunctionKind, Type, TypeBinding, TypeBindings, TypeVariableKind, }; use self::ast::{Definition, FuncId, Function, LocalId, Program}; @@ -261,6 +261,14 @@ impl<'interner> Monomorphizer<'interner> { match self.interner.expression(&expr) { HirExpression::Ident(ident) => self.ident(ident, expr), HirExpression::Literal(HirLiteral::Str(contents)) => Literal(Str(contents)), + HirExpression::Literal(HirLiteral::FmtStr(contents, idents)) => { + let fields = vecmap(idents, |ident| self.expr(ident)); + Literal(FmtStr( + contents, + fields.len() as u64, + Box::new(ast::Expression::Tuple(fields)), + )) + } HirExpression::Literal(HirLiteral::Bool(value)) => Literal(Bool(value)), HirExpression::Literal(HirLiteral::Integer(value)) => { let typ = Self::convert_type(&self.interner.id_type(expr)); @@ -587,6 +595,11 @@ impl<'interner> Monomorphizer<'interner> { HirType::Integer(_, sign, bits) => ast::Type::Integer(*sign, *bits), HirType::Bool(_) => ast::Type::Bool, HirType::String(size) => ast::Type::String(size.evaluate_to_u64().unwrap_or(0)), + HirType::FmtString(size, fields) => { + let size = size.evaluate_to_u64().unwrap_or(0); + let fields = Box::new(Self::convert_type(fields.as_ref())); + ast::Type::FmtString(size, fields) + } HirType::Unit => ast::Type::Unit, HirType::Array(length, element) => { @@ -704,18 +717,50 @@ impl<'interner> Monomorphizer<'interner> { /// of field elements to/from JSON. The type metadata attached in this method /// is the serialized `AbiType` for the argument passed to the function. /// The caller that is running a Noir program should then deserialize the `AbiType`, - /// and accurately decode the list of field elements passed to the foreign call. - fn append_abi_arg(&self, hir_argument: &HirExpression, arguments: &mut Vec) { + /// and accurately decode the list of field elements passed to the foreign call. + fn append_abi_arg( + &mut self, + hir_argument: &HirExpression, + arguments: &mut Vec, + ) { match hir_argument { HirExpression::Ident(ident) => { let typ = self.interner.id_type(ident.id); - let typ = typ.follow_bindings(); - - let abi_type = typ.as_abi_type(); - let abi_as_string = - serde_json::to_string(&abi_type).expect("ICE: expected Abi type to serialize"); + let typ: Type = typ.follow_bindings(); + match &typ { + // A format string has many different possible types that need to be handled. + // Loop over each element in the format string to fetch each type's relevant metadata + Type::FmtString(_, elements) => { + match elements.as_ref() { + Type::Tuple(element_types) => { + for typ in element_types { + let abi_type = typ.as_abi_type(); + let abi_as_string = serde_json::to_string(&abi_type) + .expect("ICE: expected Abi type to serialize"); + + arguments.push(ast::Expression::Literal(ast::Literal::Str( + abi_as_string, + ))); + } + } + _ => unreachable!( + "ICE: format string type should be a tuple but got a {elements}" + ), + } - arguments.push(ast::Expression::Literal(ast::Literal::Str(abi_as_string))); + // The caller needs information as to whether it is handling a format string or a single type + arguments.push(ast::Expression::Literal(ast::Literal::Bool(true))); + } + _ => { + let abi_type = typ.as_abi_type(); + let abi_as_string = serde_json::to_string(&abi_type) + .expect("ICE: expected Abi type to serialize"); + + arguments.push(ast::Expression::Literal(ast::Literal::Str(abi_as_string))); + // The caller needs information as to whether it is handling a format string or a single type + arguments.push(ast::Expression::Literal(ast::Literal::Bool(false))); + } + } } _ => unreachable!("logging expr {:?} is not supported", arguments[0]), } @@ -922,6 +967,18 @@ impl<'interner> Monomorphizer<'interner> { ast::Type::String(length) => { ast::Expression::Literal(ast::Literal::Str("\0".repeat(*length as usize))) } + ast::Type::FmtString(length, fields) => { + let zeroed_tuple = self.zeroed_value_of_type(fields); + let fields_len = match &zeroed_tuple { + ast::Expression::Tuple(fields) => fields.len() as u64, + _ => unreachable!("ICE: format string fields should be structured in a tuple, but got a {zeroed_tuple}"), + }; + ast::Expression::Literal(ast::Literal::FmtStr( + "\0".repeat(*length as usize), + fields_len, + Box::new(zeroed_tuple), + )) + } ast::Type::Tuple(fields) => { ast::Expression::Tuple(vecmap(fields, |field| self.zeroed_value_of_type(field))) } diff --git a/crates/noirc_frontend/src/monomorphization/printer.rs b/crates/noirc_frontend/src/monomorphization/printer.rs index 929a14e07da..ff2b7d0d256 100644 --- a/crates/noirc_frontend/src/monomorphization/printer.rs +++ b/crates/noirc_frontend/src/monomorphization/printer.rs @@ -96,6 +96,11 @@ impl AstPrinter { super::ast::Literal::Integer(x, _) => x.fmt(f), super::ast::Literal::Bool(x) => x.fmt(f), super::ast::Literal::Str(s) => s.fmt(f), + super::ast::Literal::FmtStr(s, _, _) => { + write!(f, "f\"")?; + s.fmt(f)?; + write!(f, "\"") + } } } diff --git a/crates/noirc_frontend/src/node_interner.rs b/crates/noirc_frontend/src/node_interner.rs index f01c5f22a50..062e9daf2d6 100644 --- a/crates/noirc_frontend/src/node_interner.rs +++ b/crates/noirc_frontend/src/node_interner.rs @@ -213,11 +213,11 @@ impl DefinitionKind { matches!(self, DefinitionKind::Global(..)) } - pub fn get_rhs(self) -> Option { + pub fn get_rhs(&self) -> Option { match self { DefinitionKind::Function(_) => None, - DefinitionKind::Global(id) => Some(id), - DefinitionKind::Local(id) => id, + DefinitionKind::Global(id) => Some(*id), + DefinitionKind::Local(id) => *id, DefinitionKind::GenericType(_) => None, } } @@ -637,6 +637,7 @@ fn get_type_method_key(typ: &Type) -> Option { | Type::Constant(_) | Type::Error | Type::NotConstant - | Type::Struct(_, _) => None, + | Type::Struct(_, _) + | Type::FmtString(_, _) => None, } } diff --git a/crates/noirc_frontend/src/parser/parser.rs b/crates/noirc_frontend/src/parser/parser.rs index c6d84416975..65446e5d6c6 100644 --- a/crates/noirc_frontend/src/parser/parser.rs +++ b/crates/noirc_frontend/src/parser/parser.rs @@ -795,6 +795,7 @@ fn parse_type_inner( int_type(), bool_type(), string_type(), + format_string_type(recursive_type_parser.clone()), named_type(recursive_type_parser.clone()), array_type(recursive_type_parser.clone()), recursive_type_parser.clone().delimited_by(just(Token::LeftParen), just(Token::RightParen)), @@ -841,6 +842,19 @@ fn string_type() -> impl NoirParser { .map(UnresolvedType::String) } +fn format_string_type( + type_parser: impl NoirParser, +) -> impl NoirParser { + keyword(Keyword::FormatString) + .ignore_then( + type_expression() + .then_ignore(just(Token::Comma)) + .then(type_parser) + .delimited_by(just(Token::Less), just(Token::Greater)), + ) + .map(|(size, fields)| UnresolvedType::FormatString(size, Box::new(fields))) +} + fn int_type() -> impl NoirParser { maybe_comp_time() .then(filter_map(|span, token: Token| match token { @@ -1366,6 +1380,7 @@ fn literal() -> impl NoirParser { Token::Int(x) => ExpressionKind::integer(x), Token::Bool(b) => ExpressionKind::boolean(b), Token::Str(s) => ExpressionKind::string(s), + Token::FmtStr(s) => ExpressionKind::format_string(s), unexpected => unreachable!("Non-literal {} parsed as a literal", unexpected), }) }