From a818d950574b95404cae7b1cf49b124fe9b8b39a Mon Sep 17 00:00:00 2001 From: jfecher Date: Mon, 24 Jun 2024 13:21:50 -0500 Subject: [PATCH] feat: Add some metaprogramming methods on `TypeDefinition` (#5310) # Description ## Problem\* Resolves #5285 ## Summary\* Implements `as_type`, `generics`, and `fields` on the `TypeDefinition` type. The `Type` type isn't really supported yet so `as_type` and `fields` return `Quoted` token streams instead of `Type`s for now. ## Additional Context A few bugs still need to be fixed after this PR for the vertical slice to work. ## Documentation\* Check one: - [ ] No documentation needed. - [ ] Documentation included in this PR. - [x] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --------- Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- .../src/hir/comptime/interpreter.rs | 21 ++- .../src/hir/comptime/interpreter/builtin.rs | 159 +++++++++++++++++- compiler/noirc_frontend/src/hir_def/types.rs | 19 ++- noir_stdlib/src/lib.nr | 1 + noir_stdlib/src/meta.nr | 1 + noir_stdlib/src/meta/type_def.nr | 16 ++ .../comptime_type_definition/Nargo.toml | 7 + .../comptime_type_definition/src/main.nr | 13 ++ .../derive_impl/Nargo.toml | 7 + .../derive_impl/src/main.nr | 44 +++++ 10 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 noir_stdlib/src/meta.nr create mode 100644 noir_stdlib/src/meta/type_def.nr create mode 100644 test_programs/compile_success_empty/comptime_type_definition/Nargo.toml create mode 100644 test_programs/compile_success_empty/comptime_type_definition/src/main.nr create mode 100644 test_programs/compile_success_empty/derive_impl/Nargo.toml create mode 100644 test_programs/compile_success_empty/derive_impl/src/main.nr diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs index 5d710884db1..5e236a2b980 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs @@ -98,14 +98,7 @@ impl<'a> Interpreter<'a> { .expect("all builtin functions must contain a function attribute which contains the opcode which it links to"); if let Some(builtin) = func_attrs.builtin() { - match builtin.as_str() { - "array_len" => builtin::array_len(&arguments), - "as_slice" => builtin::as_slice(arguments), - _ => { - let item = format!("Comptime evaluation for builtin function {builtin}"); - Err(InterpreterError::Unimplemented { item, location }) - } - } + builtin::call_builtin(self.interner, builtin, arguments, location) } else if let Some(foreign) = func_attrs.foreign() { let item = format!("Comptime evaluation for foreign functions like {foreign}"); Err(InterpreterError::Unimplemented { item, location }) @@ -934,6 +927,18 @@ impl<'a> Interpreter<'a> { fn evaluate_access(&mut self, access: HirMemberAccess, id: ExprId) -> IResult { let (fields, struct_type) = match self.evaluate(access.lhs)? { Value::Struct(fields, typ) => (fields, typ), + Value::Tuple(fields) => { + let (fields, field_types): (HashMap, Value>, Vec) = fields + .into_iter() + .enumerate() + .map(|(i, field)| { + let field_type = field.get_type().into_owned(); + let key_val_pair = (Rc::new(i.to_string()), field); + (key_val_pair, field_type) + }) + .unzip(); + (fields, Type::Tuple(field_types)) + } value => { let location = self.interner.expr_location(&id); return Err(InterpreterError::NonTupleOrStructInMemberAccess { value, location }); diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs index 9216ba271c1..cccc9c6d545 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs @@ -1,11 +1,36 @@ +use std::rc::Rc; + use noirc_errors::Location; use crate::{ - hir::comptime::{errors::IResult, Value}, - Type, + hir::comptime::{errors::IResult, InterpreterError, Value}, + lexer::Lexer, + macros_api::NodeInterner, + token::{SpannedToken, Token, Tokens}, + QuotedType, Type, }; -pub(super) fn array_len(arguments: &[(Value, Location)]) -> IResult { +pub(super) fn call_builtin( + interner: &NodeInterner, + name: &str, + arguments: Vec<(Value, Location)>, + location: Location, +) -> IResult { + match name { + "array_len" => array_len(&arguments), + "as_slice" => as_slice(arguments), + "slice_push_back" => slice_push_back(arguments), + "type_def_as_type" => type_def_as_type(interner, arguments), + "type_def_generics" => type_def_generics(interner, arguments), + "type_def_fields" => type_def_fields(interner, arguments), + _ => { + let item = format!("Comptime evaluation for builtin function {name}"); + Err(InterpreterError::Unimplemented { item, location }) + } + } +} + +fn array_len(arguments: &[(Value, Location)]) -> IResult { assert_eq!(arguments.len(), 1, "ICE: `array_len` should only receive a single argument"); match &arguments[0].0 { Value::Array(values, _) | Value::Slice(values, _) => Ok(Value::U32(values.len() as u32)), @@ -14,7 +39,7 @@ pub(super) fn array_len(arguments: &[(Value, Location)]) -> IResult { } } -pub(super) fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult { +fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult { assert_eq!(arguments.len(), 1, "ICE: `as_slice` should only receive a single argument"); let (array, _) = arguments.pop().unwrap(); match array { @@ -23,3 +48,129 @@ pub(super) fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult _ => unreachable!("ICE: Cannot convert types other than arrays into slices"), } } + +fn slice_push_back(mut arguments: Vec<(Value, Location)>) -> IResult { + assert_eq!(arguments.len(), 2, "ICE: `slice_push_back` should only receive two arguments"); + let (element, _) = arguments.pop().unwrap(); + let (slice, _) = arguments.pop().unwrap(); + match slice { + Value::Slice(mut values, typ) => { + values.push_back(element); + Ok(Value::Slice(values, typ)) + } + // Type checking should prevent this branch being taken. + _ => unreachable!("ICE: `slice_push_back` expects a slice as its first argument"), + } +} + +/// fn as_type(self) -> Quoted +fn type_def_as_type( + interner: &NodeInterner, + mut arguments: Vec<(Value, Location)>, +) -> IResult { + assert_eq!(arguments.len(), 1, "ICE: `generics` should only receive a single argument"); + let (type_def, span) = match arguments.pop() { + Some((Value::TypeDefinition(id), location)) => (id, location.span), + other => { + unreachable!("ICE: `as_type` expected a `TypeDefinition` argument, found {other:?}") + } + }; + + let struct_def = interner.get_struct(type_def); + let struct_def = struct_def.borrow(); + let make_token = |name| SpannedToken::new(Token::Str(name), span); + + let mut tokens = vec![make_token(struct_def.name.to_string())]; + + for (i, generic) in struct_def.generics.iter().enumerate() { + if i != 0 { + tokens.push(SpannedToken::new(Token::Comma, span)); + } + tokens.push(make_token(generic.borrow().to_string())); + } + + Ok(Value::Code(Rc::new(Tokens(tokens)))) +} + +/// fn generics(self) -> [Quoted] +fn type_def_generics( + interner: &NodeInterner, + mut arguments: Vec<(Value, Location)>, +) -> IResult { + assert_eq!(arguments.len(), 1, "ICE: `generics` should only receive a single argument"); + let (type_def, span) = match arguments.pop() { + Some((Value::TypeDefinition(id), location)) => (id, location.span), + other => { + unreachable!("ICE: `as_type` expected a `TypeDefinition` argument, found {other:?}") + } + }; + + let struct_def = interner.get_struct(type_def); + + let generics = struct_def + .borrow() + .generics + .iter() + .map(|generic| { + let name = SpannedToken::new(Token::Str(generic.borrow().to_string()), span); + Value::Code(Rc::new(Tokens(vec![name]))) + }) + .collect(); + + let typ = Type::Slice(Box::new(Type::Quoted(QuotedType::Quoted))); + Ok(Value::Slice(generics, typ)) +} + +/// fn fields(self) -> [(Quoted, Quoted)] +/// Returns (name, type) pairs of each field of this TypeDefinition +fn type_def_fields( + interner: &NodeInterner, + mut arguments: Vec<(Value, Location)>, +) -> IResult { + assert_eq!(arguments.len(), 1, "ICE: `generics` should only receive a single argument"); + let (type_def, span) = match arguments.pop() { + Some((Value::TypeDefinition(id), location)) => (id, location.span), + other => { + unreachable!("ICE: `as_type` expected a `TypeDefinition` argument, found {other:?}") + } + }; + + let struct_def = interner.get_struct(type_def); + let struct_def = struct_def.borrow(); + + let make_token = |name| SpannedToken::new(Token::Str(name), span); + let make_quoted = |tokens| Value::Code(Rc::new(Tokens(tokens))); + + let mut fields = im::Vector::new(); + + for (name, typ) in struct_def.get_fields_as_written() { + let name = make_quoted(vec![make_token(name)]); + let typ = Value::Code(Rc::new(type_to_tokens(&typ)?)); + fields.push_back(Value::Tuple(vec![name, typ])); + } + + let typ = Type::Slice(Box::new(Type::Tuple(vec![ + Type::Quoted(QuotedType::Quoted), + Type::Quoted(QuotedType::Quoted), + ]))); + Ok(Value::Slice(fields, typ)) +} + +/// FIXME(https://github.com/noir-lang/noir/issues/5309): This code is temporary. +/// It will produce poor results for type variables and will result in incorrect +/// spans on the returned tokens. +fn type_to_tokens(typ: &Type) -> IResult { + let (mut tokens, mut errors) = Lexer::lex(&typ.to_string()); + + if let Some(last) = tokens.0.last() { + if matches!(last.token(), Token::EOF) { + tokens.0.pop(); + } + } + + if !errors.is_empty() { + let error = errors.swap_remove(0); + todo!("Got lexer error: {error}") + } + Ok(tokens) +} diff --git a/compiler/noirc_frontend/src/hir_def/types.rs b/compiler/noirc_frontend/src/hir_def/types.rs index 19ab3f8853e..86d1fafd502 100644 --- a/compiler/noirc_frontend/src/hir_def/types.rs +++ b/compiler/noirc_frontend/src/hir_def/types.rs @@ -296,6 +296,16 @@ impl StructType { }) } + /// Returns the name and raw types of each field of this type. + /// This will not substitute any generic arguments so a generic field like `x` + /// in `struct Foo { x: T }` will return a `("x", T)` pair. + /// + /// This method is almost never what is wanted for type checking or monomorphization, + /// prefer to use `get_fields` whenever possible. + pub fn get_fields_as_written(&self) -> Vec<(String, Type)> { + vecmap(&self.fields, |(name, typ)| (name.0.contents.clone(), typ.clone())) + } + pub fn field_names(&self) -> BTreeSet { self.fields.iter().map(|(name, _)| name.clone()).collect() } @@ -805,10 +815,11 @@ impl Type { | Type::FmtString(_, _) | Type::Error => true, - Type::MutableReference(_) - | Type::Forall(_, _) - | Type::Quoted(_) - | Type::TraitAsType(..) => false, + // Quoted objects only exist at compile-time where the only execution + // environment is the interpreter. In this environment, they are valid. + Type::Quoted(_) => true, + + Type::MutableReference(_) | Type::Forall(_, _) | Type::TraitAsType(..) => false, Type::Alias(alias, generics) => { let alias = alias.borrow(); diff --git a/noir_stdlib/src/lib.nr b/noir_stdlib/src/lib.nr index ad47171fa46..1a756f441ba 100644 --- a/noir_stdlib/src/lib.nr +++ b/noir_stdlib/src/lib.nr @@ -26,6 +26,7 @@ mod prelude; mod uint128; mod bigint; mod runtime; +mod meta; // Oracle calls are required to be wrapped in an unconstrained function // Thus, the only argument to the `println` oracle is expected to always be an ident diff --git a/noir_stdlib/src/meta.nr b/noir_stdlib/src/meta.nr new file mode 100644 index 00000000000..1825888130b --- /dev/null +++ b/noir_stdlib/src/meta.nr @@ -0,0 +1 @@ +mod type_def; diff --git a/noir_stdlib/src/meta/type_def.nr b/noir_stdlib/src/meta/type_def.nr new file mode 100644 index 00000000000..b9354485921 --- /dev/null +++ b/noir_stdlib/src/meta/type_def.nr @@ -0,0 +1,16 @@ +impl TypeDefinition { + /// Return a syntactic version of this type definition as a type. + /// For example, `as_type(quote { type Foo { ... } })` would return `Foo` + #[builtin(type_def_as_type)] + fn as_type(self) -> Quoted {} + + /// Return each generic on this type. The names of these generics are unchanged + /// so users may need to keep name collisions in mind if this is used directly in a macro. + #[builtin(type_def_generics)] + fn generics(self) -> [Quoted] {} + + /// Returns (name, type) pairs of each field in this type. Each type is as-is + /// with any generic arguments unchanged. + #[builtin(type_def_fields)] + fn fields(self) -> [(Quoted, Quoted)] {} +} diff --git a/test_programs/compile_success_empty/comptime_type_definition/Nargo.toml b/test_programs/compile_success_empty/comptime_type_definition/Nargo.toml new file mode 100644 index 00000000000..099545a9e71 --- /dev/null +++ b/test_programs/compile_success_empty/comptime_type_definition/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "comptime_type_definition" +type = "bin" +authors = [""] +compiler_version = ">=0.31.0" + +[dependencies] \ No newline at end of file diff --git a/test_programs/compile_success_empty/comptime_type_definition/src/main.nr b/test_programs/compile_success_empty/comptime_type_definition/src/main.nr new file mode 100644 index 00000000000..025f6a0b0bf --- /dev/null +++ b/test_programs/compile_success_empty/comptime_type_definition/src/main.nr @@ -0,0 +1,13 @@ +fn main() {} + +#[my_comptime_fn] +struct MyType { + field1: [A; 10], + field2: (B, C), +} + +comptime fn my_comptime_fn(typ: TypeDefinition) { + let _ = typ.as_type(); + assert_eq(typ.generics().len(), 3); + assert_eq(typ.fields().len(), 2); +} diff --git a/test_programs/compile_success_empty/derive_impl/Nargo.toml b/test_programs/compile_success_empty/derive_impl/Nargo.toml new file mode 100644 index 00000000000..26a6020a6b1 --- /dev/null +++ b/test_programs/compile_success_empty/derive_impl/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "derive_impl" +type = "bin" +authors = [""] +compiler_version = ">=0.30.0" + +[dependencies] diff --git a/test_programs/compile_success_empty/derive_impl/src/main.nr b/test_programs/compile_success_empty/derive_impl/src/main.nr new file mode 100644 index 00000000000..abad6d4f8e1 --- /dev/null +++ b/test_programs/compile_success_empty/derive_impl/src/main.nr @@ -0,0 +1,44 @@ +comptime fn derive_default(typ: TypeDefinition) -> Quoted { + let generics: [Quoted] = typ.generics(); + assert_eq( + generics.len(), 0, "derive_default: Deriving Default on generic types is currently unimplemented" + ); + + let type_name = typ.as_type(); + let fields = typ.fields(); + + let fields = join(make_field_exprs(fields)); + + quote { + impl Default for $type_name { + fn default() -> Self { + Self { $fields } + } + } + } +} + +#[derive_default] +struct Foo { + x: Field, + y: u32, +} + +comptime fn make_field_exprs(fields: [(Quoted, Quoted)]) -> [Quoted] { + let mut result = &[]; + for my_field in fields { + let name = my_field.0; + result = result.push_back(quote { $name: Default::default(), }); + } + result +} + +comptime fn join(slice: [Quoted]) -> Quoted { + let mut result = quote {}; + for elem in slice { + result = quote { $result $elem }; + } + result +} + +fn main() {}