diff --git a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs index aa5a7fedd92..0726b557616 100644 --- a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs @@ -49,11 +49,10 @@ impl FunctionBuilder { ) -> Self { let mut new_function = Function::new(function_name, function_id); new_function.set_runtime(runtime); - let current_block = new_function.entry_block(); Self { + current_block: new_function.entry_block(), current_function: new_function, - current_block, finished_functions: Vec::new(), call_stack: CallStack::new(), } @@ -153,9 +152,10 @@ impl FunctionBuilder { instruction: Instruction, ctrl_typevars: Option>, ) -> InsertInstructionResult { + let block = self.current_block(); self.current_function.dfg.insert_instruction_and_results( instruction, - self.current_block, + block, ctrl_typevars, self.call_stack.clone(), ) @@ -317,8 +317,11 @@ impl FunctionBuilder { } /// Terminates the current block with the given terminator instruction + /// if the current block does not already have a terminator instruction. fn terminate_block_with(&mut self, terminator: TerminatorInstruction) { - self.current_function.dfg.set_block_terminator(self.current_block, terminator); + if self.current_function.dfg[self.current_block].terminator().is_none() { + self.current_function.dfg.set_block_terminator(self.current_block, terminator); + } } /// Terminate the current block with a jmp instruction to jmp to the given diff --git a/compiler/noirc_evaluator/src/ssa/ir/cfg.rs b/compiler/noirc_evaluator/src/ssa/ir/cfg.rs index ebfbf003ec4..5a3f07cd673 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/cfg.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/cfg.rs @@ -95,10 +95,6 @@ impl ControlFlowGraph { ); predecessor_node.successors.insert(to); let successor_node = self.data.entry(to).or_default(); - assert!( - successor_node.predecessors.len() < 2, - "ICE: A cfg node cannot have more than two predecessors" - ); successor_node.predecessors.insert(from); } diff --git a/compiler/noirc_evaluator/src/ssa/opt/inlining.rs b/compiler/noirc_evaluator/src/ssa/opt/inlining.rs index 776f22b2877..aff06af9921 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/inlining.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/inlining.rs @@ -487,6 +487,13 @@ impl<'function> PerFunctionContext<'function> { } TerminatorInstruction::Return { return_values, call_stack } => { let return_values = vecmap(return_values, |value| self.translate_value(*value)); + + // Note that `translate_block` would take us back to the point at which the + // inlining of this source block began. Since additional blocks may have been + // inlined since, we are interested in the block representing the current program + // point, obtained via `current_block`. + let block_id = self.context.builder.current_block(); + if self.inlining_entry { let mut new_call_stack = self.context.call_stack.clone(); new_call_stack.append(call_stack.clone()); @@ -495,11 +502,7 @@ impl<'function> PerFunctionContext<'function> { .set_call_stack(new_call_stack) .terminate_with_return(return_values.clone()); } - // Note that `translate_block` would take us back to the point at which the - // inlining of this source block began. Since additional blocks may have been - // inlined since, we are interested in the block representing the current program - // point, obtained via `current_block`. - let block_id = self.context.builder.current_block(); + Some((block_id, return_values)) } } diff --git a/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs b/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs index 409b99958a9..a9a707ca8ed 100644 --- a/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs +++ b/compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs @@ -39,6 +39,11 @@ pub(super) struct FunctionContext<'a> { pub(super) builder: FunctionBuilder, shared_context: &'a SharedContext, + + /// Contains any loops we're currently in the middle of translating. + /// These are ordered such that an inner loop is at the end of the vector and + /// outer loops are at the beginning. When a loop is finished, it is popped. + loops: Vec, } /// Shared context for all functions during ssa codegen. This is the only @@ -72,6 +77,13 @@ pub(super) struct SharedContext { pub(super) program: Program, } +#[derive(Copy, Clone)] +pub(super) struct Loop { + pub(super) loop_entry: BasicBlockId, + pub(super) loop_index: ValueId, + pub(super) loop_end: BasicBlockId, +} + /// The queue of functions remaining to compile type FunctionQueue = Vec<(ast::FuncId, IrFunctionId)>; @@ -97,7 +109,8 @@ impl<'a> FunctionContext<'a> { .1; let builder = FunctionBuilder::new(function_name, function_id, runtime); - let mut this = Self { definitions: HashMap::default(), builder, shared_context }; + let definitions = HashMap::default(); + let mut this = Self { definitions, builder, shared_context, loops: Vec::new() }; this.add_parameters_to_scope(parameters); this } @@ -1053,6 +1066,24 @@ impl<'a> FunctionContext<'a> { self.builder.decrement_array_reference_count(parameter); } } + + pub(crate) fn enter_loop( + &mut self, + loop_entry: BasicBlockId, + loop_index: ValueId, + loop_end: BasicBlockId, + ) { + self.loops.push(Loop { loop_entry, loop_index, loop_end }); + } + + pub(crate) fn exit_loop(&mut self) { + self.loops.pop(); + } + + pub(crate) fn current_loop(&self) -> Loop { + // The frontend should ensure break/continue are never used outside a loop + *self.loops.last().expect("current_loop: not in a loop!") + } } /// True if the given operator cannot be encoded directly and needs diff --git a/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs b/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs index 3d8ae0bb3eb..5acb266b4c1 100644 --- a/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/ssa_gen/mod.rs @@ -152,6 +152,8 @@ impl<'a> FunctionContext<'a> { } Expression::Assign(assign) => self.codegen_assign(assign), Expression::Semi(semi) => self.codegen_semi(semi), + Expression::Break => Ok(self.codegen_break()), + Expression::Continue => Ok(self.codegen_continue()), } } @@ -477,6 +479,10 @@ impl<'a> FunctionContext<'a> { let index_type = Self::convert_non_tuple_type(&for_expr.index_type); let loop_index = self.builder.add_block_parameter(loop_entry, index_type); + // Remember the blocks and variable used in case there are break/continue instructions + // within the loop which need to jump to them. + self.enter_loop(loop_entry, loop_index, loop_end); + self.builder.set_location(for_expr.start_range_location); let start_index = self.codegen_non_tuple_expression(&for_expr.start_range)?; @@ -507,6 +513,7 @@ impl<'a> FunctionContext<'a> { // Finish by switching back to the end of the loop self.builder.switch_to_block(loop_end); + self.exit_loop(); Ok(Self::unit_value()) } @@ -736,4 +743,19 @@ impl<'a> FunctionContext<'a> { self.codegen_expression(expr)?; Ok(Self::unit_value()) } + + fn codegen_break(&mut self) -> Values { + let loop_end = self.current_loop().loop_end; + self.builder.terminate_with_jmp(loop_end, Vec::new()); + Self::unit_value() + } + + fn codegen_continue(&mut self) -> Values { + let loop_ = self.current_loop(); + + // Must remember to increment i before jumping + let new_loop_index = self.make_offset(loop_.loop_index, 1); + self.builder.terminate_with_jmp(loop_.loop_entry, vec![new_loop_index]); + Self::unit_value() + } } diff --git a/compiler/noirc_frontend/src/ast/statement.rs b/compiler/noirc_frontend/src/ast/statement.rs index 387840b57c4..fb7f520ee71 100644 --- a/compiler/noirc_frontend/src/ast/statement.rs +++ b/compiler/noirc_frontend/src/ast/statement.rs @@ -35,6 +35,8 @@ pub enum StatementKind { Expression(Expression), Assign(AssignStatement), For(ForLoopStatement), + Break, + Continue, // This is an expression with a trailing semi-colon Semi(Expression), // This statement is the result of a recovered parse error. @@ -59,6 +61,8 @@ impl Statement { | StatementKind::Constrain(_) | StatementKind::Assign(_) | StatementKind::Semi(_) + | StatementKind::Break + | StatementKind::Continue | StatementKind::Error => { // To match rust, statements always require a semicolon, even at the end of a block if semi.is_none() { @@ -637,6 +641,8 @@ 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::Break => write!(f, "break"), + StatementKind::Continue => write!(f, "continue"), StatementKind::Semi(semi) => write!(f, "{semi};"), StatementKind::Error => write!(f, "Error"), } diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs index 30a1ba2ee34..3c6c0582292 100644 --- a/compiler/noirc_frontend/src/hir/resolution/errors.rs +++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs @@ -84,6 +84,10 @@ pub enum ResolverError { LowLevelFunctionOutsideOfStdlib { ident: Ident }, #[error("Dependency cycle found, '{item}' recursively depends on itself: {cycle} ")] DependencyCycle { span: Span, item: String, cycle: String }, + #[error("break/continue are only allowed in unconstrained functions")] + JumpInConstrainedFn { is_break: bool, span: Span }, + #[error("break/continue are only allowed within loops")] + JumpOutsideLoop { is_break: bool, span: Span }, } impl ResolverError { @@ -322,6 +326,22 @@ impl From for Diagnostic { span, ) }, + ResolverError::JumpInConstrainedFn { is_break, span } => { + let item = if is_break { "break" } else { "continue" }; + Diagnostic::simple_error( + format!("{item} is only allowed in unconstrained functions"), + "Constrained code must always have a known number of loop iterations".into(), + span, + ) + }, + ResolverError::JumpOutsideLoop { is_break, span } => { + let item = if is_break { "break" } else { "continue" }; + Diagnostic::simple_error( + format!("{item} is only allowed within loops"), + "".into(), + span, + ) + }, } } } diff --git a/compiler/noirc_frontend/src/hir/resolution/resolver.rs b/compiler/noirc_frontend/src/hir/resolution/resolver.rs index c33b83257b0..90716bb958d 100644 --- a/compiler/noirc_frontend/src/hir/resolution/resolver.rs +++ b/compiler/noirc_frontend/src/hir/resolution/resolver.rs @@ -39,7 +39,7 @@ use crate::{ use crate::{ ArrayLiteral, BinaryOpKind, Distinctness, ForRange, FunctionDefinition, FunctionReturnType, Generics, ItemVisibility, LValue, NoirStruct, NoirTypeAlias, Param, Path, PathKind, Pattern, - Shared, StructType, Type, TypeAlias, TypeVariable, TypeVariableKind, UnaryOp, + Shared, Statement, StructType, Type, TypeAlias, TypeVariable, TypeVariableKind, UnaryOp, UnresolvedGenerics, UnresolvedTraitConstraint, UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression, Visibility, ERROR_IDENT, }; @@ -115,6 +115,13 @@ pub struct Resolver<'a> { /// that are captured. We do this in order to create the hidden environment /// parameter for the lambda function. lambda_stack: Vec, + + /// True if we're currently resolving an unconstrained function + in_unconstrained_fn: bool, + + /// How many loops we're currently within. + /// This increases by 1 at the start of a loop, and decreases by 1 when it ends. + nested_loops: u32, } /// ResolverMetas are tagged onto each definition to track how many times they are used @@ -155,6 +162,8 @@ impl<'a> Resolver<'a> { current_item: None, file, in_contract, + in_unconstrained_fn: false, + nested_loops: 0, } } @@ -416,6 +425,11 @@ impl<'a> Resolver<'a> { fn intern_function(&mut self, func: NoirFunction, id: FuncId) -> (HirFunction, FuncMeta) { let func_meta = self.extract_meta(&func, id); + + if func.def.is_unconstrained { + self.in_unconstrained_fn = true; + } + let hir_func = match func.kind { FunctionKind::Builtin | FunctionKind::LowLevel | FunctionKind::Oracle => { HirFunction::empty() @@ -1148,7 +1162,7 @@ impl<'a> Resolver<'a> { }) } - pub fn resolve_stmt(&mut self, stmt: StatementKind) -> HirStatement { + pub fn resolve_stmt(&mut self, stmt: StatementKind, span: Span) -> HirStatement { match stmt { StatementKind::Let(let_stmt) => { let expression = self.resolve_expression(let_stmt.expression); @@ -1188,6 +1202,8 @@ impl<'a> Resolver<'a> { let end_range = self.resolve_expression(end_range); let (identifier, block) = (for_loop.identifier, for_loop.block); + self.nested_loops += 1; + // TODO: For loop variables are currently mutable by default since we haven't // yet implemented syntax for them to be optionally mutable. let (identifier, block) = self.in_new_scope(|this| { @@ -1200,6 +1216,8 @@ impl<'a> Resolver<'a> { (decl, this.resolve_expression(block)) }); + self.nested_loops -= 1; + HirStatement::For(HirForStatement { start_range, end_range, @@ -1210,10 +1228,18 @@ impl<'a> Resolver<'a> { range @ ForRange::Array(_) => { let for_stmt = range.into_for(for_loop.identifier, for_loop.block, for_loop.span); - self.resolve_stmt(for_stmt) + self.resolve_stmt(for_stmt, for_loop.span) } } } + StatementKind::Break => { + self.check_break_continue(true, span); + HirStatement::Break + } + StatementKind::Continue => { + self.check_break_continue(false, span); + HirStatement::Continue + } StatementKind::Error => HirStatement::Error, } } @@ -1260,8 +1286,8 @@ impl<'a> Resolver<'a> { Some(self.resolve_expression(assert_msg_call_expr)) } - pub fn intern_stmt(&mut self, stmt: StatementKind) -> StmtId { - let hir_stmt = self.resolve_stmt(stmt); + pub fn intern_stmt(&mut self, stmt: Statement) -> StmtId { + let hir_stmt = self.resolve_stmt(stmt.kind, stmt.span); self.interner.push_stmt(hir_stmt) } @@ -1909,7 +1935,7 @@ impl<'a> Resolver<'a> { fn resolve_block(&mut self, block_expr: BlockExpression) -> HirExpression { let statements = - self.in_new_scope(|this| vecmap(block_expr.0, |stmt| this.intern_stmt(stmt.kind))); + self.in_new_scope(|this| vecmap(block_expr.0, |stmt| this.intern_stmt(stmt))); HirExpression::Block(HirBlockExpression(statements)) } @@ -2036,6 +2062,15 @@ impl<'a> Resolver<'a> { } HirLiteral::FmtStr(str, fmt_str_idents) } + + fn check_break_continue(&mut self, is_break: bool, span: Span) { + if !self.in_unconstrained_fn { + self.push_err(ResolverError::JumpInConstrainedFn { is_break, span }); + } + if self.nested_loops == 0 { + self.push_err(ResolverError::JumpOutsideLoop { is_break, span }); + } + } } /// Gives an error if a user tries to create a mutable reference diff --git a/compiler/noirc_frontend/src/hir/type_check/stmt.rs b/compiler/noirc_frontend/src/hir/type_check/stmt.rs index 358bea86922..e90da555803 100644 --- a/compiler/noirc_frontend/src/hir/type_check/stmt.rs +++ b/compiler/noirc_frontend/src/hir/type_check/stmt.rs @@ -51,7 +51,7 @@ impl<'interner> TypeChecker<'interner> { HirStatement::Constrain(constrain_stmt) => self.check_constrain_stmt(constrain_stmt), HirStatement::Assign(assign_stmt) => self.check_assign_stmt(assign_stmt, stmt_id), HirStatement::For(for_loop) => self.check_for_loop(for_loop), - HirStatement::Error => (), + HirStatement::Break | HirStatement::Continue | HirStatement::Error => (), } Type::Unit } diff --git a/compiler/noirc_frontend/src/hir_def/stmt.rs b/compiler/noirc_frontend/src/hir_def/stmt.rs index b910be1fdda..4e5f718cf47 100644 --- a/compiler/noirc_frontend/src/hir_def/stmt.rs +++ b/compiler/noirc_frontend/src/hir_def/stmt.rs @@ -14,6 +14,8 @@ pub enum HirStatement { Constrain(HirConstrainStatement), Assign(HirAssignStatement), For(HirForStatement), + Break, + Continue, Expression(ExprId), Semi(ExprId), Error, diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs index 3dc9d05b15e..1e341d34510 100644 --- a/compiler/noirc_frontend/src/lexer/token.rs +++ b/compiler/noirc_frontend/src/lexer/token.rs @@ -644,10 +644,12 @@ pub enum Keyword { Assert, AssertEq, Bool, + Break, CallData, Char, CompTime, Constrain, + Continue, Contract, Crate, Dep, @@ -685,10 +687,12 @@ impl fmt::Display for Keyword { Keyword::Assert => write!(f, "assert"), Keyword::AssertEq => write!(f, "assert_eq"), Keyword::Bool => write!(f, "bool"), + Keyword::Break => write!(f, "break"), Keyword::Char => write!(f, "char"), Keyword::CallData => write!(f, "call_data"), Keyword::CompTime => write!(f, "comptime"), Keyword::Constrain => write!(f, "constrain"), + Keyword::Continue => write!(f, "continue"), Keyword::Contract => write!(f, "contract"), Keyword::Crate => write!(f, "crate"), Keyword::Dep => write!(f, "dep"), @@ -729,10 +733,12 @@ impl Keyword { "assert" => Keyword::Assert, "assert_eq" => Keyword::AssertEq, "bool" => Keyword::Bool, + "break" => Keyword::Break, "call_data" => Keyword::CallData, "char" => Keyword::Char, "comptime" => Keyword::CompTime, "constrain" => Keyword::Constrain, + "continue" => Keyword::Continue, "contract" => Keyword::Contract, "crate" => Keyword::Crate, "dep" => Keyword::Dep, diff --git a/compiler/noirc_frontend/src/monomorphization/ast.rs b/compiler/noirc_frontend/src/monomorphization/ast.rs index 7fcf8e87792..21b77127360 100644 --- a/compiler/noirc_frontend/src/monomorphization/ast.rs +++ b/compiler/noirc_frontend/src/monomorphization/ast.rs @@ -38,6 +38,8 @@ pub enum Expression { Constrain(Box, Location, Option>), Assign(Assign), Semi(Box), + Break, + Continue, } /// A definition is either a local (variable), function, or is a built-in diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs index 4938d33aff9..a99a4e61d4d 100644 --- a/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -594,6 +594,8 @@ impl<'interner> Monomorphizer<'interner> { HirStatement::Semi(expr) => { self.expr(expr).map(|expr| ast::Expression::Semi(Box::new(expr))) } + HirStatement::Break => Ok(ast::Expression::Break), + HirStatement::Continue => Ok(ast::Expression::Continue), HirStatement::Error => unreachable!(), } } diff --git a/compiler/noirc_frontend/src/monomorphization/printer.rs b/compiler/noirc_frontend/src/monomorphization/printer.rs index 7aec2193494..c3e34890ce0 100644 --- a/compiler/noirc_frontend/src/monomorphization/printer.rs +++ b/compiler/noirc_frontend/src/monomorphization/printer.rs @@ -73,6 +73,8 @@ impl AstPrinter { self.print_expr(expr, f)?; write!(f, ";") } + Expression::Break => write!(f, "break"), + Expression::Continue => write!(f, "continue"), } } diff --git a/compiler/noirc_frontend/src/parser/parser.rs b/compiler/noirc_frontend/src/parser/parser.rs index 383a1ffafc9..b2d9d7e6802 100644 --- a/compiler/noirc_frontend/src/parser/parser.rs +++ b/compiler/noirc_frontend/src/parser/parser.rs @@ -421,6 +421,8 @@ where declaration(expr_parser.clone()), assignment(expr_parser.clone()), for_loop(expr_no_constructors, statement), + break_statement(), + continue_statement(), return_statement(expr_parser.clone()), expr_parser.map(StatementKind::Expression), )) @@ -431,6 +433,14 @@ fn fresh_statement() -> impl NoirParser { statement(expression(), expression_no_constructors(expression())) } +fn break_statement() -> impl NoirParser { + keyword(Keyword::Break).to(StatementKind::Break) +} + +fn continue_statement() -> impl NoirParser { + keyword(Keyword::Continue).to(StatementKind::Continue) +} + fn declaration<'a, P>(expr_parser: P) -> impl NoirParser + 'a where P: ExprParser + 'a, diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index b8ed6fb73d2..98dbc42adcd 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -778,6 +778,8 @@ mod test { HirStatement::Semi(semi_expr) => semi_expr, HirStatement::For(for_loop) => for_loop.block, HirStatement::Error => panic!("Invalid HirStatement!"), + HirStatement::Break => panic!("Unexpected break"), + HirStatement::Continue => panic!("Unexpected continue"), }; let expr = interner.expression(&expr_id); @@ -1226,6 +1228,34 @@ fn lambda$f1(mut env$l1: (Field)) -> Field { assert_eq!(get_program_errors(src).len(), 0); } + #[test] + fn break_and_continue_in_constrained_fn() { + let src = r#" + fn main() { + for i in 0 .. 10 { + if i == 2 { + continue; + } + if i == 5 { + break; + } + } + } + "#; + assert_eq!(get_program_errors(src).len(), 2); + } + + #[test] + fn break_and_continue_outside_loop() { + let src = r#" + unconstrained fn main() { + continue; + break; + } + "#; + assert_eq!(get_program_errors(src).len(), 2); + } + // Regression for #4545 #[test] fn type_aliases_in_main() { diff --git a/docs/docs/noir/concepts/control_flow.md b/docs/docs/noir/concepts/control_flow.md index 4ce65236db3..045d3c3a5f5 100644 --- a/docs/docs/noir/concepts/control_flow.md +++ b/docs/docs/noir/concepts/control_flow.md @@ -7,21 +7,6 @@ keywords: [Noir programming language, loops, for loop, if-else statements, Rust sidebar_position: 2 --- -## Loops - -Noir has one kind of loop: the `for` loop. `for` loops allow you to repeat a block of code multiple -times. - -The following block of code between the braces is run 10 times. - -```rust -for i in 0..10 { - // do something -}; -``` - -The index for loops is of type `u64`. - ## If Expressions Noir supports `if-else` statements. The syntax is most similar to Rust's where it is not required @@ -43,3 +28,50 @@ if a == 0 { } assert(x == 2); ``` + +## Loops + +Noir has one kind of loop: the `for` loop. `for` loops allow you to repeat a block of code multiple +times. + +The following block of code between the braces is run 10 times. + +```rust +for i in 0..10 { + // do something +} +``` + +The index for loops is of type `u64`. + +### Break and Continue + +In unconstrained code, `break` and `continue` are also allowed in `for` loops. These are only allowed +in unconstrained code since normal constrained code requires that Noir knows exactly how many iterations +a loop may have. `break` and `continue` can be used like so: + +```rust +for i in 0 .. 10 { + println("Iteration start") + + if i == 2 { + continue; + } + + if i == 5 { + break; + } + + println(i); +} +println("Loop end") +``` + +When used, `break` will end the current loop early and jump to the statement after the for loop. In the example +above, the `break` will stop the loop and jump to the `println("Loop end")`. + +`continue` will stop the current iteration of the loop, and jump to the start of the next iteration. In the example +above, `continue` will jump to `println("Iteration start")` when used. Note that the loop continues as normal after this. +The iteration variable `i` is still increased by one as normal when `continue` is used. + +`break` and `continue` cannot currently be used to jump out of more than a single loop at a time. diff --git a/docs/docs/noir/concepts/unconstrained.md b/docs/docs/noir/concepts/unconstrained.md index 89d12c1c971..b8e71fe65f0 100644 --- a/docs/docs/noir/concepts/unconstrained.md +++ b/docs/docs/noir/concepts/unconstrained.md @@ -93,3 +93,7 @@ Backend circuit size: 2902 This ends up taking off another ~250 gates from our circuit! We've ended up with more ACIR opcodes than before but they're easier for the backend to prove (resulting in fewer gates). Generally we want to use brillig whenever there's something that's easy to verify but hard to compute within the circuit. For example, if you wanted to calculate a square root of a number it'll be a much better idea to calculate this in brillig and then assert that if you square the result you get back your number. + +## Break and Continue + +In addition to loops over runtime bounds, `break` and `continue` are also available in unconstrained code. See [break and continue](../concepts/control_flow/#break-and-continue) diff --git a/noir_stdlib/src/collections/map.nr b/noir_stdlib/src/collections/map.nr index 2d76acf1f3a..5f8cc6dab62 100644 --- a/noir_stdlib/src/collections/map.nr +++ b/noir_stdlib/src/collections/map.nr @@ -288,10 +288,10 @@ impl HashMap { let mut result = Option::none(); let hash = self.hash(key); - let mut break = false; + let mut should_break = false; for attempt in 0..N { - if !break { + if !should_break { let index = self.quadratic_probe(hash, attempt as u64); let slot = self._table[index]; @@ -300,7 +300,7 @@ impl HashMap { let (current_key, value) = slot.key_value_unchecked(); if current_key == key { result = Option::some(value); - break = true; + should_break = true; } } } @@ -324,10 +324,10 @@ impl HashMap { self.assert_load_factor(); let hash = self.hash(key); - let mut break = false; + let mut should_break = false; for attempt in 0..N { - if !break { + if !should_break { let index = self.quadratic_probe(hash, attempt as u64); let mut slot = self._table[index]; let mut insert = false; @@ -346,7 +346,7 @@ impl HashMap { if insert { slot.set(key, value); self._table[index] = slot; - break = true; + should_break = true; } } } @@ -364,10 +364,10 @@ impl HashMap { H: Hasher { // docs:end:remove let hash = self.hash(key); - let mut break = false; + let mut should_break = false; for attempt in 0..N { - if !break { + if !should_break { let index = self.quadratic_probe(hash, attempt as u64); let mut slot = self._table[index]; @@ -378,7 +378,7 @@ impl HashMap { slot.mark_deleted(); self._table[index] = slot; self._len -= 1; - break = true; + should_break = true; } } } diff --git a/test_programs/execution_success/break_and_continue/Nargo.toml b/test_programs/execution_success/break_and_continue/Nargo.toml new file mode 100644 index 00000000000..483602478ba --- /dev/null +++ b/test_programs/execution_success/break_and_continue/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "break_and_continue" +type = "bin" +authors = [""] +compiler_version = ">=0.24.0" + +[dependencies] diff --git a/test_programs/execution_success/break_and_continue/src/main.nr b/test_programs/execution_success/break_and_continue/src/main.nr new file mode 100644 index 00000000000..67dce03ac64 --- /dev/null +++ b/test_programs/execution_success/break_and_continue/src/main.nr @@ -0,0 +1,15 @@ +unconstrained fn main() { + let mut count = 0; + + for i in 0..10 { + if i == 2 { + continue; + } + if i == 5 { + break; + } + count += 1; + } + + assert(count == 4); +} diff --git a/tooling/nargo_fmt/src/visitor/stmt.rs b/tooling/nargo_fmt/src/visitor/stmt.rs index 44c5dad6b5d..ee8cc990e0e 100644 --- a/tooling/nargo_fmt/src/visitor/stmt.rs +++ b/tooling/nargo_fmt/src/visitor/stmt.rs @@ -95,6 +95,8 @@ impl super::FmtVisitor<'_> { self.push_rewrite(self.slice(span).to_string(), span); } StatementKind::Error => unreachable!(), + StatementKind::Break => self.push_rewrite("break;".into(), span), + StatementKind::Continue => self.push_rewrite("continue;".into(), span), } self.last_position = span.end();