diff --git a/compiler/noirc_frontend/src/ast/statement.rs b/compiler/noirc_frontend/src/ast/statement.rs index bd65fc33377..57572e80d1e 100644 --- a/compiler/noirc_frontend/src/ast/statement.rs +++ b/compiler/noirc_frontend/src/ast/statement.rs @@ -45,6 +45,7 @@ pub enum StatementKind { Expression(Expression), Assign(AssignStatement), For(ForLoopStatement), + Loop(Expression), Break, Continue, /// This statement should be executed at compile-time @@ -106,6 +107,9 @@ impl StatementKind { // A semicolon on a for loop is optional and does nothing StatementKind::For(_) => self, + // A semicolon on a loop is optional and does nothing + StatementKind::Loop(..) => self, + // No semicolon needed for a resolved statement StatementKind::Interned(_) => self, @@ -961,6 +965,7 @@ impl Display for StatementKind { StatementKind::Expression(expression) => expression.fmt(f), StatementKind::Assign(assign) => assign.fmt(f), StatementKind::For(for_loop) => for_loop.fmt(f), + StatementKind::Loop(block) => write!(f, "loop {}", block), StatementKind::Break => write!(f, "break"), StatementKind::Continue => write!(f, "continue"), StatementKind::Comptime(statement) => write!(f, "comptime {}", statement.kind), diff --git a/compiler/noirc_frontend/src/ast/visitor.rs b/compiler/noirc_frontend/src/ast/visitor.rs index 2f60532980a..ec50a982a70 100644 --- a/compiler/noirc_frontend/src/ast/visitor.rs +++ b/compiler/noirc_frontend/src/ast/visitor.rs @@ -296,6 +296,10 @@ pub trait Visitor { true } + fn visit_loop_statement(&mut self, _: &Expression) -> bool { + true + } + fn visit_comptime_statement(&mut self, _: &Statement) -> bool { true } @@ -1104,6 +1108,11 @@ impl Statement { StatementKind::For(for_loop_statement) => { for_loop_statement.accept(visitor); } + StatementKind::Loop(block) => { + if visitor.visit_loop_statement(block) { + block.accept(visitor); + } + } StatementKind::Comptime(statement) => { if visitor.visit_comptime_statement(statement) { statement.accept(visitor); diff --git a/compiler/noirc_frontend/src/elaborator/statements.rs b/compiler/noirc_frontend/src/elaborator/statements.rs index 8a46d85d563..24653772f9f 100644 --- a/compiler/noirc_frontend/src/elaborator/statements.rs +++ b/compiler/noirc_frontend/src/elaborator/statements.rs @@ -33,6 +33,7 @@ impl<'context> Elaborator<'context> { StatementKind::Constrain(constrain) => self.elaborate_constrain(constrain), StatementKind::Assign(assign) => self.elaborate_assign(assign), StatementKind::For(for_stmt) => self.elaborate_for(for_stmt), + StatementKind::Loop(block) => self.elaborate_loop(block, statement.span), StatementKind::Break => self.elaborate_jump(true, statement.span), StatementKind::Continue => self.elaborate_jump(false, statement.span), StatementKind::Comptime(statement) => self.elaborate_comptime_statement(*statement), @@ -268,6 +269,15 @@ impl<'context> Elaborator<'context> { (statement, Type::Unit) } + pub(super) fn elaborate_loop( + &mut self, + _block: Expression, + span: noirc_errors::Span, + ) -> (HirStatement, Type) { + self.push_err(ResolverError::LoopNotYetSupported { span }); + (HirStatement::Error, Type::Unit) + } + fn elaborate_jump(&mut self, is_break: bool, span: noirc_errors::Span) -> (HirStatement, Type) { let in_constrained_function = self.in_constrained_function(); diff --git a/compiler/noirc_frontend/src/hir/comptime/display.rs b/compiler/noirc_frontend/src/hir/comptime/display.rs index 29d1448f07e..ccdfdf00e72 100644 --- a/compiler/noirc_frontend/src/hir/comptime/display.rs +++ b/compiler/noirc_frontend/src/hir/comptime/display.rs @@ -732,6 +732,9 @@ fn remove_interned_in_statement_kind( block: remove_interned_in_expression(interner, for_loop.block), ..for_loop }), + StatementKind::Loop(block) => { + StatementKind::Loop(remove_interned_in_expression(interner, block)) + } StatementKind::Comptime(statement) => { StatementKind::Comptime(Box::new(remove_interned_in_statement(interner, *statement))) } diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs index a4b3c1b9c07..e0e09d53311 100644 --- a/compiler/noirc_frontend/src/hir/resolution/errors.rs +++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs @@ -170,6 +170,8 @@ pub enum ResolverError { span: Span, missing_trait_location: Location, }, + #[error("`loop` statements are not yet implemented")] + LoopNotYetSupported { span: Span }, } impl ResolverError { @@ -642,6 +644,13 @@ impl<'a> From<&'a ResolverError> for Diagnostic { diagnostic.add_secondary_with_file(format!("required by this bound in `{impl_trait}"), missing_trait_location.span, missing_trait_location.file); diagnostic }, + ResolverError::LoopNotYetSupported { span } => { + Diagnostic::simple_error( + "`loop` statements are not yet implemented".to_string(), + String::new(), + *span) + + } } } } diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs index 8df831bbaab..8c136f5e45d 100644 --- a/compiler/noirc_frontend/src/lexer/token.rs +++ b/compiler/noirc_frontend/src/lexer/token.rs @@ -1031,6 +1031,7 @@ pub enum Keyword { Impl, In, Let, + Loop, Match, Mod, Module, @@ -1090,6 +1091,7 @@ impl fmt::Display for Keyword { Keyword::Impl => write!(f, "impl"), Keyword::In => write!(f, "in"), Keyword::Let => write!(f, "let"), + Keyword::Loop => write!(f, "loop"), Keyword::Match => write!(f, "match"), Keyword::Mod => write!(f, "mod"), Keyword::Module => write!(f, "Module"), @@ -1152,6 +1154,7 @@ impl Keyword { "impl" => Keyword::Impl, "in" => Keyword::In, "let" => Keyword::Let, + "loop" => Keyword::Loop, "match" => Keyword::Match, "mod" => Keyword::Mod, "Module" => Keyword::Module, diff --git a/compiler/noirc_frontend/src/parser/parser/statement.rs b/compiler/noirc_frontend/src/parser/parser/statement.rs index 315f3b9f958..465e48e3bad 100644 --- a/compiler/noirc_frontend/src/parser/parser/statement.rs +++ b/compiler/noirc_frontend/src/parser/parser/statement.rs @@ -92,6 +92,7 @@ impl<'a> Parser<'a> { /// | ConstrainStatement /// | ComptimeStatement /// | ForStatement + /// | LoopStatement /// | IfStatement /// | BlockStatement /// | AssignStatement @@ -156,6 +157,10 @@ impl<'a> Parser<'a> { return Some(StatementKind::For(for_loop)); } + if let Some(block) = self.parse_loop() { + return Some(StatementKind::Loop(block)); + } + if let Some(kind) = self.parse_if_expr() { return Some(StatementKind::Expression(Expression { kind, @@ -287,6 +292,26 @@ impl<'a> Parser<'a> { Some(ForLoopStatement { identifier, range, block, span: self.span_since(start_span) }) } + /// LoopStatement = 'loop' Block + fn parse_loop(&mut self) -> Option { + if !self.eat_keyword(Keyword::Loop) { + return None; + } + + let block_start_span = self.current_token_span; + let block = if let Some(block) = self.parse_block() { + Expression { + kind: ExpressionKind::Block(block), + span: self.span_since(block_start_span), + } + } else { + self.expected_token(Token::LeftBrace); + Expression { kind: ExpressionKind::Error, span: self.span_since(block_start_span) } + }; + + Some(block) + } + /// ForRange /// = ExpressionExceptConstructor /// | ExpressionExceptConstructor '..' ExpressionExceptConstructor @@ -790,4 +815,30 @@ mod tests { assert!(statement.is_none()); assert_eq!(parser.errors.len(), 2); } + + #[test] + fn parses_empty_loop() { + let src = "loop { }"; + let statement = parse_statement_no_errors(src); + let StatementKind::Loop(block) = statement.kind else { + panic!("Expected loop"); + }; + let ExpressionKind::Block(block) = block.kind else { + panic!("Expected block"); + }; + assert!(block.statements.is_empty()); + } + + #[test] + fn parses_loop_with_statements() { + let src = "loop { 1; 2 }"; + let statement = parse_statement_no_errors(src); + let StatementKind::Loop(block) = statement.kind else { + panic!("Expected loop"); + }; + let ExpressionKind::Block(block) = block.kind else { + panic!("Expected block"); + }; + assert_eq!(block.statements.len(), 2); + } } diff --git a/test_programs/compile_success_empty/macros_in_comptime/src/main.nr b/test_programs/compile_success_empty/macros_in_comptime/src/main.nr index 0572192225c..112ed16c22a 100644 --- a/test_programs/compile_success_empty/macros_in_comptime/src/main.nr +++ b/test_programs/compile_success_empty/macros_in_comptime/src/main.nr @@ -33,8 +33,8 @@ comptime fn foo(x: Field) { break; } - let loop = quote { for _ in 0..0 { break; } }; - unquote!(loop); + let loop_ = quote { for _ in 0..0 { break; } }; + unquote!(loop_); } mod submodule { diff --git a/test_programs/execution_success/hint_black_box/src/main.nr b/test_programs/execution_success/hint_black_box/src/main.nr index abceadb07ff..ed2dc2d3760 100644 --- a/test_programs/execution_success/hint_black_box/src/main.nr +++ b/test_programs/execution_success/hint_black_box/src/main.nr @@ -2,12 +2,12 @@ use std::hint::black_box; fn main(a: u32, b: u32) { // This version unrolls into a number of additions - assert_eq(loop(5, a), b); + assert_eq(loop_(5, a), b); // This version simplifies into a single `constraint 50 == b` - assert_eq(loop(5, 10), b); + assert_eq(loop_(5, 10), b); // This version should not simplify down to a single constraint, // it should treat 10 as opaque: - assert_eq(loop(5, black_box(10)), b); + assert_eq(loop_(5, black_box(10)), b); // Check array handling. let arr = [a, a, a, a, a]; @@ -54,7 +54,7 @@ fn main(a: u32, b: u32) { //assert_eq(c, b); } -fn loop(n: u32, k: u32) -> u32 { +fn loop_(n: u32, k: u32) -> u32 { let mut sum = 0; for _ in 0..n { sum = sum + k; diff --git a/test_programs/execution_success/loop/src/main.nr b/test_programs/execution_success/loop/src/main.nr index b3be4c4c3ff..3e1f24742ec 100644 --- a/test_programs/execution_success/loop/src/main.nr +++ b/test_programs/execution_success/loop/src/main.nr @@ -2,12 +2,12 @@ // // The features being tested is basic looping. fn main(six_as_u32: u32) { - assert_eq(loop(4), six_as_u32); + assert_eq(loop_excl(4), six_as_u32); assert_eq(loop_incl(3), six_as_u32); assert(plain_loop() == six_as_u32); } -fn loop(x: u32) -> u32 { +fn loop_excl(x: u32) -> u32 { let mut sum = 0; for i in 0..x { sum = sum + i; diff --git a/test_programs/execution_success/loop_invariant_regression/src/main.nr b/test_programs/execution_success/loop_invariant_regression/src/main.nr index c28ce063116..61f8b1bedba 100644 --- a/test_programs/execution_success/loop_invariant_regression/src/main.nr +++ b/test_programs/execution_success/loop_invariant_regression/src/main.nr @@ -1,11 +1,11 @@ // Tests a simple loop where we expect loop invariant instructions // to be hoisted to the loop's pre-header block. fn main(x: u32, y: u32) { - loop(4, x, y); + loop_(4, x, y); array_read_loop(4, x); } -fn loop(upper_bound: u32, x: u32, y: u32) { +fn loop_(upper_bound: u32, x: u32, y: u32) { for _ in 0..upper_bound { let mut z = x * y; z = z * x; diff --git a/tooling/lsp/src/requests/completion/builtins.rs b/tooling/lsp/src/requests/completion/builtins.rs index c0910e9005e..90b8c6301b7 100644 --- a/tooling/lsp/src/requests/completion/builtins.rs +++ b/tooling/lsp/src/requests/completion/builtins.rs @@ -191,6 +191,7 @@ pub(super) fn keyword_builtin_type(keyword: &Keyword) -> Option<&'static str> { | Keyword::Impl | Keyword::In | Keyword::Let + | Keyword::Loop | Keyword::Match | Keyword::Mod | Keyword::Mut @@ -257,6 +258,7 @@ pub(super) fn keyword_builtin_function(keyword: &Keyword) -> Option ChunkFormatter<'a, 'b> { StatementKind::For(for_loop_statement) => { group.group(self.format_for_loop(for_loop_statement)); } + StatementKind::Loop(block) => { + group.group(self.format_loop(block)); + } StatementKind::Break => { group.text(self.chunk(|formatter| { formatter.write_keyword(Keyword::Break); @@ -277,6 +280,34 @@ impl<'a, 'b> ChunkFormatter<'a, 'b> { group } + fn format_loop(&mut self, block: Expression) -> ChunkGroup { + let mut group = ChunkGroup::new(); + + group.text(self.chunk(|formatter| { + formatter.write_keyword(Keyword::Loop); + })); + + group.space(self); + + let ExpressionKind::Block(block) = block.kind else { + panic!("Expected a block expression for loop body"); + }; + + group.group(self.format_block_expression( + block, true, // force multiple lines + )); + + // If there's a trailing semicolon, remove it + group.text(self.chunk(|formatter| { + formatter.skip_whitespace_if_it_is_not_a_newline(); + if formatter.is_at(Token::Semicolon) { + formatter.bump(); + } + })); + + group + } + fn format_comptime_statement(&mut self, statement: Statement) -> ChunkGroup { let mut group = ChunkGroup::new(); @@ -749,4 +780,27 @@ mod tests { let expected = src; assert_format_with_max_width(src, expected, " let x = 123456;".len()); } + + #[test] + fn format_empty_loop() { + let src = " fn foo() { loop { } } "; + let expected = "fn foo() { + loop {} +} +"; + assert_format(src, expected); + } + + #[test] + fn format_non_empty_loop() { + let src = " fn foo() { loop { 1 ; 2 } } "; + let expected = "fn foo() { + loop { + 1; + 2 + } +} +"; + assert_format(src, expected); + } }