diff --git a/compiler/noirc_frontend/src/ast/statement.rs b/compiler/noirc_frontend/src/ast/statement.rs index 2d835482082..23832277226 100644 --- a/compiler/noirc_frontend/src/ast/statement.rs +++ b/compiler/noirc_frontend/src/ast/statement.rs @@ -7,8 +7,9 @@ use iter_extended::vecmap; use noirc_errors::{Span, Spanned}; use super::{ - BlockExpression, ConstructorExpression, Expression, ExpressionKind, GenericTypeArgs, - IndexExpression, ItemVisibility, MemberAccessExpression, MethodCallExpression, UnresolvedType, + BinaryOpKind, BlockExpression, ConstructorExpression, Expression, ExpressionKind, + GenericTypeArgs, IndexExpression, InfixExpression, ItemVisibility, MemberAccessExpression, + MethodCallExpression, UnresolvedType, }; use crate::ast::UnresolvedTypeData; use crate::elaborator::types::SELF_TYPE_NAME; @@ -770,13 +771,57 @@ impl LValue { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ForBounds { + pub start: Expression, + pub end: Expression, + pub inclusive: bool, +} + +impl ForBounds { + /// Create a half-open range bounded inclusively below and exclusively above (`start..end`), + /// desugaring `start..=end` into `start..end+1` if necessary. + /// + /// Returns the `start` and `end` expressions. + pub(crate) fn into_half_open(self) -> (Expression, Expression) { + let end = if self.inclusive { + let end_span = self.end.span; + let end = ExpressionKind::Infix(Box::new(InfixExpression { + lhs: self.end, + operator: Spanned::from(end_span, BinaryOpKind::Add), + rhs: Expression::new(ExpressionKind::integer(FieldElement::from(1u32)), end_span), + })); + Expression::new(end, end_span) + } else { + self.end + }; + + (self.start, end) + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub enum ForRange { - Range(/*start:*/ Expression, /*end:*/ Expression), + Range(ForBounds), Array(Expression), } impl ForRange { + /// Create a half-open range, bounded inclusively below and exclusively above. + pub fn range(start: Expression, end: Expression) -> Self { + Self::Range(ForBounds { start, end, inclusive: false }) + } + + /// Create a range bounded inclusively below and above. + pub fn range_inclusive(start: Expression, end: Expression) -> Self { + Self::Range(ForBounds { start, end, inclusive: true }) + } + + /// Create a range over some array. + pub fn array(value: Expression) -> Self { + Self::Array(value) + } + /// Create a 'for' expression taking care of desugaring a 'for e in array' loop /// into the following if needed: /// @@ -879,7 +924,7 @@ impl ForRange { let for_loop = Statement { kind: StatementKind::For(ForLoopStatement { identifier: fresh_identifier, - range: ForRange::Range(start_range, end_range), + range: ForRange::range(start_range, end_range), block: new_block, span: for_loop_span, }), @@ -1009,7 +1054,14 @@ impl Display for Pattern { impl Display for ForLoopStatement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let range = match &self.range { - ForRange::Range(start, end) => format!("{start}..{end}"), + ForRange::Range(bounds) => { + format!( + "{}{}{}", + bounds.start, + if bounds.inclusive { "..=" } else { ".." }, + bounds.end + ) + } ForRange::Array(expr) => expr.to_string(), }; diff --git a/compiler/noirc_frontend/src/ast/visitor.rs b/compiler/noirc_frontend/src/ast/visitor.rs index ed4d17cadd8..6e3e830e3f9 100644 --- a/compiler/noirc_frontend/src/ast/visitor.rs +++ b/compiler/noirc_frontend/src/ast/visitor.rs @@ -21,9 +21,9 @@ use crate::{ }; use super::{ - FunctionReturnType, GenericTypeArgs, IntegerBitSize, ItemVisibility, Pattern, Signedness, - TraitImplItemKind, TypePath, UnresolvedGenerics, UnresolvedTraitConstraint, UnresolvedType, - UnresolvedTypeData, UnresolvedTypeExpression, + ForBounds, FunctionReturnType, GenericTypeArgs, IntegerBitSize, ItemVisibility, Pattern, + Signedness, TraitImplItemKind, TypePath, UnresolvedGenerics, UnresolvedTraitConstraint, + UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -1192,7 +1192,7 @@ impl ForRange { pub fn accept_children(&self, visitor: &mut impl Visitor) { match self { - ForRange::Range(start, end) => { + ForRange::Range(ForBounds { start, end, inclusive: _ }) => { start.accept(visitor); end.accept(visitor); } diff --git a/compiler/noirc_frontend/src/elaborator/statements.rs b/compiler/noirc_frontend/src/elaborator/statements.rs index 204a7f9cd75..6d5d0c9b467 100644 --- a/compiler/noirc_frontend/src/elaborator/statements.rs +++ b/compiler/noirc_frontend/src/elaborator/statements.rs @@ -196,7 +196,7 @@ impl<'context> Elaborator<'context> { pub(super) fn elaborate_for(&mut self, for_loop: ForLoopStatement) -> (HirStatement, Type) { let (start, end) = match for_loop.range { - ForRange::Range(start, end) => (start, end), + ForRange::Range(bounds) => bounds.into_half_open(), ForRange::Array(_) => { let for_stmt = for_loop.range.into_for(for_loop.identifier, for_loop.block, for_loop.span); diff --git a/compiler/noirc_frontend/src/hir/comptime/display.rs b/compiler/noirc_frontend/src/hir/comptime/display.rs index 3f2ecb395d0..fa45c41f8ec 100644 --- a/compiler/noirc_frontend/src/hir/comptime/display.rs +++ b/compiler/noirc_frontend/src/hir/comptime/display.rs @@ -7,7 +7,7 @@ use crate::{ ast::{ ArrayLiteral, AsTraitPath, AssignStatement, BlockExpression, CallExpression, CastExpression, ConstrainStatement, ConstructorExpression, Expression, ExpressionKind, - ForLoopStatement, ForRange, GenericTypeArgs, IfExpression, IndexExpression, + ForBounds, ForLoopStatement, ForRange, GenericTypeArgs, IfExpression, IndexExpression, InfixExpression, LValue, Lambda, LetStatement, Literal, MemberAccessExpression, MethodCallExpression, Pattern, PrefixExpression, Statement, StatementKind, UnresolvedType, UnresolvedTypeData, @@ -267,6 +267,7 @@ impl<'interner> TokenPrettyPrinter<'interner> { | Token::Dot | Token::DoubleColon | Token::DoubleDot + | Token::DoubleDotEqual | Token::Caret | Token::Pound | Token::Pipe @@ -713,10 +714,13 @@ fn remove_interned_in_statement_kind( }), StatementKind::For(for_loop) => StatementKind::For(ForLoopStatement { range: match for_loop.range { - ForRange::Range(from, to) => ForRange::Range( - remove_interned_in_expression(interner, from), - remove_interned_in_expression(interner, to), - ), + ForRange::Range(ForBounds { start, end, inclusive }) => { + ForRange::Range(ForBounds { + start: remove_interned_in_expression(interner, start), + end: remove_interned_in_expression(interner, end), + inclusive, + }) + } ForRange::Array(expr) => { ForRange::Array(remove_interned_in_expression(interner, expr)) } diff --git a/compiler/noirc_frontend/src/hir/comptime/hir_to_display_ast.rs b/compiler/noirc_frontend/src/hir/comptime/hir_to_display_ast.rs index 63c32bc7e5f..3542c724b30 100644 --- a/compiler/noirc_frontend/src/hir/comptime/hir_to_display_ast.rs +++ b/compiler/noirc_frontend/src/hir/comptime/hir_to_display_ast.rs @@ -52,7 +52,7 @@ impl HirStatement { }), HirStatement::For(for_stmt) => StatementKind::For(ForLoopStatement { identifier: for_stmt.identifier.to_display_ast(interner), - range: ForRange::Range( + range: ForRange::range( for_stmt.start_range.to_display_ast(interner), for_stmt.end_range.to_display_ast(interner), ), diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs index 48827c087f9..0b028217c46 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs @@ -1514,7 +1514,8 @@ fn expr_as_for_range( ) -> IResult { expr_as(interner, arguments, return_type, location, |expr| { if let ExprValue::Statement(StatementKind::For(for_statement)) = expr { - if let ForRange::Range(from, to) = for_statement.range { + if let ForRange::Range(bounds) = for_statement.range { + let (from, to) = bounds.into_half_open(); let identifier = Value::Quoted(Rc::new(vec![Token::Ident(for_statement.identifier.0.contents)])); let from = Value::expression(from.kind); diff --git a/compiler/noirc_frontend/src/hir/comptime/tests.rs b/compiler/noirc_frontend/src/hir/comptime/tests.rs index 5b03b27e0b2..16cd9e49a71 100644 --- a/compiler/noirc_frontend/src/hir/comptime/tests.rs +++ b/compiler/noirc_frontend/src/hir/comptime/tests.rs @@ -160,6 +160,19 @@ fn for_loop() { assert_eq!(result, Value::U8(15)); } +#[test] +fn for_loop_inclusive() { + let program = "comptime fn main() -> pub u8 { + let mut x = 0; + for i in 0 ..= 6 { + x += i; + } + x + }"; + let result = interpret(program); + assert_eq!(result, Value::U8(21)); +} + #[test] fn for_loop_u16() { let program = "comptime fn main() -> pub u16 { diff --git a/compiler/noirc_frontend/src/lexer/lexer.rs b/compiler/noirc_frontend/src/lexer/lexer.rs index 95eb41fd6d0..069487acb46 100644 --- a/compiler/noirc_frontend/src/lexer/lexer.rs +++ b/compiler/noirc_frontend/src/lexer/lexer.rs @@ -86,6 +86,11 @@ impl<'a> Lexer<'a> { self.peek_char() == Some(ch) } + /// Peeks at the character two positions ahead and returns true if it is equal to the char argument + fn peek2_char_is(&mut self, ch: char) -> bool { + self.peek2_char() == Some(ch) + } + fn ampersand(&mut self) -> SpannedTokenResult { if self.peek_char_is('&') { // When we issue this error the first '&' will already be consumed @@ -152,6 +157,7 @@ impl<'a> Lexer<'a> { Ok(token.into_single_span(self.position)) } + /// If `single` is followed by `character` then extend it as `double`. fn single_double_peek_token( &mut self, character: char, @@ -169,13 +175,23 @@ impl<'a> Lexer<'a> { } } - /// Given that some tokens can contain two characters, such as <= , !=, >= - /// Glue will take the first character of the token and check if it can be glued onto the next character - /// forming a double token + /// Given that some tokens can contain two characters, such as <= , !=, >=, or even three like ..= + /// Glue will take the first character of the token and check if it can be glued onto the next character(s) + /// forming a double or triple token + /// + /// Returns an error if called with a token which cannot be extended with anything. fn glue(&mut self, prev_token: Token) -> SpannedTokenResult { - let spanned_prev_token = prev_token.clone().into_single_span(self.position); match prev_token { - Token::Dot => self.single_double_peek_token('.', prev_token, Token::DoubleDot), + Token::Dot => { + if self.peek_char_is('.') && self.peek2_char_is('=') { + let start = self.position; + self.next_char(); + self.next_char(); + Ok(Token::DoubleDotEqual.into_span(start, start + 2)) + } else { + self.single_double_peek_token('.', prev_token, Token::DoubleDot) + } + } Token::Less => { let start = self.position; if self.peek_char_is('=') { @@ -214,7 +230,7 @@ impl<'a> Lexer<'a> { return self.parse_block_comment(start); } - Ok(spanned_prev_token) + Ok(prev_token.into_single_span(start)) } _ => Err(LexerErrorKind::NotADoubleChar { span: Span::single_char(self.position), @@ -703,8 +719,8 @@ mod tests { use crate::token::{CustomAttribute, FunctionAttribute, SecondaryAttribute, TestScope}; #[test] - fn test_single_double_char() { - let input = "! != + ( ) { } [ ] | , ; : :: < <= > >= & - -> . .. % / * = == << >>"; + fn test_single_multi_char() { + let input = "! != + ( ) { } [ ] | , ; : :: < <= > >= & - -> . .. ..= % / * = == << >>"; let expected = vec![ Token::Bang, @@ -730,6 +746,7 @@ mod tests { Token::Arrow, Token::Dot, Token::DoubleDot, + Token::DoubleDotEqual, Token::Percent, Token::Slash, Token::Star, diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs index 97faea4b445..626c46d392f 100644 --- a/compiler/noirc_frontend/src/lexer/token.rs +++ b/compiler/noirc_frontend/src/lexer/token.rs @@ -73,6 +73,8 @@ pub enum BorrowedToken<'input> { Dot, /// .. DoubleDot, + /// ..= + DoubleDotEqual, /// ( LeftParen, /// ) @@ -190,6 +192,8 @@ pub enum Token { Dot, /// .. DoubleDot, + /// ..= + DoubleDotEqual, /// ( LeftParen, /// ) @@ -279,6 +283,7 @@ pub fn token_to_borrowed_token(token: &Token) -> BorrowedToken<'_> { Token::ShiftRight => BorrowedToken::ShiftRight, Token::Dot => BorrowedToken::Dot, Token::DoubleDot => BorrowedToken::DoubleDot, + Token::DoubleDotEqual => BorrowedToken::DoubleDotEqual, Token::LeftParen => BorrowedToken::LeftParen, Token::RightParen => BorrowedToken::RightParen, Token::LeftBrace => BorrowedToken::LeftBrace, @@ -409,6 +414,7 @@ impl fmt::Display for Token { Token::ShiftRight => write!(f, ">>"), Token::Dot => write!(f, "."), Token::DoubleDot => write!(f, ".."), + Token::DoubleDotEqual => write!(f, "..="), Token::LeftParen => write!(f, "("), Token::RightParen => write!(f, ")"), Token::LeftBrace => write!(f, "{{"), diff --git a/compiler/noirc_frontend/src/parser/parser.rs b/compiler/noirc_frontend/src/parser/parser.rs index df656dc5a7d..60128c6cafe 100644 --- a/compiler/noirc_frontend/src/parser/parser.rs +++ b/compiler/noirc_frontend/src/parser/parser.rs @@ -1076,9 +1076,15 @@ where { expr_no_constructors .clone() - .then_ignore(just(Token::DoubleDot)) + .then(just(Token::DoubleDot).or(just(Token::DoubleDotEqual))) .then(expr_no_constructors.clone()) - .map(|(start, end)| ForRange::Range(start, end)) + .map(|((start, dots), end)| { + if dots == Token::DoubleDotEqual { + ForRange::range_inclusive(start, end) + } else { + ForRange::range(start, end) + } + }) .or(expr_no_constructors.map(ForRange::Array)) } @@ -1465,15 +1471,21 @@ mod test { fn parse_for_loop() { parse_all( for_loop(expression_no_constructors(expression()), fresh_statement()), - vec!["for i in x+y..z {}", "for i in 0..100 { foo; bar }"], + vec![ + "for i in x+y..z {}", + "for i in 0..100 { foo; bar }", + "for i in 90..=100 { foo; bar }", + ], ); parse_all_failing( for_loop(expression_no_constructors(expression()), fresh_statement()), vec![ "for 1 in x+y..z {}", // Cannot have a literal as the loop identifier - "for i in 0...100 {}", // Only '..' is supported, there are no inclusive ranges yet - "for i in 0..=100 {}", // Only '..' is supported, there are no inclusive ranges yet + "for i in 0...100 {}", // Only '..' is supported + "for i in .. {}", // Range needs needs bounds + "for i in ..=10 {}", // Range needs lower bound + "for i in 0.. {}", // Range needs upper bound ], ); } diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index f7ef9955550..57e94e26c80 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -1071,6 +1071,18 @@ fn resolve_for_expr() { assert_no_errors(src); } +#[test] +fn resolve_for_expr_incl() { + let src = r#" + fn main(x : u64) { + for i in 1..=20 { + let _z = x + i; + }; + } + "#; + assert_no_errors(src); +} + #[test] fn resolve_call_expr() { let src = r#" diff --git a/docs/docs/noir/concepts/control_flow.md b/docs/docs/noir/concepts/control_flow.md index 045d3c3a5f5..b365bb22728 100644 --- a/docs/docs/noir/concepts/control_flow.md +++ b/docs/docs/noir/concepts/control_flow.md @@ -42,6 +42,8 @@ for i in 0..10 { } ``` +Alternatively, `start..=end` can be used for a range that is inclusive on both ends. + The index for loops is of type `u64`. ### Break and Continue diff --git a/test_programs/execution_success/brillig_loop/src/main.nr b/test_programs/execution_success/brillig_loop/src/main.nr index 770660bb2a1..9de8c66b051 100644 --- a/test_programs/execution_success/brillig_loop/src/main.nr +++ b/test_programs/execution_success/brillig_loop/src/main.nr @@ -4,6 +4,7 @@ fn main(sum: u32) { unsafe { assert(loop(4) == sum); + assert(loop_incl(3) == sum); assert(plain_loop() == sum); } } @@ -16,6 +17,14 @@ unconstrained fn loop(x: u32) -> u32 { sum } +unconstrained fn loop_incl(x: u32) -> u32 { + let mut sum = 0; + for i in 0..=x { + sum = sum + i; + } + sum +} + unconstrained fn plain_loop() -> u32 { let mut sum = 0; for i in 0..4 { diff --git a/test_programs/execution_success/loop/Nargo.toml b/test_programs/execution_success/loop/Nargo.toml new file mode 100644 index 00000000000..66c72338363 --- /dev/null +++ b/test_programs/execution_success/loop/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "loop" +type = "bin" +authors = [""] + +[dependencies] diff --git a/test_programs/execution_success/loop/Prover.toml b/test_programs/execution_success/loop/Prover.toml new file mode 100644 index 00000000000..0f44bf96f44 --- /dev/null +++ b/test_programs/execution_success/loop/Prover.toml @@ -0,0 +1 @@ +six_as_u32 = "6" diff --git a/test_programs/execution_success/loop/src/main.nr b/test_programs/execution_success/loop/src/main.nr new file mode 100644 index 00000000000..8365cf6f801 --- /dev/null +++ b/test_programs/execution_success/loop/src/main.nr @@ -0,0 +1,23 @@ +// Tests a very simple program. +// +// The features being tested is basic looping. +fn main(six_as_u32: u32) { + assert_eq(loop(4), six_as_u32); + assert_eq(loop_incl(3), six_as_u32); +} + +fn loop(x: u32) -> u32 { + let mut sum = 0; + for i in 0..x { + sum = sum + i; + } + sum +} + +fn loop_incl(x: u32) -> u32 { + let mut sum = 0; + for i in 0..=x { + sum = sum + i; + } + sum +} diff --git a/tooling/nargo_fmt/build.rs b/tooling/nargo_fmt/build.rs index 7d5f07c43bf..4051597c088 100644 --- a/tooling/nargo_fmt/build.rs +++ b/tooling/nargo_fmt/build.rs @@ -12,7 +12,7 @@ fn main() { // is the root of the repository and append the crate path let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") { Ok(dir) => PathBuf::from(dir), - Err(_) => std::env::current_dir().unwrap().join("crates").join("nargo_cli"), + Err(_) => std::env::current_dir().unwrap().join("tooling").join("nargo_fmt"), }; let test_dir = manifest_dir.join("tests"); diff --git a/tooling/nargo_fmt/src/visitor/stmt.rs b/tooling/nargo_fmt/src/visitor/stmt.rs index 7298be641d9..7696c4c5fd4 100644 --- a/tooling/nargo_fmt/src/visitor/stmt.rs +++ b/tooling/nargo_fmt/src/visitor/stmt.rs @@ -2,7 +2,9 @@ use std::iter::zip; use noirc_errors::Span; -use noirc_frontend::ast::{ConstrainKind, ConstrainStatement, ForRange, Statement, StatementKind}; +use noirc_frontend::ast::{ + ConstrainKind, ConstrainStatement, ForBounds, ForRange, Statement, StatementKind, +}; use crate::{rewrite, visitor::expr::wrap_exprs}; @@ -67,11 +69,13 @@ impl super::FmtVisitor<'_> { StatementKind::For(for_stmt) => { let identifier = self.slice(for_stmt.identifier.span()); let range = match for_stmt.range { - ForRange::Range(start, end) => format!( - "{}..{}", + ForRange::Range(ForBounds { start, end, inclusive }) => format!( + "{}{}{}", rewrite::sub_expr(self, self.shape(), start), + if inclusive { "..=" } else { ".." }, rewrite::sub_expr(self, self.shape(), end) ), + ForRange::Array(array) => rewrite::sub_expr(self, self.shape(), array), }; let block = rewrite::sub_expr(self, self.shape(), for_stmt.block); diff --git a/tooling/nargo_fmt/tests/expected/for.nr b/tooling/nargo_fmt/tests/expected/for.nr index 98dff672bef..748c1fe008d 100644 --- a/tooling/nargo_fmt/tests/expected/for.nr +++ b/tooling/nargo_fmt/tests/expected/for.nr @@ -10,6 +10,10 @@ fn for_stmt() { b *= b; } + for k in 0..=C1 { + d *= d; + } + z *= if b == 1 { 1 } else { c }; c *= c; diff --git a/tooling/nargo_fmt/tests/input/for.nr b/tooling/nargo_fmt/tests/input/for.nr index 99b796df820..bf0a497fe65 100644 --- a/tooling/nargo_fmt/tests/input/for.nr +++ b/tooling/nargo_fmt/tests/input/for.nr @@ -12,6 +12,10 @@ fn for_stmt() { b *= b; } + + for k in 0 ..= C1 + { + d *= d; } z *= if b == 1 { 1 } else { c };