فهرست منبع

Support for `let` declarations. (#3257)

A `let` declaration is represented by a `bind_name` node in SemIR:

```carbon-semir
  %b: i32 = bind_name "b", %a
```

Because `Check` encounters the pattern before it sees the value, we
first create the `bind_name` node with an unset value and don't add it
to the block. Then, once we've seen and converted the initializer, we
update the `bind_name` to have the value and add it to the current
block, after the initializer code.
Richard Smith 2 سال پیش
والد
کامیت
74d52738ff

+ 50 - 0
toolchain/check/handle_let.cpp

@@ -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
+
+#include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
+#include "toolchain/sem_ir/node.h"
+
+namespace Carbon::Check {
+
+auto HandleLetDeclaration(Context& context, Parse::Node parse_node) -> bool {
+  auto value_id = context.node_stack().PopExpression();
+  SemIR::NodeId pattern_id =
+      context.node_stack().Pop<Parse::NodeKind::PatternBinding>();
+  context.node_stack()
+      .PopAndDiscardSoloParseNode<Parse::NodeKind::LetIntroducer>();
+
+  // Convert the value to match the type of the pattern.
+  auto pattern = context.semantics_ir().GetNode(pattern_id);
+  value_id =
+      ConvertToValueOfType(context, parse_node, value_id, pattern.type_id());
+
+  // Update the binding with its value and add it to the current block, after
+  // the computation of the value.
+  auto [name_id, absent_value_id] = pattern.GetAsBindName();
+  CARBON_CHECK(!absent_value_id.is_valid())
+      << "Binding should not already have a value!";
+  context.semantics_ir().ReplaceNode(
+      pattern_id,
+      SemIR::Node::BindName::Make(pattern.parse_node(), pattern.type_id(),
+                                  name_id, value_id));
+  context.node_block_stack().AddNodeId(pattern_id);
+
+  // Add the name of the binding to the current scope.
+  context.AddNameToLookup(pattern.parse_node(), name_id, pattern_id);
+  return true;
+}
+
+auto HandleLetIntroducer(Context& context, Parse::Node parse_node) -> bool {
+  // Push a bracketing node to establish the pattern context.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
+auto HandleLetInitializer(Context& /*context*/, Parse::Node /*parse_node*/)
+    -> bool {
+  return true;
+}
+
+}  // namespace Carbon::Check

+ 11 - 0
toolchain/check/handle_pattern_binding.cpp

@@ -28,6 +28,7 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
 
   // Allocate a node of the appropriate kind, linked to the name for error
   // locations.
+  // TODO: Each of these cases should create a `BindName` node.
   switch (auto context_parse_node_kind = context.parse_tree().node_kind(
               context.node_stack().PeekParseNode())) {
     case Parse::NodeKind::VariableIntroducer:
@@ -38,6 +39,16 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
       context.AddNodeAndPush(parse_node, SemIR::Node::Parameter::Make(
                                              name_node, cast_type_id, name_id));
       break;
+    case Parse::NodeKind::LetIntroducer:
+      // Create the node, but don't add it to a block until after we've formed
+      // its initializer.
+      // TODO: For general pattern parsing, we'll need to create a block to hold
+      // the `let` pattern before we see the initializer.
+      context.node_stack().Push(
+          parse_node,
+          context.semantics_ir().AddNodeInNoBlock(SemIR::Node::BindName::Make(
+              name_node, cast_type_id, name_id, SemIR::NodeId::Invalid)));
+      break;
     default:
       CARBON_FATAL() << "Found a pattern binding in unexpected context "
                      << context_parse_node_kind;

+ 1 - 0
toolchain/check/node_stack.h

@@ -273,6 +273,7 @@ class NodeStack {
       case Parse::NodeKind::CodeBlockStart:
       case Parse::NodeKind::FunctionIntroducer:
       case Parse::NodeKind::IfStatementElse:
+      case Parse::NodeKind::LetIntroducer:
       case Parse::NodeKind::ParameterListStart:
       case Parse::NodeKind::ParenExpressionOrTupleLiteralStart:
       case Parse::NodeKind::QualifiedDeclaration:

+ 50 - 0
toolchain/check/testdata/let/convert.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 F() -> i32 {
+  var v: (i32, i32, i32) = (1, 2, 3);
+  // Convert from object representation to value representation.
+  let w: (i32, i32, i32) = v;
+  return w[1];
+}
+
+// CHECK:STDOUT: file "convert.carbon" {
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc8_24.1: type = tuple_type (type, type, type)
+// CHECK:STDOUT:   %.loc8_24.2: (type, type, type) = tuple_literal (i32, i32, i32)
+// CHECK:STDOUT:   %.loc8_24.3: type = tuple_type (i32, i32, i32)
+// CHECK:STDOUT:   %v: ref (i32, i32, i32) = var "v"
+// CHECK:STDOUT:   %.loc8_29: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc8_32: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc8_35: i32 = int_literal 3
+// CHECK:STDOUT:   %.loc8_36.1: (i32, i32, i32) = tuple_literal (%.loc8_29, %.loc8_32, %.loc8_35)
+// CHECK:STDOUT:   %.loc8_36.2: ref i32 = tuple_access %v, member0
+// CHECK:STDOUT:   %.loc8_36.3: init i32 = initialize_from %.loc8_29 to %.loc8_36.2
+// CHECK:STDOUT:   %.loc8_36.4: ref i32 = tuple_access %v, member1
+// CHECK:STDOUT:   %.loc8_36.5: init i32 = initialize_from %.loc8_32 to %.loc8_36.4
+// CHECK:STDOUT:   %.loc8_36.6: ref i32 = tuple_access %v, member2
+// CHECK:STDOUT:   %.loc8_36.7: init i32 = initialize_from %.loc8_35 to %.loc8_36.6
+// CHECK:STDOUT:   %.loc8_36.8: init (i32, i32, i32) = tuple_init %.loc8_36.1, (%.loc8_36.3, %.loc8_36.5, %.loc8_36.7)
+// CHECK:STDOUT:   assign %v, %.loc8_36.8
+// CHECK:STDOUT:   %.loc10_24: (type, type, type) = tuple_literal (i32, i32, i32)
+// CHECK:STDOUT:   %v.ref: ref (i32, i32, i32) = name_reference "v", %v
+// CHECK:STDOUT:   %.loc10_28.1: ref i32 = tuple_access %v.ref, member0
+// CHECK:STDOUT:   %.loc10_28.2: i32 = bind_value %.loc10_28.1
+// CHECK:STDOUT:   %.loc10_28.3: ref i32 = tuple_access %v.ref, member1
+// CHECK:STDOUT:   %.loc10_28.4: i32 = bind_value %.loc10_28.3
+// CHECK:STDOUT:   %.loc10_28.5: ref i32 = tuple_access %v.ref, member2
+// CHECK:STDOUT:   %.loc10_28.6: i32 = bind_value %.loc10_28.5
+// CHECK:STDOUT:   %.loc10_28.7: (i32, i32, i32) = tuple_value %v.ref, (%.loc10_28.2, %.loc10_28.4, %.loc10_28.6)
+// CHECK:STDOUT:   %w: (i32, i32, i32) = bind_name "w", %.loc10_28.7
+// CHECK:STDOUT:   %w.ref: (i32, i32, i32) = name_reference "w", %w
+// CHECK:STDOUT:   %.loc11_12: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc11_13: i32 = tuple_index %w.ref, %.loc11_12
+// CHECK:STDOUT:   return %.loc11_13
+// CHECK:STDOUT: }

+ 26 - 0
toolchain/check/testdata/let/fail_duplicate_decl.carbon

@@ -0,0 +1,26 @@
+// 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 F(a: i32) {
+  // CHECK:STDERR: fail_duplicate_decl.carbon:[[@LINE+6]]:7: ERROR: Duplicate name being declared in the same scope.
+  // CHECK:STDERR:   let a: i32 = 1;
+  // CHECK:STDERR:       ^
+  // CHECK:STDERR: fail_duplicate_decl.carbon:[[@LINE-4]]:6: Name is previously declared here.
+  // CHECK:STDERR: fn F(a: i32) {
+  // CHECK:STDERR:      ^
+  let a: i32 = 1;
+}
+
+// CHECK:STDOUT: file "fail_duplicate_decl.carbon" {
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%a.loc7: i32) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc14: i32 = int_literal 1
+// CHECK:STDOUT:   %a.loc14: i32 = bind_name "a", %.loc14
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 23 - 0
toolchain/check/testdata/let/fail_use_in_init.carbon

@@ -0,0 +1,23 @@
+// 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 F() {
+  // CHECK:STDERR: fail_use_in_init.carbon:[[@LINE+3]]:16: ERROR: Name `a` not found.
+  // CHECK:STDERR:   let a: i32 = a;
+  // CHECK:STDERR:                ^
+  let a: i32 = a;
+}
+
+// CHECK:STDOUT: file "fail_use_in_init.carbon" {
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %a.ref: <error> = name_reference "a", <error>
+// CHECK:STDOUT:   %a: i32 = bind_name "a", <error>
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 21 - 0
toolchain/check/testdata/let/global.carbon

@@ -0,0 +1,21 @@
+// 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
+
+let n: i32 = 1;
+
+fn F() -> i32 { return n; }
+
+// CHECK:STDOUT: file "global.carbon" {
+// CHECK:STDOUT:   %.loc7: i32 = int_literal 1
+// CHECK:STDOUT:   %n: i32 = bind_name "n", %.loc7
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %n.ref: i32 = name_reference "n", package.%n
+// CHECK:STDOUT:   return %n.ref
+// CHECK:STDOUT: }

+ 22 - 0
toolchain/check/testdata/let/local.carbon

@@ -0,0 +1,22 @@
+// 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 F(a: i32) -> i32 {
+  let b: i32 = a;
+  return b;
+}
+
+// CHECK:STDOUT: file "local.carbon" {
+// CHECK:STDOUT:   %F = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%a: i32) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %a.ref: i32 = name_reference "a", %a
+// CHECK:STDOUT:   %b: i32 = bind_name "b", %a.ref
+// CHECK:STDOUT:   %b.ref: i32 = name_reference "b", %b
+// CHECK:STDOUT:   return %b.ref
+// CHECK:STDOUT: }

