diff --git a/compiler/noirc_frontend/src/elaborator/patterns.rs b/compiler/noirc_frontend/src/elaborator/patterns.rs index 2e20dc142af..50656876d7b 100644 --- a/compiler/noirc_frontend/src/elaborator/patterns.rs +++ b/compiler/noirc_frontend/src/elaborator/patterns.rs @@ -877,6 +877,8 @@ impl Elaborator<'_> { let typ = self.resolve_type(path.typ); let check_self_param = false; + self.interner.push_type_ref_location(&typ, object_location); + let Some(method) = self.lookup_method( &typ, path.item.as_str(), diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index ec307ed5243..6d2c3fd2101 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -164,7 +164,7 @@ impl Elaborator<'_> { match resolved_type { Type::DataType(ref data_type, _) => { // Record the location of the type reference - self.interner.push_type_ref_location(resolved_type.clone(), location); + self.interner.push_type_ref_location(&resolved_type, location); if !is_synthetic { self.interner.add_type_reference( data_type.borrow().id, diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index ef5985ce7aa..d40d7fdcc31 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -876,8 +876,12 @@ impl NodeInterner { } /// Store [Location] of [Type] reference - pub fn push_type_ref_location(&mut self, typ: Type, location: Location) { - self.type_ref_locations.push((typ, location)); + pub fn push_type_ref_location(&mut self, typ: &Type, location: Location) { + if !self.is_in_lsp_mode() { + return; + } + + self.type_ref_locations.push((typ.clone(), location)); } #[allow(clippy::too_many_arguments)] diff --git a/compiler/noirc_frontend/src/parser/errors.rs b/compiler/noirc_frontend/src/parser/errors.rs index 4a4cee3a3f1..1e78504e202 100644 --- a/compiler/noirc_frontend/src/parser/errors.rs +++ b/compiler/noirc_frontend/src/parser/errors.rs @@ -1,4 +1,4 @@ -use crate::ast::{Expression, IntegerBitSize, ItemVisibility}; +use crate::ast::{Expression, IntegerBitSize, ItemVisibility, UnresolvedType}; use crate::lexer::errors::LexerErrorKind; use crate::lexer::token::Token; use crate::token::TokenKind; @@ -119,6 +119,10 @@ pub enum ParserErrorReason { MissingParametersForFunctionDefinition, #[error("`StructDefinition` is deprecated. It has been renamed to `TypeDefinition`")] StructDefinitionDeprecated, + #[error("Missing angle brackets surrounding type in associated item path")] + MissingAngleBrackets, + #[error("Expected value, found built-in type `{typ}`")] + ExpectedValueFoundBuiltInType { typ: UnresolvedType }, } /// Represents a parsing error, or a parsing error in the making. @@ -313,6 +317,10 @@ impl<'a> From<&'a ParserError> for Diagnostic { ParserErrorReason::StructDefinitionDeprecated => { Diagnostic::simple_warning(format!("{reason}"), String::new(), error.location()) } + ParserErrorReason::MissingAngleBrackets => { + let secondary = "Types that don't start with an identifier need to be surrounded with angle brackets: `<`, `>`".to_string(); + Diagnostic::simple_error(format!("{reason}"), secondary, error.location()) + } other => { Diagnostic::simple_error(format!("{other}"), String::new(), error.location()) } diff --git a/compiler/noirc_frontend/src/parser/parser/expression.rs b/compiler/noirc_frontend/src/parser/parser/expression.rs index b42bd667df0..ce050e344d1 100644 --- a/compiler/noirc_frontend/src/parser/parser/expression.rs +++ b/compiler/noirc_frontend/src/parser/parser/expression.rs @@ -6,7 +6,7 @@ use crate::{ ArrayLiteral, BlockExpression, CallExpression, CastExpression, ConstrainExpression, ConstrainKind, ConstructorExpression, Expression, ExpressionKind, Ident, IfExpression, IndexExpression, Literal, MatchExpression, MemberAccessExpression, MethodCallExpression, - Statement, TypePath, UnaryOp, UnresolvedType, UnsafeExpression, + Statement, TypePath, UnaryOp, UnresolvedType, UnresolvedTypeData, UnsafeExpression, }, parser::{ParserErrorReason, labels::ParsingRuleLabel, parser::parse_many::separated_by_comma}, token::{Keyword, Token, TokenKind}, @@ -20,6 +20,15 @@ use super::{ }, }; +/// When parsing an array literal we might bump into `[expr; length]::ident()`, +/// where the user expected to call a method on an array type. +/// That actually needs to be written as `<[expr; length]>::ident()`, so +/// in that case we'll produce an error and return `ArrayLiteralOrError::Error`. +enum ArrayLiteralOrError { + ArrayLiteral(ArrayLiteral), + Error, +} + impl Parser<'_> { pub(crate) fn parse_expression_or_error(&mut self) -> Expression { self.parse_expression_or_error_impl(true) // allow constructors @@ -286,6 +295,7 @@ impl Parser<'_> { /// | ComptimeExpression /// | UnquoteExpression /// | TypePathExpression + /// | NamelessTypePathExpression /// | AsTraitPath /// | ResolvedExpression /// | InternedExpression @@ -353,8 +363,8 @@ impl Parser<'_> { return Some(kind); } - if let Some(as_trait_path) = self.parse_as_trait_path() { - return Some(ExpressionKind::AsTraitPath(as_trait_path)); + if let Some(kind) = self.parse_nameless_type_path_or_as_trait_path_type_expression() { + return Some(kind); } if let Some(kind) = self.parse_resolved_expr() { @@ -372,6 +382,25 @@ impl Parser<'_> { None } + /// NamelessTypePathExpression = '<' Type '>' '::' identifier ( '::' GenericTypeArgs )? + fn parse_nameless_type_path_or_as_trait_path_type_expression( + &mut self, + ) -> Option { + if !self.eat_less() { + return None; + } + + let typ = self.parse_type_or_error(); + if self.eat_keyword(Keyword::As) { + let as_trait_path = self.parse_as_trait_path_for_type_after_as_keyword(typ); + Some(ExpressionKind::AsTraitPath(as_trait_path)) + } else { + self.eat_or_error(Token::Greater); + let type_path = self.parse_type_path_expr_for_type(typ); + Some(ExpressionKind::TypePath(type_path)) + } + } + /// ResolvedExpression = unquote_marker fn parse_resolved_expr(&mut self) -> Option { if let Some(token) = self.eat_kind(TokenKind::UnquoteMarker) { @@ -626,8 +655,19 @@ impl Parser<'_> { fn parse_type_path_expr(&mut self) -> Option { let start_location = self.current_token_location; let typ = self.parse_primitive_type()?; - let typ = UnresolvedType { typ, location: self.location_since(start_location) }; + let location = self.location_since(start_location); + let typ = UnresolvedType { typ, location }; + if self.at(Token::DoubleColon) { + Some(ExpressionKind::TypePath(self.parse_type_path_expr_for_type(typ))) + } else { + // This is the case when we find `Field` or `i32` but `::` doesn't follow it. + self.push_error(ParserErrorReason::ExpectedValueFoundBuiltInType { typ }, location); + Some(ExpressionKind::Error) + } + } + + fn parse_type_path_expr_for_type(&mut self, typ: UnresolvedType) -> TypePath { self.eat_or_error(Token::DoubleColon); let item = if let Some(ident) = self.eat_ident() { @@ -645,7 +685,7 @@ impl Parser<'_> { generics }); - Some(ExpressionKind::TypePath(TypePath { typ, item, turbofish })) + TypePath { typ, item, turbofish } } /// Literal @@ -690,12 +730,22 @@ impl Parser<'_> { return Some(ExpressionKind::Quote(tokens)); } - if let Some(literal) = self.parse_array_literal() { - return Some(ExpressionKind::Literal(Literal::Array(literal))); + if let Some(literal_or_error) = self.parse_array_literal() { + return match literal_or_error { + ArrayLiteralOrError::ArrayLiteral(literal) => { + Some(ExpressionKind::Literal(Literal::Array(literal))) + } + ArrayLiteralOrError::Error => Some(ExpressionKind::Error), + }; } - if let Some(literal) = self.parse_slice_literal() { - return Some(ExpressionKind::Literal(Literal::Slice(literal))); + if let Some(literal_or_error) = self.parse_slice_literal() { + return match literal_or_error { + ArrayLiteralOrError::ArrayLiteral(literal) => { + Some(ExpressionKind::Literal(Literal::Slice(literal))) + } + ArrayLiteralOrError::Error => Some(ExpressionKind::Error), + }; } if let Some(kind) = self.parse_block() { @@ -718,27 +768,44 @@ impl Parser<'_> { /// ArrayElements = Expression ( ',' Expression )? ','? /// /// RepeatedArrayLiteral = '[' Expression ';' TypeExpression ']' - fn parse_array_literal(&mut self) -> Option { + fn parse_array_literal(&mut self) -> Option { + let start_location = self.current_token_location; + let errors_before_array = self.errors.len(); + if !self.eat_left_bracket() { return None; } if self.eat_right_bracket() { - return Some(ArrayLiteral::Standard(Vec::new())); + return Some(ArrayLiteralOrError::ArrayLiteral(ArrayLiteral::Standard(Vec::new()))); } let first_expr = self.parse_expression_or_error(); - if first_expr.kind == ExpressionKind::Error { - return Some(ArrayLiteral::Standard(Vec::new())); - } if self.eat_semicolon() { let length = self.parse_expression_or_error(); self.eat_or_error(Token::RightBracket); - return Some(ArrayLiteral::Repeated { + + // If it's `[expr; length]::ident`, give an error that it's missing `<...>` + if self.at(Token::DoubleColon) && matches!(self.next_token.token(), Token::Ident(..)) { + // Remove any errors that happened during `[...]` as it's likely they happened + // because of the missing angle brackets. + self.errors.truncate(errors_before_array); + + let location = self.location_since(start_location); + self.push_error(ParserErrorReason::MissingAngleBrackets, location); + + // Skip `::` and the identifier + self.bump(); + self.bump(); + + return Some(ArrayLiteralOrError::Error); + } + + return Some(ArrayLiteralOrError::ArrayLiteral(ArrayLiteral::Repeated { repeated_element: Box::new(first_expr), length: Box::new(length), - }); + })); } let comma_after_first_expr = self.eat_comma(); @@ -756,11 +823,11 @@ impl Parser<'_> { exprs.insert(0, first_expr); - Some(ArrayLiteral::Standard(exprs)) + Some(ArrayLiteralOrError::ArrayLiteral(ArrayLiteral::Standard(exprs))) } /// SliceExpression = '&' ArrayLiteral - fn parse_slice_literal(&mut self) -> Option { + fn parse_slice_literal(&mut self) -> Option { if !(self.at(Token::SliceStart) && self.next_is(Token::LeftBracket)) { return None; } @@ -780,11 +847,26 @@ impl Parser<'_> { /// /// TupleExpression = '(' Expression ( ',' Expression )+ ','? ')' fn parse_parentheses_expression(&mut self) -> Option { + let start_location = self.current_token_location; + let errors_before_parentheses = self.errors.len(); + if !self.eat_left_paren() { return None; } if self.eat_right_paren() { + // If it's `()::ident`, parse it as a type path but produce an error saying it should be `<()>::ident`. + if self.at(Token::DoubleColon) && matches!(self.next_token.token(), Token::Ident(..)) { + let location = self.location_since(start_location); + let typ = UnresolvedTypeData::Unit; + let typ = UnresolvedType { typ, location }; + let type_path = self.parse_type_path_expr_for_type(typ); + + self.push_error(ParserErrorReason::MissingAngleBrackets, location); + + return Some(ExpressionKind::TypePath(type_path)); + } + return Some(ExpressionKind::Literal(Literal::Unit)); } @@ -794,6 +876,22 @@ impl Parser<'_> { Self::parse_expression_in_list, ); + // If it's `(..)::ident`, give an error that it's missing `<...>` + if self.at(Token::DoubleColon) && matches!(self.next_token.token(), Token::Ident(..)) { + // Remove any errors that happened during `(...)` as it's likely they happened + // because of the missing angle brackets. + self.errors.truncate(errors_before_parentheses); + + let location = self.location_since(start_location); + self.push_error(ParserErrorReason::MissingAngleBrackets, location); + + // Skip `::` and the identifier + self.bump(); + self.bump(); + + return Some(ExpressionKind::Error); + } + Some(if exprs.len() == 1 && !trailing_comma { ExpressionKind::Parenthesized(Box::new(exprs.remove(0))) } else { @@ -1827,6 +1925,99 @@ mod tests { assert!(type_path.turbofish.is_some()); } + #[test] + fn parses_type_path_with_tuple() { + let src = "<()>::foo"; + let expr = parse_expression_no_errors(src); + let ExpressionKind::TypePath(type_path) = expr.kind else { + panic!("Expected type_path"); + }; + assert_eq!(type_path.typ.to_string(), "()"); + assert_eq!(type_path.item.to_string(), "foo"); + assert!(type_path.turbofish.is_none()); + } + + #[test] + fn parses_type_path_with_array_type() { + let src = "<[i32; 3]>::foo"; + let expr = parse_expression_no_errors(src); + let ExpressionKind::TypePath(type_path) = expr.kind else { + panic!("Expected type_path"); + }; + assert_eq!(type_path.typ.to_string(), "[i32; 3]"); + assert_eq!(type_path.item.to_string(), "foo"); + assert!(type_path.turbofish.is_none()); + } + + #[test] + fn parses_type_path_with_empty_tuple_missing_angle_brackets() { + let src = " + ()::foo + ^^ + "; + let (src, span) = get_source_with_error_span(src); + let mut parser = Parser::for_str_with_dummy_file(&src); + let expr = parser.parse_expression_or_error(); + + let ExpressionKind::TypePath(type_path) = expr.kind else { + panic!("Expected type_path"); + }; + assert_eq!(type_path.typ.to_string(), "()"); + assert_eq!(type_path.item.to_string(), "foo"); + assert!(type_path.turbofish.is_none()); + + let reason = get_single_error_reason(&parser.errors, span); + assert!(matches!(reason, ParserErrorReason::MissingAngleBrackets)); + } + + #[test] + fn parses_type_path_with_non_empty_tuple_missing_angle_brackets() { + let src = " + (Field, i32)::foo + ^^^^^^^^^^^^ + "; + let (src, span) = get_source_with_error_span(src); + let mut parser = Parser::for_str_with_dummy_file(&src); + let expr = parser.parse_expression_or_error(); + + assert!(matches!(expr.kind, ExpressionKind::Error)); + + let reason = get_single_error_reason(&parser.errors, span); + assert!(matches!(reason, ParserErrorReason::MissingAngleBrackets)); + } + + #[test] + fn parses_type_path_with_array_missing_angle_brackets() { + let src = " + [Field; 3]::foo + ^^^^^^^^^^ + "; + let (src, span) = get_source_with_error_span(src); + let mut parser = Parser::for_str_with_dummy_file(&src); + let expr = parser.parse_expression_or_error(); + + assert!(matches!(expr.kind, ExpressionKind::Error)); + + let reason = get_single_error_reason(&parser.errors, span); + assert!(matches!(reason, ParserErrorReason::MissingAngleBrackets)); + } + + #[test] + fn parses_primitive_type_errors() { + let src = " + Field + ^^^^^ + "; + let (src, span) = get_source_with_error_span(src); + let mut parser = Parser::for_str_with_dummy_file(&src); + let expr = parser.parse_expression_or_error(); + let ExpressionKind::Error = expr.kind else { + panic!("Expected error"); + }; + let reason = get_single_error_reason(&parser.errors, span); + assert_eq!(reason.to_string(), "Expected value, found built-in type `Field`"); + } + #[test] fn parses_unquote_var() { let src = "$foo::bar"; diff --git a/compiler/noirc_frontend/src/parser/parser/path.rs b/compiler/noirc_frontend/src/parser/parser/path.rs index a58bd9e1bb1..f27cce07b89 100644 --- a/compiler/noirc_frontend/src/parser/parser/path.rs +++ b/compiler/noirc_frontend/src/parser/parser/path.rs @@ -193,6 +193,14 @@ impl Parser<'_> { let typ = self.parse_type_or_error(); self.eat_keyword_or_error(Keyword::As); + + Some(self.parse_as_trait_path_for_type_after_as_keyword(typ)) + } + + pub(super) fn parse_as_trait_path_for_type_after_as_keyword( + &mut self, + typ: UnresolvedType, + ) -> AsTraitPath { let trait_path = self.parse_path_no_turbofish_or_error(); let trait_generics = self.parse_generic_type_args(); self.eat_or_error(Token::Greater); @@ -204,7 +212,7 @@ impl Parser<'_> { Ident::new(String::new(), self.location_at_previous_token_end()) }; - Some(AsTraitPath { typ, trait_path, trait_generics, impl_item }) + AsTraitPath { typ, trait_path, trait_generics, impl_item } } } diff --git a/compiler/noirc_frontend/src/resolve_locations.rs b/compiler/noirc_frontend/src/resolve_locations.rs index 45eb251076b..0e927d0b067 100644 --- a/compiler/noirc_frontend/src/resolve_locations.rs +++ b/compiler/noirc_frontend/src/resolve_locations.rs @@ -228,13 +228,17 @@ impl NodeInterner { /// Attempts to resolve [Location] of [Type] based on [Location] of reference in code pub(crate) fn try_resolve_type_ref(&self, location: Location) -> Option { + self.try_type_ref_at_location(location).and_then(|typ| match typ { + Type::DataType(struct_typ, _) => Some(struct_typ.borrow().location), + _ => None, + }) + } + + pub fn try_type_ref_at_location(&self, location: Location) -> Option { self.type_ref_locations .iter() .find(|(_typ, type_ref_location)| type_ref_location.contains(&location)) - .and_then(|(typ, _)| match typ { - Type::DataType(struct_typ, _) => Some(struct_typ.borrow().location), - _ => None, - }) + .map(|(typ, _type_ref_location)| typ.clone()) } fn try_resolve_type_alias(&self, location: Location) -> Option { diff --git a/test_programs/compile_success_empty/type_path/src/main.nr b/test_programs/compile_success_empty/type_path/src/main.nr index ca9a9008f6c..3d905d8b56b 100644 --- a/test_programs/compile_success_empty/type_path/src/main.nr +++ b/test_programs/compile_success_empty/type_path/src/main.nr @@ -10,6 +10,10 @@ fn main() { // whether a TypePath had generics or not, always resolved them, filling them // up with Type::Error, and eventually leading to an ICE. let _ = Field::from_be_bytes([1]); + + // Make sure `<...>::name` compiles + let _: () = <()>::method(); + let _: [i32; 3] = <[i32; 3]>::method(); } pub struct Foo {} @@ -17,3 +21,19 @@ pub struct Foo {} impl Foo { fn static() {} } + +trait Trait { + fn method() -> Self; +} + +impl Trait for () { + fn method() -> () { + () + } +} + +impl Trait for [i32; 3] { + fn method() -> [i32; 3] { + [1, 2, 3] + } +} diff --git a/tooling/lsp/src/requests/completion.rs b/tooling/lsp/src/requests/completion.rs index fa2de1eeda8..3932bcfe471 100644 --- a/tooling/lsp/src/requests/completion.rs +++ b/tooling/lsp/src/requests/completion.rs @@ -15,15 +15,14 @@ use kinds::{FunctionCompletionKind, FunctionKind, RequestedItems}; use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse}; use noirc_errors::{Location, Span}; use noirc_frontend::{ - DataType, Kind, ParsedModule, Type, TypeBinding, + DataType, ParsedModule, Type, TypeBinding, ast::{ AsTraitPath, AttributeTarget, BlockExpression, CallExpression, ConstructorExpression, Expression, ExpressionKind, ForLoopStatement, GenericTypeArgs, Ident, IfExpression, - IntegerBitSize, ItemVisibility, LValue, Lambda, LetStatement, MemberAccessExpression, - MethodCallExpression, ModuleDeclaration, NoirFunction, NoirStruct, NoirTraitImpl, Path, - PathKind, Pattern, Statement, TraitBound, TraitImplItemKind, TypeImpl, TypePath, - UnresolvedGeneric, UnresolvedGenerics, UnresolvedType, UnresolvedTypeData, - UnresolvedTypeExpression, UseTree, UseTreeKind, Visitor, + ItemVisibility, LValue, Lambda, LetStatement, MemberAccessExpression, MethodCallExpression, + ModuleDeclaration, NoirFunction, NoirStruct, NoirTraitImpl, Path, PathKind, Pattern, + Statement, TraitBound, TraitImplItemKind, TypeImpl, TypePath, UnresolvedGeneric, + UnresolvedGenerics, UnresolvedType, UnresolvedTypeData, UseTree, UseTreeKind, Visitor, }, graph::{CrateId, Dependency}, hir::{ @@ -35,7 +34,6 @@ use noirc_frontend::{ hir_def::traits::Trait, node_interner::{FuncId, NodeInterner, ReferenceId, TypeId}, parser::{Item, ItemKind, ParsedSubModule}, - shared::Signedness, token::{MetaAttribute, Token, Tokens}, }; use sort_text::underscore_sort_text; @@ -1802,35 +1800,19 @@ impl Visitor for NodeFinder<'_> { return true; } - let typ = match &type_path.typ.typ { - UnresolvedTypeData::FieldElement => Some(Type::FieldElement), - UnresolvedTypeData::Integer(signedness, integer_bit_size) => { - Some(Type::Integer(*signedness, *integer_bit_size)) - } - UnresolvedTypeData::Bool => Some(Type::Bool), - UnresolvedTypeData::String(UnresolvedTypeExpression::Constant(value, _)) => { - Some(Type::String(Box::new(Type::Constant( - *value, - Kind::Numeric(Box::new(Type::Integer( - Signedness::Unsigned, - IntegerBitSize::ThirtyTwo, - ))), - )))) - } - UnresolvedTypeData::Quoted(quoted_type) => Some(Type::Quoted(*quoted_type)), - _ => None, + let location = type_path.typ.location; + let Some(typ) = self.interner.try_type_ref_at_location(location) else { + return true; }; - if let Some(typ) = typ { - let prefix = type_path.item.as_str(); - self.complete_type_methods( - &typ, - prefix, - FunctionKind::Any, - FunctionCompletionKind::NameAndParameters, - false, // self_prefix - ); - } + let prefix = type_path.item.as_str(); + self.complete_type_methods( + &typ, + prefix, + FunctionKind::Any, + FunctionCompletionKind::NameAndParameters, + false, // self_prefix + ); false } diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index 64c91dd673d..a7f5e945bfc 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -445,6 +445,30 @@ mod completion_tests { .await; } + #[test] + async fn test_complete_type_path_for_nameless_type() { + let src = r#" + trait One { + fn some_method() -> Self; + } + + impl One for () { + fn some_method() -> Self { + 1 + } + } + + fn main() { + <()>::some_meth>|< + } + "#; + assert_completion( + src, + vec![function_completion_item("some_method()", "some_method()", "fn()")], + ) + .await; + } + #[test] async fn test_complete_function_without_arguments() { let src = r#" diff --git a/tooling/nargo_fmt/src/formatter/expression.rs b/tooling/nargo_fmt/src/formatter/expression.rs index 49697c6ce00..bc516dc98a5 100644 --- a/tooling/nargo_fmt/src/formatter/expression.rs +++ b/tooling/nargo_fmt/src/formatter/expression.rs @@ -399,7 +399,17 @@ impl ChunkFormatter<'_, '_> { pub(super) fn format_type_path(&mut self, type_path: TypePath) -> ChunkGroup { let mut group = ChunkGroup::new(); group.text(self.chunk(|formatter| { + let nameless = formatter.is_at(Token::Less); + if nameless { + formatter.write_token(Token::Less); + } + formatter.format_type(type_path.typ); + + if nameless { + formatter.write_token(Token::Greater); + } + formatter.write_token(Token::DoubleColon); formatter.write_identifier(type_path.item); if let Some(turbofish) = type_path.turbofish { @@ -2132,6 +2142,13 @@ global y = 1; assert_format(src, expected); } + #[test] + fn format_type_path_with_array_type() { + let src = "global x = < [ i32 ; 3 ] > :: max ;"; + let expected = "global x = <[i32; 3]>::max;\n"; + assert_format(src, expected); + } + #[test] fn format_if_expression_without_else_one_expression() { let src = "global x = if 1 { 2 } ;";