Skip to content

Commit

Permalink
feat: Implement traits - parser support noir-lang#2094
Browse files Browse the repository at this point in the history
* Add several parsing traits test
* Expand trait definition in AST to include body
* Add diagnostic error when where clause is not appled on generic type
  • Loading branch information
ymadzhunkov committed Aug 9, 2023
1 parent 98d0de3 commit 0df6ad4
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 22 deletions.
4 changes: 4 additions & 0 deletions crates/noirc_errors/src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ impl CustomDiagnostic {
pub fn is_error(&self) -> bool {
matches!(self.kind, DiagnosticKind::Error)
}

pub fn is_warrning(&self) -> bool {
matches!(self.kind, DiagnosticKind::Warning)
}
}

impl std::fmt::Display for CustomDiagnostic {
Expand Down
37 changes: 32 additions & 5 deletions crates/noirc_frontend/src/ast/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ use std::fmt::Display;
use iter_extended::vecmap;
use noirc_errors::Span;

use crate::{Ident, NoirFunction, UnresolvedGenerics, UnresolvedType};
use crate::{BlockExpression, Expression, Ident, NoirFunction, UnresolvedGenerics, UnresolvedType};

/// AST node for trait definitions:
/// `trait name<generics> { ... items ... }`
#[derive(Clone, Debug)]
pub struct NoirTrait {
pub name: Ident,
pub generics: Vec<Ident>,
pub where_clause: Vec<TraitConstraint>,
pub span: Span,
pub items: Vec<TraitItem>,
}

Expand All @@ -24,6 +26,12 @@ pub enum TraitItem {
parameters: Vec<(Ident, UnresolvedType)>,
return_type: UnresolvedType,
where_clause: Vec<TraitConstraint>,
body: Option<BlockExpression>,
},
Constant {
name: Ident,
typ: UnresolvedType,
default_value: Option<Expression>,
},
Type {
name: Ident,
Expand Down Expand Up @@ -68,6 +76,7 @@ pub struct TraitConstraint {
#[derive(Clone, Debug)]
pub enum TraitImplItem {
Function(NoirFunction),
Constant(Ident, UnresolvedType, Expression),
Type { name: Ident, alias: UnresolvedType },
}

Expand Down Expand Up @@ -110,7 +119,7 @@ impl Display for NoirTrait {
impl Display for TraitItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TraitItem::Function { name, generics, parameters, return_type, where_clause } => {
TraitItem::Function { name, generics, parameters, return_type, where_clause, body } => {
let generics = vecmap(generics, |generic| generic.to_string());
let parameters = vecmap(parameters, |(name, typ)| format!("{name}: {typ}"));
let where_clause = vecmap(where_clause, ToString::to_string);
Expand All @@ -121,9 +130,24 @@ impl Display for TraitItem {

write!(
f,
"fn {name}<{}>({}) -> {} where {};",
"fn {name}<{}>({}) -> {} where {}",
generics, parameters, return_type, where_clause
)
)?;

if let Some(body) = body {
write!(f, "{}", body)
} else {
write!(f, ";")
}
}
TraitItem::Constant { name, typ, default_value } => {
write!(f, "let {}: {}", name, typ)?;

if let Some(default_value) = default_value {
write!(f, "{};", default_value)
} else {
write!(f, ";")
}
}
TraitItem::Type { name } => write!(f, "type {name};"),
}
Expand Down Expand Up @@ -159,7 +183,10 @@ impl Display for TraitImplItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TraitImplItem::Function(function) => function.fmt(f),
TraitImplItem::Type { name, alias } => write!(f, "type {name} = {alias}"),
TraitImplItem::Type { name, alias } => write!(f, "type {name} = {alias};"),
TraitImplItem::Constant(name, typ, value) => {
write!(f, "let {}: {} = {};", name, typ, value)
}
}
}
}
2 changes: 2 additions & 0 deletions crates/noirc_frontend/src/parser/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub enum ParserErrorReason {
ComptimeDeprecated,
#[error("{0} are experimental and aren't fully supported yet")]
ExperimentalFeature(&'static str),
#[error("Where clauses are allowed only on functions with generic parameters")]
WhereClauseOnNonGenericFunction,
}

/// Represents a parsing error, or a parsing error in the making.
Expand Down
141 changes: 124 additions & 17 deletions crates/noirc_frontend/src/parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ fn function_definition(allow_self: bool) -> impl NoirParser<NoirFunction> {
.then(function_return_type())
.then(where_clause())
.then(block(expression()))
.map(|(((args, ret), where_clause), body)| {
.validate(|(((args, ret), where_clause), body), span, emit| {
let ((((attribute, modifiers), name), generics), parameters) = args;

validate_where_clause(&generics, &where_clause, span, emit);
FunctionDefinition {
span: name.0.span(),
name,
Expand Down Expand Up @@ -364,22 +364,38 @@ fn trait_definition() -> impl NoirParser<TopLevelStatement> {
keyword(Keyword::Trait)
.ignore_then(ident())
.then(generics())
.then(where_clause())
.then_ignore(just(Token::LeftBrace))
.then(trait_body())
.then_ignore(just(Token::RightBrace))
.validate(|((name, generics), items), span, emit| {
.validate(|(((name, generics), where_clause), items), span, emit| {
validate_where_clause(&generics, &where_clause, span, emit);
emit(ParserError::with_reason(ParserErrorReason::ExperimentalFeature("Traits"), span));
TopLevelStatement::Trait(NoirTrait { name, generics, items })
TopLevelStatement::Trait(NoirTrait { name, generics, where_clause, span, items })
})
}

fn trait_body() -> impl NoirParser<Vec<TraitItem>> {
trait_function_declaration()
.or(trait_type_declaration())
.or(trait_constant_declaration())
.separated_by(just(Token::Semicolon))
.allow_trailing()
}

fn optional_default_value() -> impl NoirParser<Option<Expression>> {
ignore_then_commit(just(Token::Assign), expression()).or_not()
}

fn trait_constant_declaration() -> impl NoirParser<TraitItem> {
keyword(Keyword::Let)
.ignore_then(ident())
.then_ignore(just(Token::Colon))
.then(parse_type())
.then(optional_default_value())
.map(|((name, typ), default_value)| TraitItem::Constant { name, typ, default_value })
}

/// trait_function_declaration: 'fn' ident generics '(' declaration_parameters ')' function_return_type
fn trait_function_declaration() -> impl NoirParser<TraitItem> {
keyword(Keyword::Fn)
Expand All @@ -388,13 +404,31 @@ fn trait_function_declaration() -> impl NoirParser<TraitItem> {
.then(parenthesized(function_declaration_parameters()))
.then(function_return_type().map(|(_, typ)| typ))
.then(where_clause())
.map(|((((name, generics), parameters), return_type), where_clause)| TraitItem::Function {
name,
generics,
parameters,
return_type,
where_clause,
})
.then(block(expression()).or_not())
.validate(
|(((((name, generics), parameters), return_type), where_clause), body), span, emit| {
validate_where_clause(&generics, &where_clause, span, emit);
TraitItem::Function { name, generics, parameters, return_type, where_clause, body }
},
)
}

fn validate_where_clause(
generics: &Vec<Ident>,
where_clause: &Vec<TraitConstraint>,
span: Span,
emit: &mut dyn FnMut(ParserError),
) {
if !where_clause.is_empty() && generics.is_empty() {
emit(ParserError::with_reason(ParserErrorReason::WhereClauseOnNonGenericFunction, span));
}

// TODO(GenericParameterNotFoundInFunction):
// Even though Rust supports where clauses that don't mention any of the generic
// parameters, these are of dubious value and can be accidentally produced by
// typos in the code, so we can consider producing compile-time errors for them.
//
// https://doc.rust-lang.org/reference/items/generics.html#where-clauses
}

/// Function declaration parameters differ from other parameters in that parameter
Expand All @@ -403,9 +437,8 @@ fn function_declaration_parameters() -> impl NoirParser<Vec<(Ident, UnresolvedTy
let typ = parse_type().recover_via(parameter_recovery());
let typ = just(Token::Colon).ignore_then(typ);

let parameter = ident().recover_via(parameter_name_recovery()).then(typ);

let parameter = parameter.or(self_parameter().validate(|param, span, emit| {
let full_parameter = ident().recover_via(parameter_name_recovery()).then(typ);
let self_parameter = self_parameter().validate(|param, span, emit| {
match param.0 {
Pattern::Identifier(ident) => (ident, param.1),
other => {
Expand All @@ -418,7 +451,9 @@ fn function_declaration_parameters() -> impl NoirParser<Vec<(Ident, UnresolvedTy
(other.into_ident(), param.1)
}
}
}));
});

let parameter = full_parameter.or(self_parameter);

parameter
.separated_by(just(Token::Comma))
Expand Down Expand Up @@ -504,7 +539,7 @@ fn where_clause() -> impl NoirParser<Vec<TraitConstraint>> {
});

keyword(Keyword::Where)
.ignore_then(constraints.repeated())
.ignore_then(constraints.separated_by(just(Token::Comma)))
.or_not()
.map(|option| option.unwrap_or_default())
}
Expand Down Expand Up @@ -1444,6 +1479,22 @@ mod test {
})
}

fn parse_all_ignore_warnings<P, T>(parser: P, programs: Vec<&str>) -> Vec<T>
where
P: NoirParser<T>,
{
vecmap(programs, move |program| {
let message = format!("Failed to parse:\n{}", program);
let (op_t, errors) = parse_recover(&parser, program);
for e in errors {
if !e.is_warrning() {
panic!("{}", &message);
}
}
op_t.expect(&message)
})
}

fn parse_all_failing<P, T>(parser: P, programs: Vec<&str>) -> Vec<CustomDiagnostic>
where
P: NoirParser<T>,
Expand Down Expand Up @@ -1743,14 +1794,70 @@ mod test {
"fn f(foo: pub u8, y : pub Field) -> u8 { x + a }",
"fn f(f: pub Field, y : Field, z : Field) -> u8 { x + a }",
"fn func_name(f: Field, y : pub Field, z : pub [u8;5],) {}",
"fn f(f: pub Field, y : Field, z : Field) -> u8 { x + a }",
"fn f<T>(f: pub Field, y : T, z : Field) -> u8 { x + a }",
"fn func_name(x: [Field], y : [Field;2],y : pub [Field;2], z : pub [u8;5]) {}",
"fn main(x: pub u8, y: pub u8) -> distinct pub [u8; 2] { [x, y] }",
],
);

parse_all_ignore_warnings(
function_definition(false),
vec![
"fn f(f: pub Field, y : Field, z : comptime Field) -> u8 { x + a }",
"fn f<T>(f: pub Field, y : T, z : comptime Field) -> u8 { x + a }",
"fn func_name<T>(f: Field, y : T) where T: SomeTrait {}",
],
);

parse_all_failing(
function_definition(false),
vec!["fn x2( f: []Field,,) {}", "fn ( f: []Field) {}", "fn ( f: []Field) {}"],
vec![
"fn x2( f: []Field,,) {}",
"fn ( f: []Field) {}",
"fn ( f: []Field) {}",
// TODO: Check for more specific error messages
"fn func_name<T>(f: Field, y : pub Field, z : pub [u8;5],) where T: {}",
"fn func_name<T>(f: Field, y : pub Field, z : pub [u8;5],) where SomeTrait {}",
"fn func_name<T>(f: Field, y : pub Field, z : pub [u8;5],) SomeTrait {}",
"fn func_name(f: Field, y : pub Field, z : pub [u8;5],) where T: SomeTrait {}",
// TODO(GenericParameterNotFoundInFunction)
// Consider making this a compilation error:
// "fn func_name<A>(f: Field, y : pub Field, z : pub [u8;5],) where T: SomeTrait {}",
],
);
}

#[test]
fn parse_trait() {
parse_all_ignore_warnings(
trait_definition(),
vec![
// Empty traits are legal in Rust and sometimes used as a way to whitelist certain types
// for a particular operation. Also known as `tag` or `marker` traits:
// https://stackoverflow.com/questions/71895489/what-is-the-purpose-of-defining-empty-impl-in-rust
"trait Empty {}",
"trait TraitWithDefaultBody { fn foo(self) {}; }",
"trait TraitAcceptingMutableRef { fn foo(&mut self); }",
"trait TraitWithTypeBoundOperation { fn identity() -> Self; }",
"trait TraitWithAssociatedType { type Element; fn item(self, index: Field) -> Self::Element; }",
"trait TraitWithAssociatedConstant { let Size: Field; }",
"trait TraitWithAssociatedConstantWithDefaultValue { let Size: Field = 10; }",
],
);

parse_all_failing(
trait_definition(),
vec![
"trait MissingBody",
"trait WrongDelimiter { fn foo() -> u8, fn bar() -> u8 }",
"trait WhereClauseWithoutGenerics where A: SomeTrait { }",
// TODO: when implemnt generics in traits the following 3 should pass
"trait GenericTrait<T> { fn elem(&mut self, index: Field) -> T; }",
"trait GenericTraitWithConstraints<T> where T: SomeTrait { fn elem(self, index: Field) -> T; }",
"trait TraitWithMultipleGenericParams<A, B, C> where A: SomeTrait, B: AnotherTrait<C> { comptime Size: Field; fn zero() -> Self; }",

],
);
}

Expand Down

0 comments on commit 0df6ad4

Please sign in to comment.