Просмотр исходного кода

Support for `while` statements. (#3258)

Basic support for type-checking and lowering of `while` statements, as
well as `break` and `continue`.
Richard Smith 2 лет назад
Родитель
Сommit
6e9d83e746

+ 13 - 0
toolchain/check/context.h

@@ -21,6 +21,12 @@ namespace Carbon::Check {
 // Context and shared functionality for semantics handlers.
 class Context {
  public:
+  // A scope in which `break` and `continue` can be used.
+  struct BreakContinueScope {
+    SemIR::NodeBlockId break_target;
+    SemIR::NodeBlockId continue_target;
+  };
+
   // Stores references for work.
   explicit Context(const Lex::TokenizedBuffer& tokens,
                    DiagnosticEmitter<Parse::Node>& emitter,
@@ -186,6 +192,10 @@ class Context {
     return return_scope_stack_;
   }
 
+  auto break_continue_stack() -> llvm::SmallVector<BreakContinueScope>& {
+    return break_continue_stack_;
+  }
+
   auto declaration_name_stack() -> DeclarationNameStack& {
     return declaration_name_stack_;
   }
@@ -273,6 +283,9 @@ class Context {
   // this will be a FunctionDeclaration.
   llvm::SmallVector<SemIR::NodeId> return_scope_stack_;
 
+  // A stack of `break` and `continue` targets.
+  llvm::SmallVector<BreakContinueScope> break_continue_stack_;
+
   // A stack for scope context.
   llvm::SmallVector<ScopeStackEntry> scope_stack_;
 

+ 79 - 12
toolchain/check/handle_loop_statement.cpp

@@ -3,25 +3,50 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 
 namespace Carbon::Check {
 
-auto HandleBreakStatement(Context& context, Parse::Node parse_node) -> bool {
-  return context.TODO(parse_node, "HandleBreakStatement");
+auto HandleBreakStatement(Context& /*context*/, Parse::Node /*parse_node*/)
+    -> bool {
+  return true;
 }
 
 auto HandleBreakStatementStart(Context& context, Parse::Node parse_node)
     -> bool {
-  return context.TODO(parse_node, "HandleBreakStatementStart");
+  auto& stack = context.break_continue_stack();
+  if (stack.empty()) {
+    CARBON_DIAGNOSTIC(BreakOutsideLoop, Error,
+                      "`break` can only be used in a loop.");
+    context.emitter().Emit(parse_node, BreakOutsideLoop);
+  } else {
+    context.AddNode(SemIR::Branch(parse_node, stack.back().break_target));
+  }
+
+  context.node_block_stack().Pop();
+  context.node_block_stack().PushUnreachable();
+  return true;
 }
 
-auto HandleContinueStatement(Context& context, Parse::Node parse_node) -> bool {
-  return context.TODO(parse_node, "HandleContinueStatement");
+auto HandleContinueStatement(Context& /*context*/, Parse::Node /*parse_node*/)
+    -> bool {
+  return true;
 }
 
 auto HandleContinueStatementStart(Context& context, Parse::Node parse_node)
     -> bool {
-  return context.TODO(parse_node, "HandleContinueStatementStart");
+  auto& stack = context.break_continue_stack();
+  if (stack.empty()) {
+    CARBON_DIAGNOSTIC(ContinueOutsideLoop, Error,
+                      "`continue` can only be used in a loop.");
+    context.emitter().Emit(parse_node, ContinueOutsideLoop);
+  } else {
+    context.AddNode(SemIR::Branch(parse_node, stack.back().continue_target));
+  }
+
+  context.node_block_stack().Pop();
+  context.node_block_stack().PushUnreachable();
+  return true;
 }
 
 auto HandleForHeader(Context& context, Parse::Node parse_node) -> bool {
@@ -40,17 +65,59 @@ auto HandleForStatement(Context& context, Parse::Node parse_node) -> bool {
   return context.TODO(parse_node, "HandleForStatement");
 }
 
-auto HandleWhileCondition(Context& context, Parse::Node parse_node) -> bool {
-  return context.TODO(parse_node, "HandleWhileCondition");
-}
-
 auto HandleWhileConditionStart(Context& context, Parse::Node parse_node)
     -> bool {
-  return context.TODO(parse_node, "HandleWhileConditionStart");
+  // Branch to the loop header block. Note that we create a new block here even
+  // if the current block is empty; this ensures that the loop always has a
+  // preheader block.
+  auto loop_header_id = context.AddDominatedBlockAndBranch(parse_node);
+  context.node_block_stack().Pop();
+
+  // Start emitting the loop header block.
+  context.node_block_stack().Push(loop_header_id);
+  context.AddCurrentCodeBlockToFunction();
+
+  context.node_stack().Push(parse_node, loop_header_id);
+  return true;
+}
+
+auto HandleWhileCondition(Context& context, Parse::Node parse_node) -> bool {
+  auto cond_value_id = context.node_stack().PopExpression();
+  auto loop_header_id =
+      context.node_stack().Peek<Parse::NodeKind::WhileConditionStart>();
+  cond_value_id = ConvertToBoolValue(context, parse_node, cond_value_id);
+
+  // Branch to either the loop body or the loop exit block.
+  auto loop_body_id =
+      context.AddDominatedBlockAndBranchIf(parse_node, cond_value_id);
+  auto loop_exit_id = context.AddDominatedBlockAndBranch(parse_node);
+  context.node_block_stack().Pop();
+
+  // Start emitting the loop body.
+  context.node_block_stack().Push(loop_body_id);
+  context.AddCurrentCodeBlockToFunction();
+  context.break_continue_stack().push_back(
+      {.break_target = loop_exit_id, .continue_target = loop_header_id});
+
+  context.node_stack().Push(parse_node, loop_exit_id);
+  return true;
 }
 
 auto HandleWhileStatement(Context& context, Parse::Node parse_node) -> bool {
-  return context.TODO(parse_node, "HandleWhileStatement");
+  auto loop_exit_id =
+      context.node_stack().Pop<Parse::NodeKind::WhileCondition>();
+  auto loop_header_id =
+      context.node_stack().Pop<Parse::NodeKind::WhileConditionStart>();
+  context.break_continue_stack().pop_back();
+
+  // Add the loop backedge.
+  context.AddNode(SemIR::Branch(parse_node, loop_header_id));
+  context.node_block_stack().Pop();
+
+  // Start emitting the loop exit block.
+  context.node_block_stack().Push(loop_exit_id);
+  context.AddCurrentCodeBlockToFunction();
+  return true;
 }
 
 }  // namespace Carbon::Check

+ 2 - 0
toolchain/check/node_stack.h

@@ -264,6 +264,8 @@ class NodeStack {
       case Parse::NodeKind::IfCondition:
       case Parse::NodeKind::IfExpressionIf:
       case Parse::NodeKind::ParameterList:
+      case Parse::NodeKind::WhileCondition:
+      case Parse::NodeKind::WhileConditionStart:
         return IdKind::NodeBlockId;
       case Parse::NodeKind::FunctionDefinitionStart:
         return IdKind::FunctionId;

+ 154 - 0
toolchain/check/testdata/while/break_continue.carbon

@@ -0,0 +1,154 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn A() -> bool;
+fn B() -> bool;
+fn C() -> bool;
+fn D() -> bool;
+fn E() -> bool;
+fn F() -> bool;
+fn G() -> bool;
+fn H() -> bool;
+
+fn While() {
+  while (A()) {
+    if (B()) { continue; }
+    if (C()) { break; }
+    while (D()) {
+      if (E()) { continue; }
+      if (F()) { break; }
+    }
+    if (G()) { continue; }
+    if (H()) { break; }
+  }
+}
+
+// CHECK:STDOUT: file "break_continue.carbon" {
+// CHECK:STDOUT:   %A = fn_decl @A
+// CHECK:STDOUT:   %B = fn_decl @B
+// CHECK:STDOUT:   %C = fn_decl @C
+// CHECK:STDOUT:   %D = fn_decl @D
+// CHECK:STDOUT:   %E = fn_decl @E
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT:   %G = fn_decl @G
+// CHECK:STDOUT:   %H = fn_decl @H
+// CHECK:STDOUT:   %While = fn_decl @While
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @A() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @B() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @C() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @D() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @E() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @H() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @While() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   br !while.cond.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.cond.loc17:
+// CHECK:STDOUT:   %A.ref = name_reference_untyped "A", package.%A
+// CHECK:STDOUT:   %.loc17_11.1: init bool = call @A()
+// CHECK:STDOUT:   %.loc17_11.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc17_11.3: ref bool = temporary %.loc17_11.2, %.loc17_11.1
+// CHECK:STDOUT:   %.loc17_11.4: bool = bind_value %.loc17_11.3
+// CHECK:STDOUT:   if %.loc17_11.4 br !while.body.loc17 else br !while.done.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.body.loc17:
+// CHECK:STDOUT:   %B.ref = name_reference_untyped "B", package.%B
+// CHECK:STDOUT:   %.loc18_10.1: init bool = call @B()
+// CHECK:STDOUT:   %.loc18_10.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc18_10.3: ref bool = temporary %.loc18_10.2, %.loc18_10.1
+// CHECK:STDOUT:   %.loc18_10.4: bool = bind_value %.loc18_10.3
+// CHECK:STDOUT:   if %.loc18_10.4 br !if.then.loc18 else br !if.else.loc18
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc18:
+// CHECK:STDOUT:   br !while.cond.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc18:
+// CHECK:STDOUT:   %C.ref = name_reference_untyped "C", package.%C
+// CHECK:STDOUT:   %.loc19_10.1: init bool = call @C()
+// CHECK:STDOUT:   %.loc19_10.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc19_10.3: ref bool = temporary %.loc19_10.2, %.loc19_10.1
+// CHECK:STDOUT:   %.loc19_10.4: bool = bind_value %.loc19_10.3
+// CHECK:STDOUT:   if %.loc19_10.4 br !if.then.loc19 else br !if.else.loc19
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc19:
+// CHECK:STDOUT:   br !while.done.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc19:
+// CHECK:STDOUT:   br !while.cond.loc20
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.cond.loc20:
+// CHECK:STDOUT:   %D.ref = name_reference_untyped "D", package.%D
+// CHECK:STDOUT:   %.loc20_13.1: init bool = call @D()
+// CHECK:STDOUT:   %.loc20_13.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc20_13.3: ref bool = temporary %.loc20_13.2, %.loc20_13.1
+// CHECK:STDOUT:   %.loc20_13.4: bool = bind_value %.loc20_13.3
+// CHECK:STDOUT:   if %.loc20_13.4 br !while.body.loc20 else br !while.done.loc20
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.body.loc20:
+// CHECK:STDOUT:   %E.ref = name_reference_untyped "E", package.%E
+// CHECK:STDOUT:   %.loc21_12.1: init bool = call @E()
+// CHECK:STDOUT:   %.loc21_12.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc21_12.3: ref bool = temporary %.loc21_12.2, %.loc21_12.1
+// CHECK:STDOUT:   %.loc21_12.4: bool = bind_value %.loc21_12.3
+// CHECK:STDOUT:   if %.loc21_12.4 br !if.then.loc21 else br !if.else.loc21
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc21:
+// CHECK:STDOUT:   br !while.cond.loc20
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc21:
+// CHECK:STDOUT:   %F.ref = name_reference_untyped "F", package.%F
+// CHECK:STDOUT:   %.loc22_12.1: init bool = call @F()
+// CHECK:STDOUT:   %.loc22_12.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc22_12.3: ref bool = temporary %.loc22_12.2, %.loc22_12.1
+// CHECK:STDOUT:   %.loc22_12.4: bool = bind_value %.loc22_12.3
+// CHECK:STDOUT:   if %.loc22_12.4 br !if.then.loc22 else br !if.else.loc22
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc22:
+// CHECK:STDOUT:   br !while.done.loc20
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc22:
+// CHECK:STDOUT:   br !while.cond.loc20
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.done.loc20:
+// CHECK:STDOUT:   %G.ref = name_reference_untyped "G", package.%G
+// CHECK:STDOUT:   %.loc24_10.1: init bool = call @G()
+// CHECK:STDOUT:   %.loc24_10.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc24_10.3: ref bool = temporary %.loc24_10.2, %.loc24_10.1
+// CHECK:STDOUT:   %.loc24_10.4: bool = bind_value %.loc24_10.3
+// CHECK:STDOUT:   if %.loc24_10.4 br !if.then.loc24 else br !if.else.loc24
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc24:
+// CHECK:STDOUT:   br !while.cond.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc24:
+// CHECK:STDOUT:   %H.ref = name_reference_untyped "H", package.%H
+// CHECK:STDOUT:   %.loc25_10.1: init bool = call @H()
+// CHECK:STDOUT:   %.loc25_10.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc25_10.3: ref bool = temporary %.loc25_10.2, %.loc25_10.1
+// CHECK:STDOUT:   %.loc25_10.4: bool = bind_value %.loc25_10.3
+// CHECK:STDOUT:   if %.loc25_10.4 br !if.then.loc25 else br !if.else.loc25
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc25:
+// CHECK:STDOUT:   br !while.done.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc25:
+// CHECK:STDOUT:   br !while.cond.loc17
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.done.loc17:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 31 - 0
toolchain/check/testdata/while/fail_bad_condition.carbon

@@ -0,0 +1,31 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn While() {
+  // CHECK:STDERR: fail_bad_condition.carbon:[[@LINE+3]]:17: ERROR: Cannot implicitly convert from `String` to `bool`.
+  // CHECK:STDERR:   while ("Hello") {}
+  // CHECK:STDERR:                 ^
+  while ("Hello") {}
+}
+
+// CHECK:STDOUT: file "fail_bad_condition.carbon" {
+// CHECK:STDOUT:   %While = fn_decl @While
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @While() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   br !while.cond
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.cond:
+// CHECK:STDOUT:   %.loc11: String = string_literal "Hello"
+// CHECK:STDOUT:   if <error> br !while.body else br !while.done
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.body:
+// CHECK:STDOUT:   br !while.cond
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.done:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 36 - 0
toolchain/check/testdata/while/fail_break_continue.carbon

@@ -0,0 +1,36 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn While() {
+  // CHECK:STDERR: fail_break_continue.carbon:[[@LINE+3]]:3: ERROR: `continue` can only be used in a loop.
+  // CHECK:STDERR:   continue;
+  // CHECK:STDERR:   ^
+  continue;
+  // CHECK:STDERR: fail_break_continue.carbon:[[@LINE+3]]:3: ERROR: `break` can only be used in a loop.
+  // CHECK:STDERR:   break;
+  // CHECK:STDERR:   ^
+  break;
+  while (false) {
+    continue;
+    break;
+  }
+  // CHECK:STDERR: fail_break_continue.carbon:[[@LINE+3]]:3: ERROR: `continue` can only be used in a loop.
+  // CHECK:STDERR:   continue;
+  // CHECK:STDERR:   ^
+  continue;
+  // CHECK:STDERR: fail_break_continue.carbon:[[@LINE+3]]:3: ERROR: `break` can only be used in a loop.
+  // CHECK:STDERR:   break;
+  // CHECK:STDERR:   ^
+  break;
+}
+
+// CHECK:STDOUT: file "fail_break_continue.carbon" {
+// CHECK:STDOUT:   %While = fn_decl @While
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @While() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT: }

+ 62 - 0
toolchain/check/testdata/while/unreachable_end.carbon

@@ -0,0 +1,62 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn Cond() -> bool;
+
+fn F();
+fn G();
+fn H();
+
+fn While() {
+  F();
+  while (Cond()) {
+    G();
+    return;
+  }
+  H();
+}
+
+// CHECK:STDOUT: file "unreachable_end.carbon" {
+// CHECK:STDOUT:   %Cond = fn_decl @Cond
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT:   %G = fn_decl @G
+// CHECK:STDOUT:   %H = fn_decl @H
+// CHECK:STDOUT:   %While = fn_decl @While
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Cond() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @H();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @While() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %F.ref = name_reference_untyped "F", package.%F
+// CHECK:STDOUT:   %.loc14_4.1: type = tuple_type ()
+// CHECK:STDOUT:   %.loc14_4.2: init () = call @F()
+// CHECK:STDOUT:   br !while.cond
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.cond:
+// CHECK:STDOUT:   %Cond.ref = name_reference_untyped "Cond", package.%Cond
+// CHECK:STDOUT:   %.loc15_14.1: init bool = call @Cond()
+// CHECK:STDOUT:   %.loc15_14.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc15_14.3: ref bool = temporary %.loc15_14.2, %.loc15_14.1
+// CHECK:STDOUT:   %.loc15_14.4: bool = bind_value %.loc15_14.3
+// CHECK:STDOUT:   if %.loc15_14.4 br !while.body else br !while.done
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.body:
+// CHECK:STDOUT:   %G.ref = name_reference_untyped "G", package.%G
+// CHECK:STDOUT:   %.loc16: init () = call @G()
+// CHECK:STDOUT:   return
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.done:
+// CHECK:STDOUT:   %H.ref = name_reference_untyped "H", package.%H
+// CHECK:STDOUT:   %.loc19: init () = call @H()
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 61 - 0
toolchain/check/testdata/while/while.carbon

@@ -0,0 +1,61 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn Cond() -> bool;
+
+fn F();
+fn G();
+fn H();
+
+fn While() {
+  F();
+  while (Cond()) {
+    G();
+  }
+  H();
+}
+
+// CHECK:STDOUT: file "while.carbon" {
+// CHECK:STDOUT:   %Cond = fn_decl @Cond
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT:   %G = fn_decl @G
+// CHECK:STDOUT:   %H = fn_decl @H
+// CHECK:STDOUT:   %While = fn_decl @While
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Cond() -> bool;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @H();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @While() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %F.ref = name_reference_untyped "F", package.%F
+// CHECK:STDOUT:   %.loc14_4.1: type = tuple_type ()
+// CHECK:STDOUT:   %.loc14_4.2: init () = call @F()
+// CHECK:STDOUT:   br !while.cond
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.cond:
+// CHECK:STDOUT:   %Cond.ref = name_reference_untyped "Cond", package.%Cond
+// CHECK:STDOUT:   %.loc15_14.1: init bool = call @Cond()
+// CHECK:STDOUT:   %.loc15_14.2: ref bool = temporary_storage
+// CHECK:STDOUT:   %.loc15_14.3: ref bool = temporary %.loc15_14.2, %.loc15_14.1
+// CHECK:STDOUT:   %.loc15_14.4: bool = bind_value %.loc15_14.3
+// CHECK:STDOUT:   if %.loc15_14.4 br !while.body else br !while.done
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.body:
+// CHECK:STDOUT:   %G.ref = name_reference_untyped "G", package.%G
+// CHECK:STDOUT:   %.loc16: init () = call @G()
+// CHECK:STDOUT:   br !while.cond
+// CHECK:STDOUT:
+// CHECK:STDOUT: !while.done:
+// CHECK:STDOUT:   %H.ref = name_reference_untyped "H", package.%H
+// CHECK:STDOUT:   %.loc18: init () = call @H()
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 2 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -114,6 +114,8 @@ CARBON_DIAGNOSTIC_KIND(AddressOfNonReference)
 CARBON_DIAGNOSTIC_KIND(ArrayInitFromLiteralArgCountMismatch)
 CARBON_DIAGNOSTIC_KIND(ArrayInitFromExpressionArgCountMismatch)
 CARBON_DIAGNOSTIC_KIND(AssignmentToNonAssignable)
+CARBON_DIAGNOSTIC_KIND(BreakOutsideLoop)
+CARBON_DIAGNOSTIC_KIND(ContinueOutsideLoop)
 CARBON_DIAGNOSTIC_KIND(DereferenceOfNonPointer)
 CARBON_DIAGNOSTIC_KIND(DereferenceOfType)
 CARBON_DIAGNOSTIC_KIND(NameNotFound)

+ 8 - 0
toolchain/lower/file_context.cpp

@@ -186,6 +186,14 @@ auto FileContext::BuildFunctionDefinition(SemIR::FunctionId function_id)
     function_lowering.builder().SetInsertPoint(llvm_block);
     function_lowering.LowerBlock(block_id);
   }
+
+  // LLVM requires that the entry block has no predecessors.
+  auto* entry_block = &llvm_function->getEntryBlock();
+  if (entry_block->hasNPredecessorsOrMore(1)) {
+    auto* new_entry_block = llvm::BasicBlock::Create(
+        llvm_context(), "entry", llvm_function, entry_block);
+    llvm::BranchInst::Create(entry_block, new_entry_block);
+  }
 }
 
 auto FileContext::BuildType(SemIR::NodeId node_id) -> llvm::Type* {

+ 1 - 0
toolchain/lower/handle.cpp

@@ -74,6 +74,7 @@ auto HandleBoolLiteral(FunctionContext& context, SemIR::NodeId node_id,
 auto HandleBranch(FunctionContext& context, SemIR::NodeId /*node_id*/,
                   SemIR::Branch node) -> void {
   // Opportunistically avoid creating a BasicBlock that contains just a branch.
+  // TODO: Don't do this if it would remove a loop preheader block.
   llvm::BasicBlock* block = context.builder().GetInsertBlock();
   if (block->empty() && context.TryToReuseBlock(node.target_id, block)) {
     // Reuse this block as the branch target.

+ 66 - 0
toolchain/lower/testdata/while/break_continue.carbon

@@ -0,0 +1,66 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn A() -> bool;
+fn B() -> bool;
+fn C() -> bool;
+
+fn While() {
+  while (A()) {
+    if (B()) { continue; }
+    if (C()) { break; }
+  }
+}
+
+// CHECK:STDOUT: ; ModuleID = 'break_continue.carbon'
+// CHECK:STDOUT: source_filename = "break_continue.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @A()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @B()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @C()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @While() {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   br label %0
+// CHECK:STDOUT:
+// CHECK:STDOUT: 0:                                                ; preds = %entry, %8, %4
+// CHECK:STDOUT:   %A = call i1 @A()
+// CHECK:STDOUT:   %temp = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %A, ptr %temp, align 1
+// CHECK:STDOUT:   %1 = load i1, ptr %temp, align 1
+// CHECK:STDOUT:   br i1 %1, label %2, label %9
+// CHECK:STDOUT:
+// CHECK:STDOUT: 2:                                                ; preds = %0
+// CHECK:STDOUT:   %B = call i1 @B()
+// CHECK:STDOUT:   %temp1 = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %B, ptr %temp1, align 1
+// CHECK:STDOUT:   %3 = load i1, ptr %temp1, align 1
+// CHECK:STDOUT:   br i1 %3, label %4, label %5
+// CHECK:STDOUT:
+// CHECK:STDOUT: 4:                                                ; preds = %2
+// CHECK:STDOUT:   br label %0
+// CHECK:STDOUT:
+// CHECK:STDOUT: 5:                                                ; preds = %2
+// CHECK:STDOUT:   %C = call i1 @C()
+// CHECK:STDOUT:   %temp2 = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %C, ptr %temp2, align 1
+// CHECK:STDOUT:   %6 = load i1, ptr %temp2, align 1
+// CHECK:STDOUT:   br i1 %6, label %7, label %8
+// CHECK:STDOUT:
+// CHECK:STDOUT: 7:                                                ; preds = %5
+// CHECK:STDOUT:   br label %9
+// CHECK:STDOUT:
+// CHECK:STDOUT: 8:                                                ; preds = %5
+// CHECK:STDOUT:   br label %0
+// CHECK:STDOUT:
+// CHECK:STDOUT: 9:                                                ; preds = %7, %0
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; uselistorder directives
+// CHECK:STDOUT:   uselistorder label %0, { 1, 2, 0 }
+// CHECK:STDOUT: }

+ 77 - 0
toolchain/lower/testdata/while/preheader.carbon

@@ -0,0 +1,77 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn Cond() -> bool;
+
+fn F();
+fn G();
+
+// TODO: It would be better to preserve the loop preheader blocks from SemIR
+// into LLVM IR. If we don't, LLVM will recreate them for us as part of loop
+// canonicalization.
+fn While() {
+  while (Cond()) {
+    F();
+  }
+
+  if (Cond()) {
+    while (Cond()) {
+      G();
+    }
+  }
+}
+
+// CHECK:STDOUT: ; ModuleID = 'preheader.carbon'
+// CHECK:STDOUT: source_filename = "preheader.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @Cond()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @F()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @G()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @While() {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   br label %0
+// CHECK:STDOUT:
+// CHECK:STDOUT: 0:                                                ; preds = %entry, %2
+// CHECK:STDOUT:   %Cond = call i1 @Cond()
+// CHECK:STDOUT:   %temp = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %Cond, ptr %temp, align 1
+// CHECK:STDOUT:   %1 = load i1, ptr %temp, align 1
+// CHECK:STDOUT:   br i1 %1, label %2, label %3
+// CHECK:STDOUT:
+// CHECK:STDOUT: 2:                                                ; preds = %0
+// CHECK:STDOUT:   call void @F()
+// CHECK:STDOUT:   br label %0
+// CHECK:STDOUT:
+// CHECK:STDOUT: 3:                                                ; preds = %0
+// CHECK:STDOUT:   %Cond1 = call i1 @Cond()
+// CHECK:STDOUT:   %temp2 = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %Cond1, ptr %temp2, align 1
+// CHECK:STDOUT:   %4 = load i1, ptr %temp2, align 1
+// CHECK:STDOUT:   br i1 %4, label %5, label %9
+// CHECK:STDOUT:
+// CHECK:STDOUT: 5:                                                ; preds = %7, %3
+// CHECK:STDOUT:   %Cond3 = call i1 @Cond()
+// CHECK:STDOUT:   %temp4 = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %Cond3, ptr %temp4, align 1
+// CHECK:STDOUT:   %6 = load i1, ptr %temp4, align 1
+// CHECK:STDOUT:   br i1 %6, label %7, label %8
+// CHECK:STDOUT:
+// CHECK:STDOUT: 7:                                                ; preds = %5
+// CHECK:STDOUT:   call void @G()
+// CHECK:STDOUT:   br label %5
+// CHECK:STDOUT:
+// CHECK:STDOUT: 8:                                                ; preds = %5
+// CHECK:STDOUT:   br label %9
+// CHECK:STDOUT:
+// CHECK:STDOUT: 9:                                                ; preds = %8, %3
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; uselistorder directives
+// CHECK:STDOUT:   uselistorder label %0, { 1, 0 }
+// CHECK:STDOUT: }

+ 51 - 0
toolchain/lower/testdata/while/unreachable_end.carbon

@@ -0,0 +1,51 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn Cond() -> bool;
+
+fn F();
+fn G();
+fn H();
+
+fn While() {
+  F();
+  while (Cond()) {
+    G();
+    return;
+  }
+  H();
+}
+
+// CHECK:STDOUT: ; ModuleID = 'unreachable_end.carbon'
+// CHECK:STDOUT: source_filename = "unreachable_end.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @Cond()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @F()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @G()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @H()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @While() {
+// CHECK:STDOUT:   call void @F()
+// CHECK:STDOUT:   br label %1
+// CHECK:STDOUT:
+// CHECK:STDOUT: 1:                                                ; preds = %0
+// CHECK:STDOUT:   %Cond = call i1 @Cond()
+// CHECK:STDOUT:   %temp = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %Cond, ptr %temp, align 1
+// CHECK:STDOUT:   %2 = load i1, ptr %temp, align 1
+// CHECK:STDOUT:   br i1 %2, label %3, label %4
+// CHECK:STDOUT:
+// CHECK:STDOUT: 3:                                                ; preds = %1
+// CHECK:STDOUT:   call void @G()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT:
+// CHECK:STDOUT: 4:                                                ; preds = %1
+// CHECK:STDOUT:   call void @H()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }

+ 50 - 0
toolchain/lower/testdata/while/while.carbon

@@ -0,0 +1,50 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn Cond() -> bool;
+
+fn F();
+fn G();
+fn H();
+
+fn While() {
+  F();
+  while (Cond()) {
+    G();
+  }
+  H();
+}
+
+// CHECK:STDOUT: ; ModuleID = 'while.carbon'
+// CHECK:STDOUT: source_filename = "while.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i1 @Cond()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @F()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @G()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @H()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @While() {
+// CHECK:STDOUT:   call void @F()
+// CHECK:STDOUT:   br label %1
+// CHECK:STDOUT:
+// CHECK:STDOUT: 1:                                                ; preds = %3, %0
+// CHECK:STDOUT:   %Cond = call i1 @Cond()
+// CHECK:STDOUT:   %temp = alloca i1, align 1
+// CHECK:STDOUT:   store i1 %Cond, ptr %temp, align 1
+// CHECK:STDOUT:   %2 = load i1, ptr %temp, align 1
+// CHECK:STDOUT:   br i1 %2, label %3, label %4
+// CHECK:STDOUT:
+// CHECK:STDOUT: 3:                                                ; preds = %1
+// CHECK:STDOUT:   call void @G()
+// CHECK:STDOUT:   br label %1
+// CHECK:STDOUT:
+// CHECK:STDOUT: 4:                                                ; preds = %1
+// CHECK:STDOUT:   call void @H()
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }

+ 17 - 0
toolchain/sem_ir/formatter.cpp

@@ -315,6 +315,23 @@ class NodeNamer {
         break;
       }
 
+      case Parse::NodeKind::WhileConditionStart:
+        name = "while.cond";
+        break;
+
+      case Parse::NodeKind::WhileCondition:
+        switch (node.kind()) {
+          case NodeKind::BranchIf:
+            name = "while.body";
+            break;
+          case NodeKind::Branch:
+            name = "while.done";
+            break;
+          default:
+            break;
+        }
+        break;
+
       default:
         break;
     }