+ 2 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -64,6 +64,7 @@ CARBON_DIAGNOSTIC_KIND(ExpectedCloseSymbol)
 CARBON_DIAGNOSTIC_KIND(ExpectedCodeBlock)
 CARBON_DIAGNOSTIC_KIND(ExpectedExpression)
 CARBON_DIAGNOSTIC_KIND(ExpectedIdentifierAfterDotOrArrow)
+CARBON_DIAGNOSTIC_KIND(ExpectedLetBindingName)
 CARBON_DIAGNOSTIC_KIND(ExpectedParameterName)
 CARBON_DIAGNOSTIC_KIND(ExpectedParenAfter)
 CARBON_DIAGNOSTIC_KIND(ExpectedExpressionSemi)
@@ -97,6 +98,7 @@ CARBON_DIAGNOSTIC_KIND(ExpectedElseAfterIf)
 CARBON_DIAGNOSTIC_KIND(ExpectedDeclarationName)
 CARBON_DIAGNOSTIC_KIND(ExpectedDeclarationSemi)
 CARBON_DIAGNOSTIC_KIND(ExpectedDeclarationSemiOrDefinition)
+CARBON_DIAGNOSTIC_KIND(ExpectedInitializerAfterLet)
 CARBON_DIAGNOSTIC_KIND(MethodImplNotAllowed)
 CARBON_DIAGNOSTIC_KIND(ParametersRequiredByIntroducer)
 CARBON_DIAGNOSTIC_KIND(ParametersRequiredByDeduced)

