Skip to content

Commit

Permalink
feat: Add some metaprogramming methods on TypeDefinition (#5310)
Browse files Browse the repository at this point in the history
# 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 <[email protected]>
  • Loading branch information
jfecher and TomAFrench authored Jun 24, 2024
1 parent 9b59f6b commit a818d95
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 16 deletions.
21 changes: 13 additions & 8 deletions compiler/noirc_frontend/src/hir/comptime/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -934,6 +927,18 @@ impl<'a> Interpreter<'a> {
fn evaluate_access(&mut self, access: HirMemberAccess, id: ExprId) -> IResult<Value> {
let (fields, struct_type) = match self.evaluate(access.lhs)? {
Value::Struct(fields, typ) => (fields, typ),
Value::Tuple(fields) => {
let (fields, field_types): (HashMap<Rc<String>, Value>, Vec<Type>) = 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 });
Expand Down
159 changes: 155 additions & 4 deletions compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs
Original file line number Diff line number Diff line change
@@ -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<Value> {
pub(super) fn call_builtin(
interner: &NodeInterner,
name: &str,
arguments: Vec<(Value, Location)>,
location: Location,
) -> IResult<Value> {
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<Value> {
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)),
Expand All @@ -14,7 +39,7 @@ pub(super) fn array_len(arguments: &[(Value, Location)]) -> IResult<Value> {
}
}

pub(super) fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult<Value> {
fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult<Value> {
assert_eq!(arguments.len(), 1, "ICE: `as_slice` should only receive a single argument");
let (array, _) = arguments.pop().unwrap();
match array {
Expand All @@ -23,3 +48,129 @@ pub(super) fn as_slice(mut arguments: Vec<(Value, Location)>) -> IResult<Value>
_ => unreachable!("ICE: Cannot convert types other than arrays into slices"),
}
}

fn slice_push_back(mut arguments: Vec<(Value, Location)>) -> IResult<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Tokens> {
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)
}
19 changes: 15 additions & 4 deletions compiler/noirc_frontend/src/hir_def/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> { 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<Ident> {
self.fields.iter().map(|(name, _)| name.clone()).collect()
}
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions noir_stdlib/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions noir_stdlib/src/meta.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod type_def;
16 changes: 16 additions & 0 deletions noir_stdlib/src/meta/type_def.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
impl TypeDefinition {
/// Return a syntactic version of this type definition as a type.
/// For example, `as_type(quote { type Foo<A, B> { ... } })` would return `Foo<A, B>`
#[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)] {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "comptime_type_definition"
type = "bin"
authors = [""]
compiler_version = ">=0.31.0"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
fn main() {}

#[my_comptime_fn]
struct MyType<A, B, C> {
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);
}
7 changes: 7 additions & 0 deletions test_programs/compile_success_empty/derive_impl/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "derive_impl"
type = "bin"
authors = [""]
compiler_version = ">=0.30.0"

[dependencies]
44 changes: 44 additions & 0 deletions test_programs/compile_success_empty/derive_impl/src/main.nr
Original file line number Diff line number Diff line change
@@ -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() {}

0 comments on commit a818d95

Please sign in to comment.