+ 4 - 3
toolchain/lower/handle.cpp

@@ -60,9 +60,10 @@ auto HandleBinaryOperatorAdd(FunctionContext& /*context*/,
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
-auto HandleBindName(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                    SemIR::Node /*node*/) -> void {
-  // Probably need to do something here, but not necessary for now.
+auto HandleBindName(FunctionContext& context, SemIR::NodeId node_id,
+                    SemIR::Node node) -> void {
+  auto [name_id, value_id] = node.GetAsBindName();
+  context.SetLocal(node_id, context.GetLocal(value_id));
 }
 
 auto HandleBlockArg(FunctionContext& context, SemIR::NodeId node_id,

+ 18 - 0
toolchain/lower/testdata/let/local.carbon

@@ -0,0 +1,18 @@
+// 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 Run() -> i32 {
+  let n: i32 = 1;
+  let m: i32 = n;
+  return m;
+}
+
+// CHECK:STDOUT: ; ModuleID = 'local.carbon'
+// CHECK:STDOUT: source_filename = "local.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @main() {
+// CHECK:STDOUT:   ret i32 1
+// CHECK:STDOUT: }

+ 61 - 0
toolchain/lower/testdata/let/tuple.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 F() -> i32 {
+  var a: (i32, i32, i32) = (1, 2, 3);
+  var b: (i32, i32) = (4, 5);
+  let c: ((i32, i32, i32), (i32, i32)) = (a, b);
+  return c[1][1];
+}
+
+// CHECK:STDOUT: ; ModuleID = 'tuple.carbon'
+// CHECK:STDOUT: source_filename = "tuple.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @F() {
+// CHECK:STDOUT:   %a = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %tuple.elem = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 0
+// CHECK:STDOUT:   store i32 1, ptr %tuple.elem, align 4
+// CHECK:STDOUT:   %tuple.elem1 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 1
+// CHECK:STDOUT:   store i32 2, ptr %tuple.elem1, align 4
+// CHECK:STDOUT:   %tuple.elem2 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 2
+// CHECK:STDOUT:   store i32 3, ptr %tuple.elem2, align 4
+// CHECK:STDOUT:   %b = alloca { i32, i32 }, align 8
+// CHECK:STDOUT:   %tuple.elem3 = getelementptr inbounds { i32, i32 }, ptr %b, i32 0, i32 0
+// CHECK:STDOUT:   store i32 4, ptr %tuple.elem3, align 4
+// CHECK:STDOUT:   %tuple.elem4 = getelementptr inbounds { i32, i32 }, ptr %b, i32 0, i32 1
+// CHECK:STDOUT:   store i32 5, ptr %tuple.elem4, align 4
+// CHECK:STDOUT:   %tuple.elem5 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 0
+// CHECK:STDOUT:   %1 = load i32, ptr %tuple.elem5, align 4
+// CHECK:STDOUT:   %tuple.elem6 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 1
+// CHECK:STDOUT:   %2 = load i32, ptr %tuple.elem6, align 4
+// CHECK:STDOUT:   %tuple.elem7 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 2
+// CHECK:STDOUT:   %3 = load i32, ptr %tuple.elem7, align 4
+// CHECK:STDOUT:   %tuple = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %4 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %1, ptr %4, align 4
+// CHECK:STDOUT:   %5 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %2, ptr %5, align 4
+// CHECK:STDOUT:   %6 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 2
+// CHECK:STDOUT:   store i32 %3, ptr %6, align 4
+// CHECK:STDOUT:   %tuple.elem8 = getelementptr inbounds { i32, i32 }, ptr %b, i32 0, i32 0
+// CHECK:STDOUT:   %7 = load i32, ptr %tuple.elem8, align 4
+// CHECK:STDOUT:   %tuple.elem9 = getelementptr inbounds { i32, i32 }, ptr %b, i32 0, i32 1
+// CHECK:STDOUT:   %8 = load i32, ptr %tuple.elem9, align 4
+// CHECK:STDOUT:   %tuple10 = alloca { i32, i32 }, align 8
+// CHECK:STDOUT:   %9 = getelementptr inbounds { i32, i32 }, ptr %tuple10, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %7, ptr %9, align 4
+// CHECK:STDOUT:   %10 = getelementptr inbounds { i32, i32 }, ptr %tuple10, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %8, ptr %10, align 4
+// CHECK:STDOUT:   %tuple11 = alloca { { i32, i32, i32 }, { i32, i32 } }, align 8
+// CHECK:STDOUT:   %11 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32 } }, ptr %tuple11, i32 0, i32 0
+// CHECK:STDOUT:   store ptr %tuple, ptr %11, align 8
+// CHECK:STDOUT:   %12 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32 } }, ptr %tuple11, i32 0, i32 1
+// CHECK:STDOUT:   store ptr %tuple10, ptr %12, align 8
+// CHECK:STDOUT:   %tuple.index = getelementptr inbounds { { i32, i32, i32 }, { i32, i32 } }, ptr %tuple11, i32 0, i32 1
+// CHECK:STDOUT:   %tuple.index12 = getelementptr inbounds { i32, i32 }, ptr %tuple.index, i32 0, i32 1
+// CHECK:STDOUT:   %tuple.index.load = load i32, ptr %tuple.index12, align 4
+// CHECK:STDOUT:   ret i32 %tuple.index.load
+// CHECK:STDOUT: }

+ 6 - 1
toolchain/parse/context.h

@@ -29,7 +29,12 @@ class Context {
   enum class ListTokenKind : int8_t { Comma, Close, CommaClose };
 
   // Supported kinds for HandlePattern.
-  enum class PatternKind : int8_t { DeducedParameter, Parameter, Variable };
+  enum class PatternKind : int8_t {
+    DeducedParameter,
+    Parameter,
+    Variable,
+    Let
+  };
 
   // Supported return values for GetDeclarationContext.
   enum class DeclarationContext : int8_t {

+ 4 - 0
toolchain/parse/handle_declaration_scope_loop.cpp

@@ -57,6 +57,10 @@ auto HandleDeclarationScopeLoop(Context& context) -> void {
       context.PushState(State::VarAsSemicolon);
       break;
     }
+    case Lex::TokenKind::Let: {
+      context.PushState(State::Let);
+      break;
+    }
     default: {
       HandleUnrecognizedDeclaration(context);
       break;

+ 61 - 0
toolchain/parse/handle_let.cpp

@@ -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
+
+#include "toolchain/parse/context.h"
+
+namespace Carbon::Parse {
+
+auto HandleLet(Context& context) -> void {
+  context.PopAndDiscardState();
+
+  // These will start at the `let`.
+  context.PushState(State::LetFinish);
+  context.PushState(State::LetAfterPattern);
+
+  context.AddLeafNode(NodeKind::LetIntroducer, context.Consume());
+
+  // This will start at the pattern.
+  context.PushState(State::PatternAsLet);
+}
+
+auto HandleLetAfterPattern(Context& context) -> void {
+  auto state = context.PopState();
+
+  if (state.has_error) {
+    if (auto after_pattern =
+            context.FindNextOf({Lex::TokenKind::Equal, Lex::TokenKind::Semi})) {
+      context.SkipTo(*after_pattern);
+    }
+  }
+
+  if (auto equals = context.ConsumeIf(Lex::TokenKind::Equal)) {
+    context.AddLeafNode(NodeKind::LetInitializer, *equals);
+    context.PushState(State::Expression);
+  } else if (!state.has_error) {
+    CARBON_DIAGNOSTIC(
+        ExpectedInitializerAfterLet, Error,
+        "Expected `=`; `let` declaration must have an initializer.");
+    context.emitter().Emit(*context.position(), ExpectedInitializerAfterLet);
+    context.ReturnErrorOnState();
+  }
+}
+
+auto HandleLetFinish(Context& context) -> void {
+  auto state = context.PopState();
+
+  auto end_token = state.token;
+  if (context.PositionIs(Lex::TokenKind::Semi)) {
+    end_token = context.Consume();
+  } else {
+    context.EmitExpectedDeclarationSemi(Lex::TokenKind::Let);
+    state.has_error = true;
+    if (auto semi_token = context.SkipPastLikelyEnd(state.token)) {
+      end_token = *semi_token;
+    }
+  }
+  context.AddNode(NodeKind::LetDeclaration, end_token, state.subtree_start,
+                  state.has_error);
+}
+
+}  // namespace Carbon::Parse

+ 11 - 1
toolchain/parse/handle_pattern.cpp

@@ -6,7 +6,7 @@
 
 namespace Carbon::Parse {
 
-// Handles PatternAs(DeducedParameter|FunctionParameter|Variable).
+// Handles PatternAs(DeducedParameter|FunctionParameter|Variable|Let).
 static auto HandlePattern(Context& context, Context::PatternKind pattern_kind)
     -> void {
   auto state = context.PopState();
@@ -36,6 +36,12 @@ static auto HandlePattern(Context& context, Context::PatternKind pattern_kind)
         context.emitter().Emit(*context.position(), ExpectedVariableName);
         break;
       }
+      case Context::PatternKind::Let: {
+        CARBON_DIAGNOSTIC(ExpectedLetBindingName, Error,
+                          "Expected pattern in `let` declaration.");
+        context.emitter().Emit(*context.position(), ExpectedLetBindingName);
+        break;
+      }
     }
     // Add a placeholder for the type.
     context.AddLeafNode(NodeKind::InvalidParse, *context.position(),
@@ -90,6 +96,10 @@ auto HandlePatternAsVariable(Context& context) -> void {
   HandlePattern(context, Context::PatternKind::Variable);
 }
 
+auto HandlePatternAsLet(Context& context) -> void {
+  HandlePattern(context, Context::PatternKind::Let);
+}
+
 // Handles PatternFinishAs(Generic|Regular).
 static auto HandlePatternFinish(Context& context, NodeKind node_kind) -> void {
   auto state = context.PopState();

+ 4 - 0
toolchain/parse/handle_statement.cpp

@@ -30,6 +30,10 @@ auto HandleStatement(Context& context) -> void {
       context.PushState(State::StatementIf);
       break;
     }
+    case Lex::TokenKind::Let: {
+      context.PushState(State::Let);
+      break;
+    }
     case Lex::TokenKind::Return: {
       context.PushState(State::StatementReturn);
       break;

+ 10 - 0
toolchain/parse/node_kind.def

@@ -142,6 +142,16 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(GenericPatternBinding, 2)
 CARBON_PARSE_NODE_KIND_CHILD_COUNT(Address, 1)
 CARBON_PARSE_NODE_KIND_CHILD_COUNT(Template, 1)
 
+// `let`:
+//   LetIntroducer
+//   _external_: PatternBinding
+//   LetInitializer
+//   _external_: expression
+// LetDeclaration
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(LetIntroducer, 0)
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(LetInitializer, 0)
+CARBON_PARSE_NODE_KIND_BRACKET(LetDeclaration, LetIntroducer)
+
 // `var`:
 //   VariableIntroducer
 //   _external_: PatternBinding

+ 34 - 8
toolchain/parse/state.def

@@ -38,6 +38,9 @@
 #define CARBON_PARSE_STATE_VARIANTS3(State, Variant1, Variant2, Variant3) \
   CARBON_PARSE_STATE_VARIANT(State, Variant1)                             \
   CARBON_PARSE_STATE_VARIANTS2(State, Variant2, Variant3)
+#define CARBON_PARSE_STATE_VARIANTS4(State, Variant1, Variant2, Variant3, Variant4) \
+  CARBON_PARSE_STATE_VARIANT(State, Variant1)                                       \
+  CARBON_PARSE_STATE_VARIANTS3(State, Variant2, Variant3, Variant4)
 
 // Handles an index expression `a[0]`.
 //
@@ -209,8 +212,6 @@ CARBON_PARSE_STATE(DeclarationNameAndParamsAfterDeduced)
 // Handles processing of a declaration scope. Things like fn, class, interface,
 // and so on.
 //
-// If `EndOfFile`:
-//   (state done)
 // If `Class`:
 //   1. TypeIntroducerAsClass
 //   2. DeclarationScopeLoop
@@ -224,15 +225,18 @@ CARBON_PARSE_STATE(DeclarationNameAndParamsAfterDeduced)
 //   1. TypeIntroducerAsInterface
 //   2. DeclarationScopeLoop
 // If `Namespace`:
-//   2. Namespace
-//   3. DeclarationScopeLoop
+//   1. Namespace
+//   2. DeclarationScopeLoop
 // If `Semi`:
 //   1. DeclarationScopeLoop
 // If `Var`:
-//   1. Var
+//   1. VarAsSemicolon
+//   2. DeclarationScopeLoop
+// If `Let`:
+//   1. Let
 //   2. DeclarationScopeLoop
 // Else:
-//   1. DeclarationScopeLoop
+//   (state done)
 CARBON_PARSE_STATE(DeclarationScopeLoop)
 
 // Handles periods. Only does one `.<expression>` segment; the source is
@@ -513,14 +517,14 @@ CARBON_PARSE_STATE_VARIANTS2(ParenExpressionParameterFinish, Unknown, Tuple)
 CARBON_PARSE_STATE_VARIANTS2(ParenExpressionFinish, Normal, Tuple)
 
 // Handles pattern parsing for a pattern, enqueuing type expression processing.
-// This covers parameter and `var` support.
+// This covers parameter, `let`, and `var` support.
 //
 // If valid:
 //   1. Expression
 //   2. PatternFinishAs(Generic|Regular)
 // Else:
 //   1. PatternFinish
-CARBON_PARSE_STATE_VARIANTS3(Pattern, DeducedParameter, Parameter, Variable)
+CARBON_PARSE_STATE_VARIANTS4(Pattern, DeducedParameter, Parameter, Variable, Let)
 
 // Handles `addr` in a pattern.
 //
@@ -739,4 +743,26 @@ CARBON_PARSE_STATE(VarAfterPattern)
 //   (state done)
 CARBON_PARSE_STATE_VARIANTS2(VarFinish, Semicolon, For)
 
+// Handles the start of a `let`.
+//
+// Always:
+//   1. PatternAsLet
+//   2. LetAfterPattern
+//   3. LetFinish
+CARBON_PARSE_STATE(Let)
+
+// Handles `let` after the pattern, followed by an initializer.
+//
+// If `Equal`:
+//   1. Expression
+// Else:
+//   (state done)
+CARBON_PARSE_STATE(LetAfterPattern)
+
+// Handles `let` parsing at the end.
+//
+// Always:
+//   (state done)
+CARBON_PARSE_STATE(LetFinish)
+
 #undef CARBON_PARSE_STATE

+ 23 - 0
toolchain/parse/testdata/let/fail_bad_name.carbon

@@ -0,0 +1,23 @@
+// 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
+
+// CHECK:STDERR: fail_bad_name.carbon:[[@LINE+3]]:5: ERROR: Expected pattern in `let` declaration.
+// CHECK:STDERR: let ? = 4;
+// CHECK:STDERR:     ^
+let ? = 4;
+
+// CHECK:STDOUT: - filename: fail_bad_name.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'Name', text: '?', has_error: yes},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: '?', has_error: yes},
+// CHECK:STDOUT:       {kind: 'PatternBinding', text: '?', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:       {kind: 'Literal', text: '4'},
+// CHECK:STDOUT:     {kind: 'LetDeclaration', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 21 - 0
toolchain/parse/testdata/let/fail_empty.carbon

@@ -0,0 +1,21 @@
+// 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
+
+// CHECK:STDERR: fail_empty.carbon:[[@LINE+3]]:4: ERROR: Expected pattern in `let` declaration.
+// CHECK:STDERR: let;
+// CHECK:STDERR:    ^
+let;
+
+// CHECK:STDOUT: - filename: fail_empty.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'Name', text: ';', has_error: yes},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: ';', has_error: yes},
+// CHECK:STDOUT:       {kind: 'PatternBinding', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'LetDeclaration', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 23 - 0
toolchain/parse/testdata/let/fail_missing_type.carbon

@@ -0,0 +1,23 @@
+// 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
+
+// CHECK:STDERR: fail_missing_type.carbon:[[@LINE+3]]:7: ERROR: Expected pattern in `let` declaration.
+// CHECK:STDERR: let a = 4;
+// CHECK:STDERR:       ^
+let a = 4;
+
+// CHECK:STDOUT: - filename: fail_missing_type.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'a'},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: '=', has_error: yes},
+// CHECK:STDOUT:       {kind: 'PatternBinding', text: 'a', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:       {kind: 'Literal', text: '4'},
+// CHECK:STDOUT:     {kind: 'LetDeclaration', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 39 - 0
toolchain/parse/testdata/let/fail_missing_value.carbon

@@ -0,0 +1,39 @@
+// 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
+
+// CHECK:STDERR: fail_missing_value.carbon:[[@LINE+3]]:11: ERROR: Expected `=`; `let` declaration must have an initializer.
+// CHECK:STDERR: let a: i32;
+// CHECK:STDERR:           ^
+let a: i32;
+
+fn F() {
+  // CHECK:STDERR: fail_missing_value.carbon:[[@LINE+3]]:13: ERROR: Expected `=`; `let` declaration must have an initializer.
+  // CHECK:STDERR:   let b: i32;
+  // CHECK:STDERR:             ^
+  let b: i32;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_value.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'a'},
+// CHECK:STDOUT:         {kind: 'Literal', text: 'i32'},
+// CHECK:STDOUT:       {kind: 'PatternBinding', text: ':', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'LetDeclaration', text: ';', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:           {kind: 'Name', text: 'b'},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'PatternBinding', text: ':', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'LetDeclaration', text: ';', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 11},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 36 - 0
toolchain/parse/testdata/let/let.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
+
+let v: i32 = 0;
+fn F() {
+  let s: String = "hello";
+}
+
+// CHECK:STDOUT: - filename: let.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'v'},
+// CHECK:STDOUT:         {kind: 'Literal', text: 'i32'},
+// CHECK:STDOUT:       {kind: 'PatternBinding', text: ':', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:       {kind: 'Literal', text: '0'},
+// CHECK:STDOUT:     {kind: 'LetDeclaration', text: ';', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:           {kind: 'Name', text: 's'},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'String'},
+// CHECK:STDOUT:         {kind: 'PatternBinding', text: ':', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'Literal', text: '"hello"'},
+// CHECK:STDOUT:       {kind: 'LetDeclaration', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 13},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 9 - 0
toolchain/sem_ir/file.cpp

@@ -198,6 +198,7 @@ static auto GetTypePrecedence(NodeKind kind) -> int {
     case NodeKind::ArrayInit:
     case NodeKind::Assign:
     case NodeKind::BinaryOperatorAdd:
+    case NodeKind::BindName:
     case NodeKind::BindValue:
     case NodeKind::BlockArg:
     case NodeKind::BoolLiteral:
@@ -366,6 +367,7 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
       case NodeKind::ArrayInit:
       case NodeKind::Assign:
       case NodeKind::BinaryOperatorAdd:
+      case NodeKind::BindName:
       case NodeKind::BindValue:
       case NodeKind::BlockArg:
       case NodeKind::BoolLiteral:
@@ -482,6 +484,12 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
       case NodeKind::UnaryOperatorNot:
         return ExpressionCategory::Value;
 
+      case NodeKind::BindName: {
+        auto [name_id, value_id] = node.GetAsBindName();
+        node_id = value_id;
+        continue;
+      }
+
       case NodeKind::ArrayIndex: {
         auto [base_id, index_id] = node.GetAsArrayIndex();
         node_id = base_id;
@@ -549,6 +557,7 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
       case NodeKind::ArrayInit:
       case NodeKind::Assign:
       case NodeKind::BinaryOperatorAdd:
+      case NodeKind::BindName:
       case NodeKind::BindValue:
       case NodeKind::BlockArg:
       case NodeKind::BoolLiteral:

+ 5 - 0
toolchain/sem_ir/formatter.cpp

@@ -369,6 +369,11 @@ class NodeNamer {
           CollectNamesInBlock(scope_idx, block_id);
           break;
         }
+        case NodeKind::BindName: {
+          auto [name_id, value_id] = node.GetAsBindName();
+          add_node_name_id(name_id);
+          continue;
+        }
         case NodeKind::FunctionDeclaration: {
           add_node_name_id(
               semantics_ir_.GetFunction(node.GetAsFunctionDeclaration())

+ 3 - 0
toolchain/sem_ir/node.h

@@ -322,6 +322,9 @@ class Node : public Printable<Node> {
   using BinaryOperatorAdd = Node::Factory<NodeKind::BinaryOperatorAdd,
                                           NodeId /*lhs_id*/, NodeId /*rhs_id*/>;
 
+  using BindName =
+      Factory<NodeKind::BindName, StringId /*name_id*/, NodeId /*value_id*/>;
+
   using BindValue = Factory<NodeKind::BindValue, NodeId /*value_id*/>;
 
   using BlockArg = Factory<NodeKind::BlockArg, NodeBlockId /*block_id*/>;

+ 1 - 0
toolchain/sem_ir/node_kind.def

@@ -51,6 +51,7 @@ CARBON_SEMANTICS_NODE_KIND_IMPL(ArrayInit, "array_init", Typed, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(ArrayType, "array_type", Typed, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(Assign, "assign", None, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(BinaryOperatorAdd, "add", Typed, NotTerminator)
+CARBON_SEMANTICS_NODE_KIND_IMPL(BindName, "bind_name", Typed, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(BindValue, "bind_value", Typed, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(BlockArg, "block_arg", Typed, NotTerminator)
 CARBON_SEMANTICS_NODE_KIND_IMPL(BoolLiteral, "bool_literal", Typed,