瀏覽代碼

Split evaluation up into one function per instruction kind (#5008)

Replace the large and growing `TryEvalInstInContext` function with one
function per kind. While we still have special-case handling for a small
number of instruction kinds, most instructions are now handled either
fully automatically or use a common codepath that evaluates the
instruction operands and then performs an eval-context-independent
evaluation of the instruction.

To support this, `InstConstantKind` is expanded to describe more
fine-grained details about how each kind of instruction interacts with
constant evaluation. Also, the operand kinds of instructions become
slightly more fine-grained: we now distinguish between operands that
describe the destination of an initializing expression (`DestInstId`)
from other `InstId` operands, because `DestInstId` operands need
different treatment during constant evaluation. In particular, an
initializing expression can have a constant value even if its
destination is non-constant or has not yet been set, because evaluation
of an initializing expression doesn't include the store to the
destination.

Some minor test changes:

- We now more consistently propagate errors into the results of constant
evaluation, so more instructions that depend on errors have a constant
value of `<error>`.
- Diagnostic location for invalid array types now point at the whole
array type rather than the array index expression, because
`EvalConstantinst` doesn't have access to the original expression.
- Diagnostic for failed `RequireCompleteType` doesn't print the original
type any more because `EvalConstantInst` doesn't have access to the
original expression.

As a follow-up, some of this -- in particular, the `EvalConstantInst`
overloads -- will be moved to a separate file, in an effort to split the
overall constant evaluation machinery apart from the logic to evaluate
each individual kind of instruction.
Richard Smith 1 年之前
父節點
當前提交
0d2f364f39
共有 50 個文件被更改,包括 1036 次插入915 次删除
  1. 739 710
      toolchain/check/eval.cpp
  2. 2 2
      toolchain/check/testdata/array/fail_bound_negative.carbon
  3. 2 2
      toolchain/check/testdata/array/fail_bound_overflow.carbon
  4. 1 1
      toolchain/check/testdata/array/fail_incomplete_element.carbon
  5. 1 1
      toolchain/check/testdata/array/fail_out_of_bound_non_literal.carbon
  6. 3 3
      toolchain/check/testdata/builtin_conversions/no_prelude/fail_todo_convert_facet_value_to_narrowed_facet_type.carbon
  7. 2 2
      toolchain/check/testdata/builtin_conversions/value_with_type_through_access.carbon
  8. 2 2
      toolchain/check/testdata/choice/fail_invalid.carbon
  9. 2 2
      toolchain/check/testdata/class/fail_generic_method.carbon
  10. 6 6
      toolchain/check/testdata/class/fail_init.carbon
  11. 1 1
      toolchain/check/testdata/class/fail_self_param.carbon
  12. 2 2
      toolchain/check/testdata/class/no_prelude/import_access.carbon
  13. 2 2
      toolchain/check/testdata/class/no_prelude/name_poisoning.carbon
  14. 1 1
      toolchain/check/testdata/deduce/int_float.carbon
  15. 1 1
      toolchain/check/testdata/function/declaration/fail_param_in_type.carbon
  16. 1 1
      toolchain/check/testdata/function/definition/no_prelude/fail_decl_param_mismatch.carbon
  17. 2 2
      toolchain/check/testdata/generic/complete_type.carbon
  18. 3 4
      toolchain/check/testdata/impl/assoc_const_self.carbon
  19. 1 1
      toolchain/check/testdata/impl/fail_call_invalid.carbon
  20. 1 1
      toolchain/check/testdata/impl/fail_self_type_mismatch.carbon
  21. 1 1
      toolchain/check/testdata/impl/fail_todo_use_assoc_const.carbon
  22. 3 3
      toolchain/check/testdata/impl/no_prelude/name_poisoning.carbon
  23. 2 2
      toolchain/check/testdata/index/fail_array_large_index.carbon
  24. 1 1
      toolchain/check/testdata/index/fail_array_non_int_indexing.carbon
  25. 1 1
      toolchain/check/testdata/index/fail_array_out_of_bound_access.carbon
  26. 1 1
      toolchain/check/testdata/index/fail_negative_indexing.carbon
  27. 2 2
      toolchain/check/testdata/interface/no_prelude/fail_assoc_const_not_constant.carbon
  28. 7 7
      toolchain/check/testdata/interface/no_prelude/import_access.carbon
  29. 2 2
      toolchain/check/testdata/let/generic_import.carbon
  30. 1 1
      toolchain/check/testdata/operators/builtin/fail_assignment_to_error.carbon
  31. 4 4
      toolchain/check/testdata/packages/fail_import_type_error.carbon
  32. 1 1
      toolchain/check/testdata/packages/no_prelude/core_name_poisoning.carbon
  33. 2 2
      toolchain/check/testdata/pointer/fail_deref_error.carbon
  34. 2 2
      toolchain/check/testdata/pointer/fail_deref_function.carbon
  35. 2 2
      toolchain/check/testdata/pointer/fail_deref_namespace.carbon
  36. 6 6
      toolchain/check/testdata/pointer/fail_deref_not_pointer.carbon
  37. 2 2
      toolchain/check/testdata/pointer/fail_deref_type.carbon
  38. 1 1
      toolchain/check/testdata/struct/no_prelude/fail_nested_incomplete.carbon
  39. 1 1
      toolchain/check/testdata/tuple/fail_nested_incomplete.carbon
  40. 1 1
      toolchain/check/testdata/where_expr/designator.carbon
  41. 1 1
      toolchain/check/testdata/where_expr/equal_rewrite.carbon
  42. 2 2
      toolchain/check/testdata/where_expr/fail_not_facet.carbon
  43. 2 0
      toolchain/lower/constant.cpp
  44. 3 1
      toolchain/lower/function_context.cpp
  45. 4 0
      toolchain/sem_ir/formatter.cpp
  46. 1 1
      toolchain/sem_ir/id_kind.h
  47. 25 3
      toolchain/sem_ir/ids.h
  48. 2 1
      toolchain/sem_ir/inst.h
  49. 40 24
      toolchain/sem_ir/inst_kind.h
  50. 138 92
      toolchain/sem_ir/typed_insts.h

+ 739 - 710
toolchain/check/eval.cpp

@@ -16,6 +16,7 @@
 #include "toolchain/sem_ir/builtin_function_kind.h"
 #include "toolchain/sem_ir/function.h"
 #include "toolchain/sem_ir/generic.h"
+#include "toolchain/sem_ir/id_kind.h"
 #include "toolchain/sem_ir/ids.h"
 #include "toolchain/sem_ir/inst_kind.h"
 #include "toolchain/sem_ir/typed_insts.h"
@@ -223,15 +224,20 @@ enum class Phase : uint8_t {
 };
 }  // namespace
 
+// Returns whether the specified phase is a constant phase.
+static auto IsConstant(Phase phase) -> bool {
+  return phase < Phase::UnknownDueToError;
+}
+
 // Gets the phase in which the value of a constant will become available.
-static auto GetPhase(EvalContext& eval_context, SemIR::ConstantId constant_id)
-    -> Phase {
+static auto GetPhase(const SemIR::ConstantValueStore& constant_values,
+                     SemIR::ConstantId constant_id) -> Phase {
   if (!constant_id.is_constant()) {
     return Phase::Runtime;
   } else if (constant_id == SemIR::ErrorInst::SingletonConstantId) {
     return Phase::UnknownDueToError;
   }
-  switch (eval_context.constant_values().GetDependence(constant_id)) {
+  switch (constant_values.GetDependence(constant_id)) {
     case SemIR::ConstantDependence::None:
       return Phase::Concrete;
     case SemIR::ConstantDependence::PeriodSelf:
@@ -256,7 +262,7 @@ static auto LatestPhase(Phase a, Phase b) -> Phase {
 static auto UpdatePhaseIgnorePeriodSelf(EvalContext& eval_context,
                                         SemIR::ConstantId constant_id,
                                         Phase* phase) {
-  Phase constant_phase = GetPhase(eval_context, constant_id);
+  Phase constant_phase = GetPhase(eval_context.constant_values(), constant_id);
   // Since LatestPhase(x, Phase::Concrete) == x, this is equivalent to replacing
   // Phase::PeriodSelfSymbolic with Phase::Concrete.
   if (constant_phase != Phase::PeriodSelfSymbolic) {
@@ -333,16 +339,26 @@ static auto MakeFloatResult(Context& context, SemIR::TypeId type_id,
 static auto GetConstantValue(EvalContext& eval_context, SemIR::InstId inst_id,
                              Phase* phase) -> SemIR::InstId {
   auto const_id = eval_context.GetConstantValue(inst_id);
-  *phase = LatestPhase(*phase, GetPhase(eval_context, const_id));
+  *phase =
+      LatestPhase(*phase, GetPhase(eval_context.constant_values(), const_id));
   return eval_context.constant_values().GetInstId(const_id);
 }
 
+// Explicitly discard a `DestInstId`, because we should not be using the
+// destination as part of evaluation.
+static auto GetConstantValue(EvalContext& /*eval_context*/,
+                             SemIR::DestInstId /*inst_id*/, Phase* /*phase*/)
+    -> SemIR::DestInstId {
+  return SemIR::InstId::None;
+}
+
 // Given a type which may refer to a generic parameter, returns the
 // corresponding type in the evaluation context.
 static auto GetConstantValue(EvalContext& eval_context, SemIR::TypeId type_id,
                              Phase* phase) -> SemIR::TypeId {
   auto const_id = eval_context.GetConstantValue(type_id);
-  *phase = LatestPhase(*phase, GetPhase(eval_context, const_id));
+  *phase =
+      LatestPhase(*phase, GetPhase(eval_context.constant_values(), const_id));
   return eval_context.context().types().GetTypeIdForTypeConstantId(const_id);
 }
 
@@ -505,6 +521,16 @@ static auto GetConstantFacetTypeInfo(EvalContext& eval_context,
   return info;
 }
 
+static auto GetConstantValue(EvalContext& eval_context,
+                             SemIR::FacetTypeId facet_type_id, Phase* phase)
+    -> SemIR::FacetTypeId {
+  SemIR::FacetTypeInfo info =
+      GetConstantFacetTypeInfo(eval_context, facet_type_id, phase);
+  info.Canonicalize();
+  // TODO: Return `facet_type_id` if we can detect nothing has changed.
+  return eval_context.facet_types().Add(info);
+}
+
 // Replaces the specified field of the given typed instruction with its constant
 // value, if it has constant phase. Returns true on success, false if the value
 // has runtime phase.
@@ -520,112 +546,81 @@ static auto ReplaceFieldWithConstantValue(EvalContext& eval_context,
   return true;
 }
 
-// If the specified fields of the given typed instruction have constant values,
-// replaces the fields with their constant values and builds a corresponding
-// constant value. Otherwise returns `ConstantId::NotConstant`. Returns
-// `ErrorInst::SingletonConstantId` if any subexpression is an error.
-//
-// The constant value is then checked by calling `validate_fn(typed_inst)`,
-// which should return a `bool` indicating whether the new constant is valid. If
-// validation passes, `transform_fn(typed_inst)` is called to produce the final
-// constant instruction, and a corresponding ConstantId for the new constant is
-// returned. If validation fails, it should produce a suitable error message.
-// `ErrorInst::SingletonConstantId` is returned.
-template <typename InstT, typename ValidateFn, typename TransformFn,
-          typename... EachFieldIdT>
-static auto RebuildIfFieldsAreConstantImpl(
-    EvalContext& eval_context, SemIR::Inst inst, ValidateFn validate_fn,
-    TransformFn transform_fn, EachFieldIdT InstT::*... each_field_id)
-    -> SemIR::ConstantId {
-  // Build a constant instruction by replacing each non-constant operand with
-  // its constant value.
-  auto typed_inst = inst.As<InstT>();
-  Phase phase = Phase::Concrete;
-  if ((ReplaceFieldWithConstantValue(eval_context, &typed_inst, each_field_id,
-                                     &phase) &&
-       ...)) {
-    if (phase == Phase::UnknownDueToError || !validate_fn(typed_inst)) {
-      return SemIR::ErrorInst::SingletonConstantId;
-    }
-    return MakeConstantResult(eval_context.context(), transform_fn(typed_inst),
-                              phase);
-  }
-  return MakeNonConstantResult(phase);
-}
-
-// Same as above but with an identity transform function.
-template <typename InstT, typename ValidateFn, typename... EachFieldIdT>
-static auto RebuildAndValidateIfFieldsAreConstant(
-    EvalContext& eval_context, SemIR::Inst inst, ValidateFn validate_fn,
-    EachFieldIdT InstT::*... each_field_id) -> SemIR::ConstantId {
-  return RebuildIfFieldsAreConstantImpl(eval_context, inst, validate_fn,
-                                        std::identity{}, each_field_id...);
-}
+// Function template that can be called with an argument of type `T`. Used below
+// to detect which overloads of `GetConstantValue` exist.
+template <typename T>
+static void Accept(T /*arg*/) {}
+
+// Determines whether a `GetConstantValue` overload exists for a given ID type.
+// Note that we do not check whether `GetConstantValue` is *callable* with a
+// given ID type, because that would use the `InstId` overload for
+// `AbsoluteInstId` and similar wrapper types, which should be left alone.
+template <typename IdT>
+static constexpr bool HasGetConstantValueOverload = requires {
+  Accept<auto (*)(EvalContext&, IdT, Phase*)->IdT>(GetConstantValue);
+};
 
-// Same as above but with no validation step.
-template <typename InstT, typename TransformFn, typename... EachFieldIdT>
-static auto TransformIfFieldsAreConstant(EvalContext& eval_context,
-                                         SemIR::Inst inst,
-                                         TransformFn transform_fn,
-                                         EachFieldIdT InstT::*... each_field_id)
-    -> SemIR::ConstantId {
-  return RebuildIfFieldsAreConstantImpl(
-      eval_context, inst, [](...) { return true; }, transform_fn,
-      each_field_id...);
+// Given the stored value `arg` of an instruction field and its corresponding
+// kind `kind`, returns the constant value to use for that field, if it has a
+// constant phase. `*phase` is updated to include the new constant value. If
+// the resulting phase is not constant, the returned value is not useful and
+// will typically be `NoneIndex`.
+template <typename... Type>
+static auto GetConstantValueForArg(EvalContext& eval_context,
+                                   SemIR::TypeEnum<Type...> kind, int32_t arg,
+                                   Phase* phase) -> int32_t {
+  using Handler = auto(EvalContext&, int32_t arg, Phase * phase)->int32_t;
+  static constexpr Handler* Handlers[] = {
+      [](EvalContext& eval_context, int32_t arg, Phase* phase) -> int32_t {
+        auto id = SemIR::Inst::FromRaw<Type>(arg);
+        if constexpr (HasGetConstantValueOverload<Type>) {
+          // If we have a custom `GetConstantValue` overload, call it.
+          return SemIR::Inst::ToRaw(GetConstantValue(eval_context, id, phase));
+        } else {
+          // Otherwise, we assume the value is already constant.
+          return arg;
+        }
+      }...,
+      [](EvalContext&, int32_t, Phase*) -> int32_t {
+        // Handler for IdKind::Invalid is next.
+        CARBON_FATAL("Instruction has argument with invalid IdKind");
+      },
+      [](EvalContext&, int32_t arg, Phase*) -> int32_t {
+        // Handler for IdKind::None is last.
+        return arg;
+      }};
+  return Handlers[kind.ToIndex()](eval_context, arg, phase);
 }
 
-// Same as above but with no validation or transform step.
-template <typename InstT, typename... EachFieldIdT>
-static auto RebuildIfFieldsAreConstant(EvalContext& eval_context,
-                                       SemIR::Inst inst,
-                                       EachFieldIdT InstT::*... each_field_id)
-    -> SemIR::ConstantId {
-  return RebuildIfFieldsAreConstantImpl(
-      eval_context, inst, [](...) { return true; }, std::identity{},
-      each_field_id...);
-}
+// Given an instruction, replaces its type and operands with their constant
+// values from the specified evaluation context. `*phase` is updated to describe
+// the constant phase of the result. Returns whether `*phase` is a constant
+// phase; if not, `inst` may not be fully updated and should not be used.
+static auto ReplaceAllFieldsWithConstantValues(EvalContext& eval_context,
+                                               SemIR::Inst* inst, Phase* phase)
+    -> bool {
+  auto type_id = SemIR::TypeId(
+      GetConstantValueForArg(eval_context, SemIR::IdKind::For<SemIR::TypeId>,
+                             inst->type_id().index, phase));
+  inst->SetType(type_id);
+  if (!IsConstant(*phase)) {
+    return false;
+  }
 
-// Rebuilds the given aggregate initialization instruction as a corresponding
-// constant aggregate value, if its elements are all constants.
-static auto RebuildInitAsValue(EvalContext& eval_context, SemIR::Inst inst,
-                               SemIR::InstKind value_kind)
-    -> SemIR::ConstantId {
-  return TransformIfFieldsAreConstant(
-      eval_context, inst,
-      [&](SemIR::AnyAggregateInit result) {
-        return SemIR::AnyAggregateValue{.kind = value_kind,
-                                        .type_id = result.type_id,
-                                        .elements_id = result.elements_id};
-      },
-      &SemIR::AnyAggregateInit::type_id, &SemIR::AnyAggregateInit::elements_id);
-}
+  auto kinds = inst->ArgKinds();
+  auto arg0 =
+      GetConstantValueForArg(eval_context, kinds.first, inst->arg0(), phase);
+  if (!IsConstant(*phase)) {
+    return false;
+  }
 
-// Performs an access into an aggregate, retrieving the specified element.
-static auto PerformAggregateAccess(EvalContext& eval_context, SemIR::Inst inst)
-    -> SemIR::ConstantId {
-  auto access_inst = inst.As<SemIR::AnyAggregateAccess>();
-  Phase phase = Phase::Concrete;
-  if (ReplaceFieldWithConstantValue(eval_context, &access_inst,
-                                    &SemIR::AnyAggregateAccess::aggregate_id,
-                                    &phase)) {
-    if (auto aggregate =
-            eval_context.insts().TryGetAs<SemIR::AnyAggregateValue>(
-                access_inst.aggregate_id)) {
-      auto elements = eval_context.inst_blocks().Get(aggregate->elements_id);
-      auto index = static_cast<size_t>(access_inst.index.index);
-      CARBON_CHECK(index < elements.size(), "Access out of bounds.");
-      // `Phase` is not used here. If this element is a concrete constant, then
-      // so is the result of indexing, even if the aggregate also contains a
-      // symbolic context.
-      return eval_context.GetConstantValue(elements[index]);
-    } else {
-      CARBON_CHECK(phase != Phase::Concrete,
-                   "Failed to evaluate template constant {0} arg0: {1}", inst,
-                   eval_context.insts().Get(access_inst.aggregate_id));
-    }
-    return MakeConstantResult(eval_context.context(), access_inst, phase);
+  auto arg1 =
+      GetConstantValueForArg(eval_context, kinds.second, inst->arg1(), phase);
+  if (!IsConstant(*phase)) {
+    return false;
   }
-  return MakeNonConstantResult(phase);
+  inst->SetArgs(arg0, arg1);
+  return true;
 }
 
 // Performs an index into a homogeneous aggregate, retrieving the specified
@@ -1549,641 +1544,675 @@ static auto MakeFacetTypeResult(Context& context,
       phase);
 }
 
-// Implementation for `TryEvalInst`, wrapping `Context` with `EvalContext`.
+// The result of constant evaluation of an instruction.
+class ConstantEvalResult {
+ public:
+  // Produce a new constant as the result of an evaluation. The phase of the
+  // produced constant must be the same as the greatest phase of the operands in
+  // the evaluation. This will typically be the case if the evaluation uses all
+  // of its operands.
+  static auto New(SemIR::Inst inst) -> ConstantEvalResult {
+    return ConstantEvalResult(inst);
+  }
+
+  // Produce an existing constant as the result of an evaluation.
+  static constexpr auto Existing(SemIR::ConstantId existing_id)
+      -> ConstantEvalResult {
+    CARBON_CHECK(existing_id.is_constant());
+    return ConstantEvalResult(existing_id);
+  }
+
+  // Indicates that an error was produced by evaluation.
+  static const ConstantEvalResult Error;
+
+  // Indicates that we encountered an instruction whose evaluation is
+  // non-constant despite having constant operands. This should be rare;
+  // usually we want to produce an error in this case.
+  static const ConstantEvalResult NotConstant;
+
+  // Indicates that we encountered an instruction for which we've not
+  // implemented constant evaluation yet. Instruction is treated as not
+  // constant.
+  static const ConstantEvalResult TODO;
+
+  // Returns whether the result of evaluation is that we should produce a new
+  // constant described by `new_inst()` rather than an existing `ConstantId`
+  // described by `existing()`.
+  auto is_new() const -> bool { return !result_id_.has_value(); }
+
+  // Returns the existing constant that this the instruction evaluates to, or
+  // `None` if this is evaluation produces a new constant.
+  auto existing() const -> SemIR::ConstantId { return result_id_; }
+
+  // Returns the new constant instruction that is the result of evaluation.
+  auto new_inst() const -> SemIR::Inst {
+    CARBON_CHECK(is_new());
+    return new_inst_;
+  }
+
+ private:
+  constexpr explicit ConstantEvalResult(SemIR::ConstantId raw_id)
+      : result_id_(raw_id) {}
+
+  explicit ConstantEvalResult(SemIR::Inst inst)
+      : result_id_(SemIR::ConstantId::None), new_inst_(inst) {}
+
+  SemIR::ConstantId result_id_;
+  union {
+    SemIR::Inst new_inst_;
+  };
+};
+
+constexpr ConstantEvalResult ConstantEvalResult::Error =
+    Existing(SemIR::ErrorInst::SingletonConstantId);
+
+constexpr ConstantEvalResult ConstantEvalResult::NotConstant =
+    ConstantEvalResult(SemIR::ConstantId::NotConstant);
+
+constexpr ConstantEvalResult ConstantEvalResult::TODO = NotConstant;
+
+// `EvalConstantInst` evaluates an instruction whose operands are all constant,
+// in a context unrelated to the enclosing evaluation. The function is given the
+// instruction after its operands, including its type, are replaced by their
+// evaluated value, and returns a `ConstantEvalResult` describing the result of
+// evaluating the instruction.
 //
-// Tail call should not be diagnosed as recursion.
-// https://github.com/llvm/llvm-project/issues/125724
-// NOLINTNEXTLINE(misc-no-recursion): Tail call.
-static auto TryEvalInstInContext(EvalContext& eval_context,
-                                 SemIR::InstId inst_id, SemIR::Inst inst)
-    -> SemIR::ConstantId {
-  // TODO: Ensure we have test coverage for each of these cases that can result
-  // in a constant, once those situations are all reachable.
-  CARBON_KIND_SWITCH(inst) {
-    // These cases are constants if their operands are.
-    case SemIR::AddrOf::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::AddrOf::type_id,
-                                        &SemIR::AddrOf::lvalue_id);
-    case CARBON_KIND(SemIR::ArrayType array_type): {
-      return RebuildAndValidateIfFieldsAreConstant(
-          eval_context, inst,
-          [&](SemIR::ArrayType result) {
-            auto bound_id = array_type.bound_id;
-            auto bound_inst = eval_context.insts().Get(result.bound_id);
-            auto int_bound = bound_inst.TryAs<SemIR::IntValue>();
-            if (!int_bound) {
-              CARBON_CHECK(eval_context.constant_values()
-                               .Get(result.bound_id)
-                               .is_symbolic(),
-                           "Unexpected inst {0} for template constant int",
-                           bound_inst);
-              return true;
-            }
-            // TODO: We should check that the size of the resulting array type
-            // fits in 64 bits, not just that the bound does. Should we use a
-            // 32-bit limit for 32-bit targets?
-            const auto& bound_val = eval_context.ints().Get(int_bound->int_id);
-            if (eval_context.types().IsSignedInt(int_bound->type_id) &&
-                bound_val.isNegative()) {
-              CARBON_DIAGNOSTIC(ArrayBoundNegative, Error,
-                                "array bound of {0} is negative", TypedInt);
-              eval_context.emitter().Emit(
-                  eval_context.GetDiagnosticLoc(bound_id), ArrayBoundNegative,
-                  {.type = int_bound->type_id, .value = bound_val});
-              return false;
-            }
-            if (bound_val.getActiveBits() > 64) {
-              CARBON_DIAGNOSTIC(ArrayBoundTooLarge, Error,
-                                "array bound of {0} is too large", TypedInt);
-              eval_context.emitter().Emit(
-                  eval_context.GetDiagnosticLoc(bound_id), ArrayBoundTooLarge,
-                  {.type = int_bound->type_id, .value = bound_val});
-              return false;
-            }
-            return true;
-          },
-          &SemIR::ArrayType::bound_id, &SemIR::ArrayType::element_type_id);
-    }
-    case SemIR::AssociatedEntity::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::AssociatedEntity::type_id);
-    case SemIR::AssociatedEntityType::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst, &SemIR::AssociatedEntityType::interface_type_id);
-    case SemIR::BoundMethod::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::BoundMethod::type_id,
-                                        &SemIR::BoundMethod::object_id,
-                                        &SemIR::BoundMethod::function_decl_id);
-    case SemIR::ClassType::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::ClassType::specific_id);
-    case SemIR::CompleteTypeWitness::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst, &SemIR::CompleteTypeWitness::object_repr_id);
-    case SemIR::FacetValue::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::FacetValue::type_id,
-                                        &SemIR::FacetValue::type_inst_id,
-                                        &SemIR::FacetValue::witness_inst_id);
-    case SemIR::FunctionType::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::FunctionType::specific_id);
-    case SemIR::FunctionTypeWithSelfType::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst,
-          &SemIR::FunctionTypeWithSelfType::interface_function_type_id,
-          &SemIR::FunctionTypeWithSelfType::self_id);
-    case SemIR::GenericClassType::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst, &SemIR::GenericClassType::enclosing_specific_id);
-    case SemIR::GenericInterfaceType::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst,
-          &SemIR::GenericInterfaceType::enclosing_specific_id);
-    case SemIR::ImplWitness::Kind:
-      // We intentionally don't replace the `elements_id` field here. We want to
-      // track that specific InstBlock in particular, not coalesce blocks with
-      // the same members. That block may get updated, and we want to pick up
-      // those changes.
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::ImplWitness::specific_id);
-    case CARBON_KIND(SemIR::IntType int_type): {
-      return RebuildAndValidateIfFieldsAreConstant(
-          eval_context, inst,
-          [&](SemIR::IntType result) {
-            return ValidateIntType(
-                eval_context.context(),
-                eval_context.GetDiagnosticLoc({inst_id, int_type.bit_width_id}),
-                result);
-          },
-          &SemIR::IntType::bit_width_id);
-    }
-    case SemIR::PointerType::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::PointerType::pointee_id);
-    case CARBON_KIND(SemIR::FloatType float_type): {
-      return RebuildAndValidateIfFieldsAreConstant(
-          eval_context, inst,
-          [&](SemIR::FloatType result) {
-            return ValidateFloatType(eval_context.context(),
-                                     eval_context.GetDiagnosticLoc(
-                                         {inst_id, float_type.bit_width_id}),
-                                     result);
-          },
-          &SemIR::FloatType::bit_width_id);
-    }
-    case SemIR::SpecificFunction::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::SpecificFunction::callee_id,
-                                        &SemIR::SpecificFunction::specific_id);
-    case SemIR::StructType::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::StructType::fields_id);
-    case SemIR::StructValue::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::StructValue::type_id,
-                                        &SemIR::StructValue::elements_id);
-    case SemIR::TupleType::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::TupleType::elements_id);
-    case SemIR::TupleValue::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::TupleValue::type_id,
-                                        &SemIR::TupleValue::elements_id);
-    case SemIR::UnboundElementType::Kind:
-      return RebuildIfFieldsAreConstant(
-          eval_context, inst, &SemIR::UnboundElementType::class_type_id,
-          &SemIR::UnboundElementType::element_type_id);
-
-    // Initializers evaluate to a value of the object representation.
-    case SemIR::ArrayInit::Kind:
-      // TODO: Add an `ArrayValue` to represent a constant array object
-      // representation instead of using a `TupleValue`.
-      return RebuildInitAsValue(eval_context, inst, SemIR::TupleValue::Kind);
-    case SemIR::ClassInit::Kind:
-      // TODO: Add a `ClassValue` to represent a constant class object
-      // representation instead of using a `StructValue`.
-      return RebuildInitAsValue(eval_context, inst, SemIR::StructValue::Kind);
-    case SemIR::StructInit::Kind:
-      return RebuildInitAsValue(eval_context, inst, SemIR::StructValue::Kind);
-    case SemIR::TupleInit::Kind:
-      return RebuildInitAsValue(eval_context, inst, SemIR::TupleValue::Kind);
-
-    case SemIR::Vtable::Kind:
-      return RebuildIfFieldsAreConstant(eval_context, inst,
-                                        &SemIR::Vtable::virtual_functions_id);
-    case SemIR::AutoType::Kind:
-    case SemIR::BoolType::Kind:
-    case SemIR::BoundMethodType::Kind:
-    case SemIR::ErrorInst::Kind:
-    case SemIR::IntLiteralType::Kind:
-    case SemIR::LegacyFloatType::Kind:
-    case SemIR::NamespaceType::Kind:
-    case SemIR::SpecificFunctionType::Kind:
-    case SemIR::StringType::Kind:
-    case SemIR::TypeType::Kind:
-    case SemIR::VtableType::Kind:
-    case SemIR::WitnessType::Kind:
-      // Builtins are always concrete constants.
-      return MakeConstantResult(eval_context.context(), inst, Phase::Concrete);
-
-    case CARBON_KIND(SemIR::FunctionDecl fn_decl): {
-      return TransformIfFieldsAreConstant(
-          eval_context, fn_decl,
-          [&](SemIR::FunctionDecl result) {
-            return SemIR::StructValue{.type_id = result.type_id,
-                                      .elements_id = SemIR::InstBlockId::Empty};
-          },
-          &SemIR::FunctionDecl::type_id);
-    }
+// An overload is provided for each type whose constant kind is one of the
+// following:
+//
+// - InstConstantKind::Indirect
+// - InstConstantKind::SymbolicOnly
+// - InstConstantKind::Conditional
+//
+// ... except for cases where the result of evaluation depends on the evaluation
+// context itself. Those cases are handled by explicit specialization of
+// `TryEvalTypedInst`.
+
+static auto EvalConstantInst(Context& context, SemIRLoc loc,
+                             SemIR::ArrayType inst) -> ConstantEvalResult {
+  auto bound_inst = context.insts().Get(inst.bound_id);
+  auto int_bound = bound_inst.TryAs<SemIR::IntValue>();
+  if (!int_bound) {
+    CARBON_CHECK(context.constant_values().Get(inst.bound_id).is_symbolic(),
+                 "Unexpected inst {0} for template constant int", bound_inst);
+    return ConstantEvalResult::New(inst);
+  }
+  // TODO: We should check that the size of the resulting array type
+  // fits in 64 bits, not just that the bound does. Should we use a
+  // 32-bit limit for 32-bit targets?
+  const auto& bound_val = context.ints().Get(int_bound->int_id);
+  if (context.types().IsSignedInt(int_bound->type_id) &&
+      bound_val.isNegative()) {
+    CARBON_DIAGNOSTIC(ArrayBoundNegative, Error,
+                      "array bound of {0} is negative", TypedInt);
+    context.emitter().Emit(loc, ArrayBoundNegative,
+                           {.type = int_bound->type_id, .value = bound_val});
+    return ConstantEvalResult::Error;
+  }
+  if (bound_val.getActiveBits() > 64) {
+    CARBON_DIAGNOSTIC(ArrayBoundTooLarge, Error,
+                      "array bound of {0} is too large", TypedInt);
+    context.emitter().Emit(loc, ArrayBoundTooLarge,
+                           {.type = int_bound->type_id, .value = bound_val});
+    return ConstantEvalResult::Error;
+  }
+  return ConstantEvalResult::New(inst);
+}
 
-    case CARBON_KIND(SemIR::ClassDecl class_decl): {
-      // If the class has generic parameters, we don't produce a class type, but
-      // a callable whose return value is a class type.
-      if (eval_context.classes().Get(class_decl.class_id).has_parameters()) {
-        return TransformIfFieldsAreConstant(
-            eval_context, class_decl,
-            [&](SemIR::ClassDecl result) {
-              return SemIR::StructValue{
-                  .type_id = result.type_id,
-                  .elements_id = SemIR::InstBlockId::Empty};
-            },
-            &SemIR::ClassDecl::type_id);
-      }
-      // A non-generic class declaration evaluates to the class type.
-      return MakeConstantResult(
-          eval_context.context(),
-          SemIR::ClassType{.type_id = SemIR::TypeType::SingletonTypeId,
-                           .class_id = class_decl.class_id,
-                           .specific_id = SemIR::SpecificId::None},
-          Phase::Concrete);
-    }
+static auto EvalConstantInst(Context& context, SemIRLoc loc,
+                             SemIR::IntType inst) -> ConstantEvalResult {
+  return ValidateIntType(context, loc, inst) ? ConstantEvalResult::New(inst)
+                                             : ConstantEvalResult::Error;
+}
 
-    case CARBON_KIND(SemIR::FacetType facet_type): {
-      Phase phase = Phase::Concrete;
-      SemIR::FacetTypeInfo info = GetConstantFacetTypeInfo(
-          eval_context, facet_type.facet_type_id, &phase);
-      info.Canonicalize();
-      // TODO: Reuse `inst` if we can detect that nothing has changed.
-      return MakeFacetTypeResult(eval_context.context(), info, phase);
-    }
+static auto EvalConstantInst(Context& context, SemIRLoc loc,
+                             SemIR::FloatType inst) -> ConstantEvalResult {
+  return ValidateFloatType(context, loc, inst) ? ConstantEvalResult::New(inst)
+                                               : ConstantEvalResult::Error;
+}
 
-    case CARBON_KIND(SemIR::InterfaceDecl interface_decl): {
-      // If the interface has generic parameters, we don't produce an interface
-      // type, but a callable whose return value is an interface type.
-      if (eval_context.interfaces()
-              .Get(interface_decl.interface_id)
-              .has_parameters()) {
-        return TransformIfFieldsAreConstant(
-            eval_context, interface_decl,
-            [&](SemIR::InterfaceDecl result) {
-              return SemIR::StructValue{
-                  .type_id = result.type_id,
-                  .elements_id = SemIR::InstBlockId::Empty};
-            },
-            &SemIR::InterfaceDecl::type_id);
-      }
-      // A non-generic interface declaration evaluates to a facet type.
-      return MakeConstantResult(
-          eval_context.context(),
-          FacetTypeFromInterface(eval_context.context(),
-                                 interface_decl.interface_id,
-                                 SemIR::SpecificId::None),
-          Phase::Concrete);
-    }
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::ArrayInit init) -> ConstantEvalResult {
+  // TODO: Add an `ArrayValue` to represent a constant array object
+  // representation instead of using a `TupleValue`.
+  return ConstantEvalResult::New(
+      SemIR::TupleValue{.type_id = init.type_id, .elements_id = init.inits_id});
+}
 
-    case CARBON_KIND(SemIR::SpecificConstant specific): {
-      // Pull the constant value out of the specific.
-      return SemIR::GetConstantValueInSpecific(
-          eval_context.sem_ir(), specific.specific_id, specific.inst_id);
-    }
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::ClassInit init) -> ConstantEvalResult {
+  // TODO: Add a `ClassValue` to represent a constant class object
+  // representation instead of using a `StructValue`.
+  return ConstantEvalResult::New(SemIR::StructValue{
+      .type_id = init.type_id, .elements_id = init.elements_id});
+}
 
-    // These cases are treated as being the unique canonical definition of the
-    // corresponding constant value.
-    // TODO: This doesn't properly handle redeclarations. Consider adding a
-    // corresponding `Value` inst for each of these cases, or returning the
-    // first declaration.
-    case SemIR::AdaptDecl::Kind:
-    case SemIR::AssociatedConstantDecl::Kind:
-    case SemIR::BaseDecl::Kind:
-    case SemIR::FieldDecl::Kind:
-    case SemIR::ImplDecl::Kind:
-    case SemIR::Namespace::Kind:
-      return SemIR::ConstantId::ForConcreteConstant(inst_id);
-
-    case SemIR::BoolLiteral::Kind:
-    case SemIR::FloatLiteral::Kind:
-    case SemIR::IntValue::Kind:
-    case SemIR::StringLiteral::Kind:
-      // Promote literals to the constant block.
-      // TODO: Convert literals into a canonical form. Currently we can form two
-      // different `i32` constants with the same value if they are represented
-      // by `APInt`s with different bit widths.
-      // TODO: Can the type of an IntValue or FloatLiteral be symbolic? If so,
-      // we may need to rebuild.
-      return MakeConstantResult(eval_context.context(), inst, Phase::Concrete);
-
-    // The elements of a constant aggregate can be accessed.
-    case SemIR::ClassElementAccess::Kind:
-    case SemIR::StructAccess::Kind:
-    case SemIR::TupleAccess::Kind:
-      return PerformAggregateAccess(eval_context, inst);
-
-    case CARBON_KIND(SemIR::ImplWitnessAccess access_inst): {
-      // This is PerformAggregateAccess followed by GetConstantInSpecific.
-      Phase phase = Phase::Concrete;
-      if (ReplaceFieldWithConstantValue(eval_context, &access_inst,
-                                        &SemIR::ImplWitnessAccess::witness_id,
-                                        &phase)) {
-        if (auto witness = eval_context.insts().TryGetAs<SemIR::ImplWitness>(
-                access_inst.witness_id)) {
-          auto elements = eval_context.inst_blocks().Get(witness->elements_id);
-          auto index = static_cast<size_t>(access_inst.index.index);
-          CARBON_CHECK(index < elements.size(), "Access out of bounds.");
-          // `Phase` is not used here. If this element is a concrete constant,
-          // then so is the result of indexing, even if the aggregate also
-          // contains a symbolic context.
-
-          auto element = elements[index];
-          if (!element.has_value()) {
-            // TODO: Perhaps this should be a `{}` value with incomplete type?
-            CARBON_DIAGNOSTIC(ImplAccessMemberBeforeComplete, Error,
-                              "accessing member from impl before the end of "
-                              "its definition");
-            // TODO: Add note pointing to the impl declaration.
-            eval_context.emitter().Emit(eval_context.GetDiagnosticLoc(inst_id),
-                                        ImplAccessMemberBeforeComplete);
-            return SemIR::ErrorInst::SingletonConstantId;
-          }
-          LoadImportRef(eval_context.context(), element);
-          return GetConstantValueInSpecific(eval_context.sem_ir(),
-                                            witness->specific_id, element);
-        } else {
-          CARBON_CHECK(phase != Phase::Concrete,
-                       "Failed to evaluate template constant {0} arg0: {1}",
-                       inst, eval_context.insts().Get(access_inst.witness_id));
-        }
-        return MakeConstantResult(eval_context.context(), access_inst, phase);
-      }
-      return MakeNonConstantResult(phase);
-    }
-    case CARBON_KIND(SemIR::ArrayIndex index): {
-      return PerformArrayIndex(eval_context, index);
-    }
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::StructInit init) -> ConstantEvalResult {
+  return ConstantEvalResult::New(SemIR::StructValue{
+      .type_id = init.type_id, .elements_id = init.elements_id});
+}
 
-    case CARBON_KIND(SemIR::Call call): {
-      return MakeConstantForCall(eval_context,
-                                 eval_context.GetDiagnosticLoc(inst_id), call);
-    }
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::TupleInit init) -> ConstantEvalResult {
+  return ConstantEvalResult::New(SemIR::TupleValue{
+      .type_id = init.type_id, .elements_id = init.elements_id});
+}
 
-    // TODO: These need special handling.
-    case SemIR::BindValue::Kind:
-    case SemIR::Deref::Kind:
-    case SemIR::ImportRefLoaded::Kind:
-    case SemIR::ReturnSlot::Kind:
-    case SemIR::Temporary::Kind:
-    case SemIR::TemporaryStorage::Kind:
-    case SemIR::ValueAsRef::Kind:
-    case SemIR::VtablePtr::Kind:
-      break;
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::FunctionDecl inst) -> ConstantEvalResult {
+  return ConstantEvalResult::New(SemIR::StructValue{
+      .type_id = inst.type_id, .elements_id = SemIR::InstBlockId::Empty});
+}
 
-    case CARBON_KIND(SemIR::SymbolicBindingPattern bind): {
-      // TODO: Disable constant evaluation of SymbolicBindingPattern once
-      // DeduceGenericCallArguments no longer needs implicit params to have
-      // constant values.
-      const auto& bind_name =
-          eval_context.entity_names().Get(bind.entity_name_id);
-
-      // If we know which specific we're evaluating within and this is an
-      // argument of that specific, its constant value is the corresponding
-      // argument value.
-      if (auto value =
-              eval_context.GetCompileTimeBindValue(bind_name.bind_index());
-          value.has_value()) {
-        return value;
-      }
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ClassDecl inst) -> ConstantEvalResult {
+  // If the class has generic parameters, we don't produce a class type, but a
+  // callable whose return value is a class type.
+  if (context.classes().Get(inst.class_id).has_parameters()) {
+    return ConstantEvalResult::New(SemIR::StructValue{
+        .type_id = inst.type_id, .elements_id = SemIR::InstBlockId::Empty});
+  }
 
-      // The constant form of a symbolic binding is an idealized form of the
-      // original, with no equivalent value.
-      bind.entity_name_id =
-          eval_context.entity_names().MakeCanonical(bind.entity_name_id);
-      return MakeConstantResult(eval_context.context(), bind,
-                                bind_name.is_template ? Phase::TemplateSymbolic
-                                                      : Phase::CheckedSymbolic);
-    }
-    case CARBON_KIND(SemIR::BindSymbolicName bind): {
-      const auto& bind_name =
-          eval_context.entity_names().Get(bind.entity_name_id);
+  // A non-generic class declaration evaluates to the class type.
+  return ConstantEvalResult::New(
+      SemIR::ClassType{.type_id = SemIR::TypeType::SingletonTypeId,
+                       .class_id = inst.class_id,
+                       .specific_id = SemIR::SpecificId::None});
+}
 
-      Phase phase;
-      if (bind_name.name_id == SemIR::NameId::PeriodSelf) {
-        phase = Phase::PeriodSelfSymbolic;
-      } else {
-        // If we know which specific we're evaluating within and this is an
-        // argument of that specific, its constant value is the corresponding
-        // argument value.
-        if (auto value =
-                eval_context.GetCompileTimeBindValue(bind_name.bind_index());
-            value.has_value()) {
-          return value;
-        }
-        phase = bind_name.is_template ? Phase::TemplateSymbolic
-                                      : Phase::CheckedSymbolic;
-      }
-      // The constant form of a symbolic binding is an idealized form of the
-      // original, with no equivalent value.
-      bind.entity_name_id =
-          eval_context.entity_names().MakeCanonical(bind.entity_name_id);
-      bind.value_id = SemIR::InstId::None;
-      if (!ReplaceFieldWithConstantValue(
-              eval_context, &bind, &SemIR::BindSymbolicName::type_id, &phase)) {
-        return MakeNonConstantResult(phase);
-      }
-      return MakeConstantResult(eval_context.context(), bind, phase);
-    }
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::InterfaceDecl inst) -> ConstantEvalResult {
+  // If the interface has generic parameters, we don't produce an interface
+  // type, but a callable whose return value is an interface type.
+  if (context.interfaces().Get(inst.interface_id).has_parameters()) {
+    return ConstantEvalResult::New(SemIR::StructValue{
+        .type_id = inst.type_id, .elements_id = SemIR::InstBlockId::Empty});
+  }
 
-    // AsCompatible changes the type of the source instruction; its constant
-    // value, if there is one, needs to be modified to be of the same type.
-    case CARBON_KIND(SemIR::AsCompatible inst): {
-      auto value = eval_context.GetConstantValue(inst.source_id);
-      if (!value.is_constant()) {
-        return value;
-      }
+  // A non-generic interface declaration evaluates to a facet type.
+  return ConstantEvalResult::New(FacetTypeFromInterface(
+      context, inst.interface_id, SemIR::SpecificId::None));
+}
 
-      auto from_phase = Phase::Concrete;
-      auto value_inst_id =
-          GetConstantValue(eval_context, inst.source_id, &from_phase);
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::SpecificConstant inst)
+    -> ConstantEvalResult {
+  // Pull the constant value out of the specific.
+  return ConstantEvalResult::Existing(SemIR::GetConstantValueInSpecific(
+      context.sem_ir(), inst.specific_id, inst.inst_id));
+}
 
-      auto to_phase = Phase::Concrete;
-      auto type_id = GetConstantValue(eval_context, inst.type_id, &to_phase);
+// Performs an access into an aggregate, retrieving the specified element.
+static auto PerformAggregateAccess(Context& context, SemIR::Inst inst)
+    -> ConstantEvalResult {
+  auto access_inst = inst.As<SemIR::AnyAggregateAccess>();
+  if (auto aggregate = context.insts().TryGetAs<SemIR::AnyAggregateValue>(
+          access_inst.aggregate_id)) {
+    auto elements = context.inst_blocks().Get(aggregate->elements_id);
+    auto index = static_cast<size_t>(access_inst.index.index);
+    CARBON_CHECK(index < elements.size(), "Access out of bounds.");
+    // `Phase` is not used here. If this element is a concrete constant, then
+    // so is the result of indexing, even if the aggregate also contains a
+    // symbolic context.
+    return ConstantEvalResult::Existing(
+        context.constant_values().Get(elements[index]));
+  }
+
+  return ConstantEvalResult::New(inst);
+}
 
-      auto value_inst = eval_context.insts().Get(value_inst_id);
-      value_inst.SetType(type_id);
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ClassElementAccess inst)
+    -> ConstantEvalResult {
+  return PerformAggregateAccess(context, inst);
+}
 
-      if (to_phase >= from_phase) {
-        // If moving from a concrete constant value to a symbolic type, the new
-        // constant value takes on the phase of the new type. We're adding the
-        // symbolic bit to the new constant value due to the presence of a
-        // symbolic type.
-        return MakeConstantResult(eval_context.context(), value_inst, to_phase);
-      } else {
-        // If moving from a symbolic constant value to a concrete type, the new
-        // constant value has a phase that depends on what is in the value. If
-        // there is anything symbolic within the value, then it's symbolic. We
-        // can't easily determine that here without evaluating a new constant
-        // value. See
-        // https://github.com/carbon-language/carbon-lang/pull/4881#discussion_r1939961372
-        [[clang::musttail]] return TryEvalInstInContext(
-            eval_context, SemIR::InstId::None, value_inst);
-      }
-    }
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::StructAccess inst) -> ConstantEvalResult {
+  return PerformAggregateAccess(context, inst);
+}
 
-    // These semantic wrappers don't change the constant value.
-    case CARBON_KIND(SemIR::BindAlias typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.value_id);
-    }
-    case CARBON_KIND(SemIR::ExportDecl typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.value_id);
-    }
-    case CARBON_KIND(SemIR::NameRef typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.value_id);
-    }
-    case CARBON_KIND(SemIR::ValueParamPattern param_pattern): {
-      // TODO: Treat this as a non-expression (here and in GetExprCategory)
-      // once generic deduction doesn't need patterns to have constant values.
-      return eval_context.GetConstantValue(param_pattern.subpattern_id);
-    }
-    case CARBON_KIND(SemIR::Converted typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.result_id);
-    }
-    case CARBON_KIND(SemIR::InitializeFrom typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.src_id);
-    }
-    case CARBON_KIND(SemIR::SpliceBlock typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.result_id);
-    }
-    case CARBON_KIND(SemIR::ValueOfInitializer typed_inst): {
-      return eval_context.GetConstantValue(typed_inst.init_id);
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::TupleAccess inst) -> ConstantEvalResult {
+  return PerformAggregateAccess(context, inst);
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc loc,
+                             SemIR::ImplWitnessAccess inst)
+    -> ConstantEvalResult {
+  // This is PerformAggregateAccess followed by GetConstantInSpecific.
+  if (auto witness =
+          context.insts().TryGetAs<SemIR::ImplWitness>(inst.witness_id)) {
+    auto elements = context.inst_blocks().Get(witness->elements_id);
+    auto index = static_cast<size_t>(inst.index.index);
+    CARBON_CHECK(index < elements.size(), "Access out of bounds.");
+    auto element = elements[index];
+    if (!element.has_value()) {
+      // TODO: Perhaps this should be a `{}` value with incomplete type?
+      CARBON_DIAGNOSTIC(ImplAccessMemberBeforeComplete, Error,
+                        "accessing member from impl before the end of "
+                        "its definition");
+      // TODO: Add note pointing to the impl declaration.
+      context.emitter().Emit(loc, ImplAccessMemberBeforeComplete);
+      return ConstantEvalResult::Error;
     }
-    case CARBON_KIND(SemIR::FacetAccessType typed_inst): {
-      Phase phase = Phase::Concrete;
-      if (ReplaceFieldWithConstantValue(
-              eval_context, &typed_inst,
-              &SemIR::FacetAccessType::facet_value_inst_id, &phase)) {
-        if (auto facet_value = eval_context.insts().TryGetAs<SemIR::FacetValue>(
-                typed_inst.facet_value_inst_id)) {
-          return eval_context.constant_values().Get(facet_value->type_inst_id);
-        }
-        return MakeConstantResult(eval_context.context(), typed_inst, phase);
-      } else {
-        return MakeNonConstantResult(phase);
-      }
+
+    LoadImportRef(context, element);
+    return ConstantEvalResult::Existing(GetConstantValueInSpecific(
+        context.sem_ir(), witness->specific_id, element));
+  }
+
+  return ConstantEvalResult::New(inst);
+}
+
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::BindValue /*inst*/) -> ConstantEvalResult {
+  // TODO: Handle this once we've decided how to represent constant values of
+  // reference expressions.
+  return ConstantEvalResult::TODO;
+}
+
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::Deref /*inst*/) -> ConstantEvalResult {
+  // TODO: Handle this.
+  return ConstantEvalResult::TODO;
+}
+
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::Temporary /*inst*/) -> ConstantEvalResult {
+  // TODO: Handle this. Can we just return the value of `init_id`?
+  return ConstantEvalResult::TODO;
+}
+
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::VtablePtr /*inst*/) -> ConstantEvalResult {
+  // TODO: Handle this.
+  return ConstantEvalResult::TODO;
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::AsCompatible inst) -> ConstantEvalResult {
+  // AsCompatible changes the type of the source instruction; its constant
+  // value, if there is one, needs to be modified to be of the same type.
+  auto value_id = context.constant_values().Get(inst.source_id);
+  CARBON_CHECK(value_id.is_constant());
+
+  auto value_inst =
+      context.insts().Get(context.constant_values().GetInstId(value_id));
+  auto phase = GetPhase(context.constant_values(),
+                        context.types().GetConstantId(inst.type_id));
+  value_inst.SetType(inst.type_id);
+
+  // Finish computing the new phase by incorporating the phases of the
+  // arguments.
+  EvalContext eval_context(context, SemIR::InstId::None);
+  auto kinds = value_inst.ArgKinds();
+  GetConstantValueForArg(eval_context, kinds.first, value_inst.arg0(), &phase);
+  GetConstantValueForArg(eval_context, kinds.second, value_inst.arg1(), &phase);
+  CARBON_CHECK(IsConstant(phase));
+
+  // We can't use `ConstantEvalResult::New` because it would use the wrong
+  // phase, so manually build a new constant.
+  return ConstantEvalResult::Existing(
+      MakeConstantResult(context, value_inst, phase));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::BindAlias inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.value_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ExportDecl inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.value_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::NameRef inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.value_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ValueParamPattern inst)
+    -> ConstantEvalResult {
+  // TODO: Treat this as a non-expression (here and in GetExprCategory)
+  // once generic deduction doesn't need patterns to have constant values.
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.subpattern_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::Converted inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.result_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::InitializeFrom inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.src_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::SpliceBlock inst) -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.result_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ValueOfInitializer inst)
+    -> ConstantEvalResult {
+  return ConstantEvalResult::Existing(
+      context.constant_values().Get(inst.init_id));
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::FacetAccessType inst)
+    -> ConstantEvalResult {
+  if (auto facet_value = context.insts().TryGetAs<SemIR::FacetValue>(
+          inst.facet_value_inst_id)) {
+    return ConstantEvalResult::Existing(
+        context.constant_values().Get(facet_value->type_inst_id));
+  }
+  return ConstantEvalResult::New(inst);
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::FacetAccessWitness inst)
+    -> ConstantEvalResult {
+  if (auto facet_value = context.insts().TryGetAs<SemIR::FacetValue>(
+          inst.facet_value_inst_id)) {
+    return ConstantEvalResult::Existing(
+        context.constant_values().Get(facet_value->witness_inst_id));
+  }
+  return ConstantEvalResult::New(inst);
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::UnaryOperatorNot inst)
+    -> ConstantEvalResult {
+  // `not true` -> `false`, `not false` -> `true`.
+  // All other uses of unary `not` are non-constant.
+  auto const_id = context.constant_values().Get(inst.operand_id);
+  if (const_id.is_concrete()) {
+    auto value = context.insts().GetAs<SemIR::BoolLiteral>(
+        context.constant_values().GetInstId(const_id));
+    value.value = SemIR::BoolValue::From(!value.value.ToBool());
+    return ConstantEvalResult::New(value);
+  }
+  return ConstantEvalResult::NotConstant;
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc /*loc*/,
+                             SemIR::ConstType inst) -> ConstantEvalResult {
+  // `const (const T)` evaluates to `const T`.
+  if (context.types().Is<SemIR::ConstType>(inst.inner_id)) {
+    return ConstantEvalResult::Existing(
+        context.types().GetConstantId(inst.inner_id));
+  }
+  // Otherwise, `const T` evaluates to itself.
+  return ConstantEvalResult::New(inst);
+}
+
+static auto EvalConstantInst(Context& context, SemIRLoc loc,
+                             SemIR::RequireCompleteType inst)
+    -> ConstantEvalResult {
+  auto witness_type_id =
+      GetSingletonType(context, SemIR::WitnessType::SingletonInstId);
+
+  // If the type is a concrete constant, require it to be complete now.
+  auto complete_type_id = inst.complete_type_id;
+  if (context.types().GetConstantId(complete_type_id).is_concrete()) {
+    if (!TryToCompleteType(context, complete_type_id, loc, [&] {
+          // TODO: It'd be nice to report the original type prior to
+          // evaluation here.
+          CARBON_DIAGNOSTIC(IncompleteTypeInMonomorphization, Error,
+                            "type {0} is incomplete", SemIR::TypeId);
+          return context.emitter().Build(loc, IncompleteTypeInMonomorphization,
+                                         complete_type_id);
+        })) {
+      return ConstantEvalResult::Error;
     }
-    case CARBON_KIND(SemIR::FacetAccessWitness typed_inst): {
-      Phase phase = Phase::Concrete;
-      if (ReplaceFieldWithConstantValue(
-              eval_context, &typed_inst,
-              &SemIR::FacetAccessWitness::facet_value_inst_id, &phase)) {
-        if (auto facet_value = eval_context.insts().TryGetAs<SemIR::FacetValue>(
-                typed_inst.facet_value_inst_id)) {
-          return eval_context.constant_values().Get(
-              facet_value->witness_inst_id);
-        }
-        return MakeConstantResult(eval_context.context(), typed_inst, phase);
-      } else {
-        return MakeNonConstantResult(phase);
+    return ConstantEvalResult::New(SemIR::CompleteTypeWitness{
+        .type_id = witness_type_id,
+        .object_repr_id = context.types().GetObjectRepr(complete_type_id)});
+  }
+
+  // If it's not a concrete constant, require it to be complete once it
+  // becomes one.
+  return ConstantEvalResult::New(inst);
+}
+
+static auto EvalConstantInst(Context& /*context*/, SemIRLoc /*loc*/,
+                             SemIR::ImportRefUnloaded inst)
+    -> ConstantEvalResult {
+  CARBON_FATAL("ImportRefUnloaded should be loaded before TryEvalInst: {0}",
+               inst);
+}
+
+// Evaluates an instruction of a known type in an evaluation context. The
+// default behavior of this function depends on the constant kind of the
+// instruction:
+//
+//  -  InstConstantKind::Never: returns ConstantId::NotConstant.
+//  -  InstConstantKind::Indirect, SymbolicOnly, Conditional: evaluates all the
+//     operands of the instruction, and calls `EvalConstantInst` to evaluate the
+//     resulting constant instruction.
+//  -  InstConstantKind::WheneverPossible, Always: evaluates all the operands of
+//     the instruction, and produces the resulting constant instruction as the
+//     result.
+//  -  InstConstantKind::Unique: returns the `inst_id` as the resulting
+//     constant.
+//
+// Returns an error constant ID if any of the nested evaluations fail, and
+// returns NotConstant if any of the nested evaluations is non-constant.
+//
+// This template is explicitly specialized for instructions that need special
+// handling.
+template <typename InstT>
+static auto TryEvalTypedInst(EvalContext& eval_context, SemIR::InstId inst_id,
+                             SemIR::Inst inst) -> SemIR::ConstantId {
+  constexpr auto ConstantKind = InstT::Kind.constant_kind();
+  if constexpr (ConstantKind == SemIR::InstConstantKind::Never) {
+    return SemIR::ConstantId::NotConstant;
+  } else if constexpr (ConstantKind == SemIR::InstConstantKind::Unique) {
+    CARBON_CHECK(inst_id.has_value());
+    return SemIR::ConstantId::ForConcreteConstant(inst_id);
+  } else {
+    // Build a constant instruction by replacing each non-constant operand with
+    // its constant value.
+    Phase phase = Phase::Concrete;
+    if (!ReplaceAllFieldsWithConstantValues(eval_context, &inst, &phase)) {
+      if constexpr (ConstantKind == SemIR::InstConstantKind::Always) {
+        CARBON_CHECK(phase == Phase::UnknownDueToError,
+                     "{0} should always be constant", InstT::Kind);
       }
+      return MakeNonConstantResult(phase);
     }
-    case CARBON_KIND(SemIR::WhereExpr typed_inst): {
-      Phase phase = Phase::Concrete;
-      SemIR::TypeId base_facet_type_id =
-          eval_context.insts().Get(typed_inst.period_self_id).type_id();
-      SemIR::Inst base_facet_inst =
-          eval_context.GetConstantValueAsInst(base_facet_type_id);
-      SemIR::FacetTypeInfo info = {.other_requirements = false};
-      // `where` provides that the base facet is an error, `type`, or a facet
-      // type.
-      if (auto facet_type = base_facet_inst.TryAs<SemIR::FacetType>()) {
-        info = GetConstantFacetTypeInfo(eval_context, facet_type->facet_type_id,
-                                        &phase);
-      } else if (base_facet_type_id == SemIR::ErrorInst::SingletonTypeId) {
-        return SemIR::ErrorInst::SingletonConstantId;
-      } else {
-        CARBON_CHECK(base_facet_type_id == SemIR::TypeType::SingletonTypeId,
-                     "Unexpected type_id: {0}, inst: {1}", base_facet_type_id,
-                     base_facet_inst);
-      }
-      if (typed_inst.requirements_id.has_value()) {
-        auto insts = eval_context.inst_blocks().Get(typed_inst.requirements_id);
-        for (auto inst_id : insts) {
-          if (auto rewrite =
-                  eval_context.insts().TryGetAs<SemIR::RequirementRewrite>(
-                      inst_id)) {
-            SemIR::ConstantId lhs =
-                eval_context.GetConstantValue(rewrite->lhs_id);
-            SemIR::ConstantId rhs =
-                eval_context.GetConstantValue(rewrite->rhs_id);
-            // `where` requirements using `.Self` should not be considered
-            // symbolic
-            UpdatePhaseIgnorePeriodSelf(eval_context, lhs, &phase);
-            UpdatePhaseIgnorePeriodSelf(eval_context, rhs, &phase);
-            info.rewrite_constraints.push_back(
-                {.lhs_const_id = lhs, .rhs_const_id = rhs});
-          } else {
-            // TODO: Handle other requirements
-            info.other_requirements = true;
-          }
-        }
+    if constexpr (ConstantKind == SemIR::InstConstantKind::Always ||
+                  ConstantKind == SemIR::InstConstantKind::WheneverPossible) {
+      return MakeConstantResult(eval_context.context(), inst, phase);
+    } else {
+      ConstantEvalResult result = EvalConstantInst(
+          eval_context.context(), eval_context.GetDiagnosticLoc({inst_id}),
+          inst.As<InstT>());
+      if (result.is_new()) {
+        return MakeConstantResult(eval_context.context(), result.new_inst(),
+                                  phase);
       }
-      info.Canonicalize();
-      return MakeFacetTypeResult(eval_context.context(), info, phase);
+      return result.existing();
     }
+  }
+}
 
-    // `not true` -> `false`, `not false` -> `true`.
-    // All other uses of unary `not` are non-constant.
-    case CARBON_KIND(SemIR::UnaryOperatorNot typed_inst): {
-      auto const_id = eval_context.GetConstantValue(typed_inst.operand_id);
-      auto phase = GetPhase(eval_context, const_id);
-      if (phase == Phase::Concrete) {
-        auto value = eval_context.insts().GetAs<SemIR::BoolLiteral>(
-            eval_context.constant_values().GetInstId(const_id));
-        return MakeBoolResult(eval_context.context(), value.type_id,
-                              !value.value.ToBool());
-      }
-      if (phase == Phase::UnknownDueToError) {
-        return SemIR::ErrorInst::SingletonConstantId;
-      }
-      break;
-    }
+// Specialize evaluation for array indexing because we want to check the index
+// expression even if the array expression is non-constant.
+template <>
+auto TryEvalTypedInst<SemIR::ArrayIndex>(EvalContext& eval_context,
+                                         SemIR::InstId /*inst_id*/,
+                                         SemIR::Inst inst)
+    -> SemIR::ConstantId {
+  return PerformArrayIndex(eval_context, inst.As<SemIR::ArrayIndex>());
+}
 
-    // `const (const T)` evaluates to `const T`. Otherwise, `const T` evaluates
-    // to itself.
-    case CARBON_KIND(SemIR::ConstType typed_inst): {
-      auto phase = Phase::Concrete;
-      auto inner_id =
-          GetConstantValue(eval_context, typed_inst.inner_id, &phase);
-      if (eval_context.context().types().Is<SemIR::ConstType>(inner_id)) {
-        return eval_context.context().types().GetConstantId(inner_id);
-      }
-      typed_inst.inner_id = inner_id;
-      return MakeConstantResult(eval_context.context(), typed_inst, phase);
-    }
+// Specialize evaluation for function calls because we want to check the callee
+// expression even if an argument expression is non-constant, and because we
+// will eventually want to perform control flow handling here.
+template <>
+auto TryEvalTypedInst<SemIR::Call>(EvalContext& eval_context,
+                                   SemIR::InstId inst_id, SemIR::Inst inst)
+    -> SemIR::ConstantId {
+  return MakeConstantForCall(eval_context,
+                             eval_context.GetDiagnosticLoc(inst_id),
+                             inst.As<SemIR::Call>());
+}
 
-    case CARBON_KIND(SemIR::RequireCompleteType require_complete): {
-      auto phase = Phase::Concrete;
-      auto witness_type_id = GetSingletonType(
-          eval_context.context(), SemIR::WitnessType::SingletonInstId);
-      auto complete_type_id = GetConstantValue(
-          eval_context, require_complete.complete_type_id, &phase);
-
-      // If the type is a concrete constant, require it to be complete now.
-      if (phase == Phase::Concrete) {
-        if (!TryToCompleteType(
-                eval_context.context(), complete_type_id,
-                eval_context.GetDiagnosticLoc(inst_id), [&] {
-                  CARBON_DIAGNOSTIC(IncompleteTypeInMonomorphization, Error,
-                                    "{0} evaluates to incomplete type {1}",
-                                    SemIR::TypeId, SemIR::TypeId);
-                  return eval_context.emitter().Build(
-                      eval_context.GetDiagnosticLoc(inst_id),
-                      IncompleteTypeInMonomorphization,
-                      require_complete.complete_type_id, complete_type_id);
-                })) {
-          return SemIR::ErrorInst::SingletonConstantId;
-        }
-        return MakeConstantResult(
-            eval_context.context(),
-            SemIR::CompleteTypeWitness{
-                .type_id = witness_type_id,
-                .object_repr_id =
-                    eval_context.types().GetObjectRepr(complete_type_id)},
-            phase);
-      }
+// ImportRefLoaded can have a constant value, but it's owned and maintained by
+// `import_ref.cpp`, not by us.
+// TODO: Rearrange how `ImportRefLoaded` instructions are created so we never
+// call this.
+template <>
+auto TryEvalTypedInst<SemIR::ImportRefLoaded>(EvalContext& /*eval_context*/,
+                                              SemIR::InstId /*inst_id*/,
+                                              SemIR::Inst /*inst*/)
+    -> SemIR::ConstantId {
+  return SemIR::ConstantId::NotConstant;
+}
+
+// TODO: Disable constant evaluation of SymbolicBindingPattern once
+// DeduceGenericCallArguments no longer needs implicit params to have constant
+// values.
+template <>
+auto TryEvalTypedInst<SemIR::SymbolicBindingPattern>(EvalContext& eval_context,
+                                                     SemIR::InstId /*inst_id*/,
+                                                     SemIR::Inst inst)
+    -> SemIR::ConstantId {
+  auto bind = inst.As<SemIR::SymbolicBindingPattern>();
+
+  const auto& bind_name = eval_context.entity_names().Get(bind.entity_name_id);
+
+  // If we know which specific we're evaluating within and this is an
+  // argument of that specific, its constant value is the corresponding
+  // argument value.
+  if (auto value = eval_context.GetCompileTimeBindValue(bind_name.bind_index());
+      value.has_value()) {
+    return value;
+  }
 
-      // If it's not a concrete constant, require it to be complete once it
-      // becomes one.
-      return MakeConstantResult(
-          eval_context.context(),
-          SemIR::RequireCompleteType{.type_id = witness_type_id,
-                                     .complete_type_id = complete_type_id},
-          phase);
+  // The constant form of a symbolic binding is an idealized form of the
+  // original, with no equivalent value.
+  bind.entity_name_id =
+      eval_context.entity_names().MakeCanonical(bind.entity_name_id);
+  return MakeConstantResult(
+      eval_context.context(), bind,
+      bind_name.is_template ? Phase::TemplateSymbolic : Phase::CheckedSymbolic);
+}
+
+// Symbolic bindings are a special case because they can reach into the eval
+// context and produce a context-specific value.
+template <>
+auto TryEvalTypedInst<SemIR::BindSymbolicName>(EvalContext& eval_context,
+                                               SemIR::InstId /*inst_id*/,
+                                               SemIR::Inst inst)
+    -> SemIR::ConstantId {
+  auto bind = inst.As<SemIR::BindSymbolicName>();
+
+  const auto& bind_name = eval_context.entity_names().Get(bind.entity_name_id);
+
+  Phase phase;
+  if (bind_name.name_id == SemIR::NameId::PeriodSelf) {
+    phase = Phase::PeriodSelfSymbolic;
+  } else {
+    // If we know which specific we're evaluating within and this is an
+    // argument of that specific, its constant value is the corresponding
+    // argument value.
+    if (auto value =
+            eval_context.GetCompileTimeBindValue(bind_name.bind_index());
+        value.has_value()) {
+      return value;
     }
+    phase = bind_name.is_template ? Phase::TemplateSymbolic
+                                  : Phase::CheckedSymbolic;
+  }
+  // The constant form of a symbolic binding is an idealized form of the
+  // original, with no equivalent value.
+  bind.entity_name_id =
+      eval_context.entity_names().MakeCanonical(bind.entity_name_id);
+  bind.value_id = SemIR::InstId::None;
+  if (!ReplaceFieldWithConstantValue(
+          eval_context, &bind, &SemIR::BindSymbolicName::type_id, &phase)) {
+    return MakeNonConstantResult(phase);
+  }
+  return MakeConstantResult(eval_context.context(), bind, phase);
+}
 
-    // These cases are either not expressions or not constant.
-    case SemIR::AddrPattern::Kind:
-    case SemIR::Assign::Kind:
-    case SemIR::BindName::Kind:
-    case SemIR::BindingPattern::Kind:
-    case SemIR::BlockArg::Kind:
-    case SemIR::Branch::Kind:
-    case SemIR::BranchIf::Kind:
-    case SemIR::BranchWithArg::Kind:
-    case SemIR::ImportCppDecl::Kind:
-    case SemIR::ImportDecl::Kind:
-    case SemIR::NameBindingDecl::Kind:
-    case SemIR::OutParam::Kind:
-    case SemIR::OutParamPattern::Kind:
-    case SemIR::RequirementEquivalent::Kind:
-    case SemIR::RequirementImpls::Kind:
-    case SemIR::RequirementRewrite::Kind:
-    case SemIR::Return::Kind:
-    case SemIR::ReturnExpr::Kind:
-    case SemIR::ReturnSlotPattern::Kind:
-    case SemIR::StructLiteral::Kind:
-    case SemIR::TupleLiteral::Kind:
-    case SemIR::TuplePattern::Kind:
-    case SemIR::ValueParam::Kind:
-    case SemIR::VarPattern::Kind:
-    case SemIR::VarStorage::Kind:
-      break;
+// TODO: Convert this to an EvalConstantInst instruction. This will require
+// providing a `GetConstantValue` overload for a requirement block.
+template <>
+auto TryEvalTypedInst<SemIR::WhereExpr>(EvalContext& eval_context,
+                                        SemIR::InstId /*inst_id*/,
+                                        SemIR::Inst inst) -> SemIR::ConstantId {
+  auto typed_inst = inst.As<SemIR::WhereExpr>();
 
-    case SemIR::ImportRefUnloaded::Kind:
-      CARBON_FATAL("ImportRefUnloaded should be loaded before TryEvalInst: {0}",
-                   inst);
+  Phase phase = Phase::Concrete;
+  SemIR::TypeId base_facet_type_id =
+      eval_context.insts().Get(typed_inst.period_self_id).type_id();
+  SemIR::Inst base_facet_inst =
+      eval_context.GetConstantValueAsInst(base_facet_type_id);
+  SemIR::FacetTypeInfo info = {.other_requirements = false};
+  // `where` provides that the base facet is an error, `type`, or a facet
+  // type.
+  if (auto facet_type = base_facet_inst.TryAs<SemIR::FacetType>()) {
+    info = GetConstantFacetTypeInfo(eval_context, facet_type->facet_type_id,
+                                    &phase);
+  } else if (base_facet_type_id == SemIR::ErrorInst::SingletonTypeId) {
+    return SemIR::ErrorInst::SingletonConstantId;
+  } else {
+    CARBON_CHECK(base_facet_type_id == SemIR::TypeType::SingletonTypeId,
+                 "Unexpected type_id: {0}, inst: {1}", base_facet_type_id,
+                 base_facet_inst);
+  }
+  if (typed_inst.requirements_id.has_value()) {
+    auto insts = eval_context.inst_blocks().Get(typed_inst.requirements_id);
+    for (auto inst_id : insts) {
+      if (auto rewrite =
+              eval_context.insts().TryGetAs<SemIR::RequirementRewrite>(
+                  inst_id)) {
+        SemIR::ConstantId lhs = eval_context.GetConstantValue(rewrite->lhs_id);
+        SemIR::ConstantId rhs = eval_context.GetConstantValue(rewrite->rhs_id);
+        // `where` requirements using `.Self` should not be considered
+        // symbolic
+        UpdatePhaseIgnorePeriodSelf(eval_context, lhs, &phase);
+        UpdatePhaseIgnorePeriodSelf(eval_context, rhs, &phase);
+        info.rewrite_constraints.push_back(
+            {.lhs_const_id = lhs, .rhs_const_id = rhs});
+      } else {
+        // TODO: Handle other requirements
+        info.other_requirements = true;
+      }
+    }
   }
-  return SemIR::ConstantId::NotConstant;
+  info.Canonicalize();
+  return MakeFacetTypeResult(eval_context.context(), info, phase);
+}
+
+// Implementation for `TryEvalInst`, wrapping `Context` with `EvalContext`.
+static auto TryEvalInstInContext(EvalContext& eval_context,
+                                 SemIR::InstId inst_id, SemIR::Inst inst)
+    -> SemIR::ConstantId {
+  using EvalInstFn =
+      auto(EvalContext & eval_context, SemIR::InstId inst_id, SemIR::Inst inst)
+          ->SemIR::ConstantId;
+  static constexpr EvalInstFn* EvalInstFns[] = {
+#define CARBON_SEM_IR_INST_KIND(Kind) &TryEvalTypedInst<SemIR::Kind>,
+#include "toolchain/sem_ir/inst_kind.def"
+  };
+  [[clang::musttail]] return EvalInstFns[inst.kind().AsInt()](eval_context,
+                                                              inst_id, inst);
 }
 
 auto TryEvalInst(Context& context, SemIR::InstId inst_id, SemIR::Inst inst)

+ 2 - 2
toolchain/check/testdata/array/fail_bound_negative.carbon

@@ -10,9 +10,9 @@
 
 fn Negate(n: i32) -> i32 = "int.snegate";
 
-// CHECK:STDERR: fail_bound_negative.carbon:[[@LINE+4]]:19: error: array bound of -1 is negative [ArrayBoundNegative]
+// CHECK:STDERR: fail_bound_negative.carbon:[[@LINE+4]]:8: error: array bound of -1 is negative [ArrayBoundNegative]
 // CHECK:STDERR: var a: array(i32, Negate(1));
-// CHECK:STDERR:                   ^~~~~~~~~
+// CHECK:STDERR:        ^~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 var a: array(i32, Negate(1));
 

+ 2 - 2
toolchain/check/testdata/array/fail_bound_overflow.carbon

@@ -8,9 +8,9 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/array/fail_bound_overflow.carbon
 
-// CHECK:STDERR: fail_bound_overflow.carbon:[[@LINE+4]]:19: error: array bound of 39999999999999999993 is too large [ArrayBoundTooLarge]
+// CHECK:STDERR: fail_bound_overflow.carbon:[[@LINE+4]]:8: error: array bound of 39999999999999999993 is too large [ArrayBoundTooLarge]
 // CHECK:STDERR: var a: array(i32, 39999999999999999993);
-// CHECK:STDERR:                   ^~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
 var a: array(i32, 39999999999999999993);
 

+ 1 - 1
toolchain/check/testdata/array/fail_incomplete_element.carbon

@@ -74,7 +74,7 @@ var p: Incomplete* = &a[0];
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %a.ref: <error> = name_ref a, file.%a
+// CHECK:STDOUT:   %a.ref: <error> = name_ref a, file.%a [concrete = <error>]
 // CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0]
 // CHECK:STDOUT:   %addr: <error> = addr_of <error> [concrete = <error>]
 // CHECK:STDOUT:   assign file.%p.var, <error>

+ 1 - 1
toolchain/check/testdata/array/fail_out_of_bound_non_literal.carbon

@@ -135,7 +135,7 @@ var b: i32 = a[{.index = 3}.index];
 // CHECK:STDOUT:   %.loc16_28.2: %i32 = value_of_initializer %int.convert_checked.loc16 [concrete = constants.%int_3.822]
 // CHECK:STDOUT:   %.loc16_28.3: %i32 = converted %.loc16_28.1, %.loc16_28.2 [concrete = constants.%int_3.822]
 // CHECK:STDOUT:   %.loc16_34.1: ref %i32 = array_index %a.ref, %.loc16_28.3 [concrete = <error>]
-// CHECK:STDOUT:   %.loc16_34.2: %i32 = bind_value %.loc16_34.1
+// CHECK:STDOUT:   %.loc16_34.2: %i32 = bind_value %.loc16_34.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, %.loc16_34.2
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 3 - 3
toolchain/check/testdata/builtin_conversions/no_prelude/fail_todo_convert_facet_value_to_narrowed_facet_type.carbon

@@ -184,9 +184,9 @@ fn HandleAnimal[T:! Animal & Eats](a: T) { Feed(a); }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %HandleAnimal.decl: %HandleAnimal.type = fn_decl @HandleAnimal [concrete = constants.%HandleAnimal] {
 // CHECK:STDOUT:     %T.patt.loc15_17.1: <error> = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc15_17.2 (constants.%T.patt.e01)]
-// CHECK:STDOUT:     %T.param_patt: <error> = value_param_pattern %T.patt.loc15_17.1, runtime_param<none> [symbolic = %T.patt.loc15_17.2 (constants.%T.patt.e01)]
+// CHECK:STDOUT:     %T.param_patt: <error> = value_param_pattern %T.patt.loc15_17.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:     %a.patt: <error> = binding_pattern a
-// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param0
+// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %T.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -238,7 +238,7 @@ fn HandleAnimal[T:! Animal & Eats](a: T) { Feed(a); }
 // CHECK:STDOUT:   fn[%T.param_patt: <error>](%a.param_patt: <error>) {
 // CHECK:STDOUT:   !entry:
 // CHECK:STDOUT:     %Feed.ref: %Feed.type = name_ref Feed, file.%Feed.decl [concrete = constants.%Feed]
-// CHECK:STDOUT:     %a.ref: <error> = name_ref a, %a
+// CHECK:STDOUT:     %a.ref: <error> = name_ref a, %a [concrete = <error>]
 // CHECK:STDOUT:     return
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/builtin_conversions/value_with_type_through_access.carbon

@@ -524,7 +524,7 @@ fn G() {
 // CHECK:STDOUT:     %x.patt: @F.%HoldsType.loc21_31.2 (%HoldsType.f95cf2.1) = binding_pattern x
 // CHECK:STDOUT:     %x.param_patt: @F.%HoldsType.loc21_31.2 (%HoldsType.f95cf2.1) = value_param_pattern %x.patt, runtime_param0
 // CHECK:STDOUT:     %a.patt: <error> = binding_pattern a
-// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1
+// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %T.param: %Class = value_param runtime_param<none>
 // CHECK:STDOUT:     %Class.ref: type = name_ref Class, file.%Class.decl [concrete = constants.%Class]
@@ -723,7 +723,7 @@ fn G() {
 // CHECK:STDOUT:     %x.patt: @F.%HoldsType.loc12_40.2 (%HoldsType) = binding_pattern x
 // CHECK:STDOUT:     %x.param_patt: @F.%HoldsType.loc12_40.2 (%HoldsType) = value_param_pattern %x.patt, runtime_param0
 // CHECK:STDOUT:     %a.patt: <error> = binding_pattern a
-// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1
+// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %T.param: %array_type = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc12_23: type = splice_block %array_type [concrete = constants.%array_type] {

+ 2 - 2
toolchain/check/testdata/choice/fail_invalid.carbon

@@ -68,8 +68,8 @@ fn F() {
 // CHECK:STDOUT:   %.loc11_23.1: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %Never.ref: type = name_ref Never, file.%Never.decl [concrete = constants.%Never]
 // CHECK:STDOUT:   %.loc11_23.2: ref %Never = temporary_storage
-// CHECK:STDOUT:   %.loc11_23.3: ref %Never = temporary %.loc11_23.2, <error>
-// CHECK:STDOUT:   %.loc11_23.4: ref %Never = converted %.loc11_23.1, %.loc11_23.3
+// CHECK:STDOUT:   %.loc11_23.3: ref %Never = temporary %.loc11_23.2, <error> [concrete = <error>]
+// CHECK:STDOUT:   %.loc11_23.4: ref %Never = converted %.loc11_23.1, %.loc11_23.3 [concrete = <error>]
 // CHECK:STDOUT:   %never: ref %Never = bind_name never, %.loc11_23.4
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/class/fail_generic_method.carbon

@@ -78,9 +78,9 @@ fn Class(N:! i32).F[self: Self](n: T) {}
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %.decl: %.type = fn_decl @.1 [concrete = constants.%.d85] {
 // CHECK:STDOUT:     %self.patt: <error> = binding_pattern self
-// CHECK:STDOUT:     %self.param_patt: <error> = value_param_pattern %self.patt, runtime_param0
+// CHECK:STDOUT:     %self.param_patt: <error> = value_param_pattern %self.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:     %n.patt: <error> = binding_pattern n
-// CHECK:STDOUT:     %n.param_patt: <error> = value_param_pattern %n.patt, runtime_param1
+// CHECK:STDOUT:     %n.param_patt: <error> = value_param_pattern %n.patt, runtime_param1 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %N.param: %i32 = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc33: type = splice_block %i32 [concrete = constants.%i32] {

+ 6 - 6
toolchain/check/testdata/class/fail_init.carbon

@@ -106,8 +106,8 @@ fn F() {
 // CHECK:STDOUT:   %.loc21_10.1: %struct_type.a = struct_literal (%int_1.loc21)
 // CHECK:STDOUT:   %Class.ref.loc21: type = name_ref Class, file.%Class.decl [concrete = constants.%Class]
 // CHECK:STDOUT:   %.loc21_10.2: ref %Class = temporary_storage
-// CHECK:STDOUT:   %.loc21_10.3: ref %Class = temporary %.loc21_10.2, <error>
-// CHECK:STDOUT:   %.loc21_12: ref %Class = converted %.loc21_10.1, %.loc21_10.3
+// CHECK:STDOUT:   %.loc21_10.3: ref %Class = temporary %.loc21_10.2, <error> [concrete = <error>]
+// CHECK:STDOUT:   %.loc21_12: ref %Class = converted %.loc21_10.1, %.loc21_10.3 [concrete = <error>]
 // CHECK:STDOUT:   %int_1.loc26: Core.IntLiteral = int_value 1 [concrete = constants.%int_1.5b8]
 // CHECK:STDOUT:   %int_2.loc26: Core.IntLiteral = int_value 2 [concrete = constants.%int_2]
 // CHECK:STDOUT:   %.loc26_18.1: %struct_type.a.c = struct_literal (%int_1.loc26, %int_2.loc26)
@@ -120,16 +120,16 @@ fn F() {
 // CHECK:STDOUT:   %.loc26_18.3: ref %Class = temporary_storage
 // CHECK:STDOUT:   %.loc26_18.4: ref %i32 = class_element_access %.loc26_18.3, element0
 // CHECK:STDOUT:   %.loc26_18.5: init %i32 = initialize_from %.loc26_18.2 to %.loc26_18.4 [concrete = constants.%int_1.5d2]
-// CHECK:STDOUT:   %.loc26_18.6: ref %Class = temporary %.loc26_18.3, <error>
-// CHECK:STDOUT:   %.loc26_20: ref %Class = converted %.loc26_18.1, %.loc26_18.6
+// CHECK:STDOUT:   %.loc26_18.6: ref %Class = temporary %.loc26_18.3, <error> [concrete = <error>]
+// CHECK:STDOUT:   %.loc26_20: ref %Class = converted %.loc26_18.1, %.loc26_18.6 [concrete = <error>]
 // CHECK:STDOUT:   %int_1.loc31: Core.IntLiteral = int_value 1 [concrete = constants.%int_1.5b8]
 // CHECK:STDOUT:   %int_2.loc31: Core.IntLiteral = int_value 2 [concrete = constants.%int_2]
 // CHECK:STDOUT:   %int_3: Core.IntLiteral = int_value 3 [concrete = constants.%int_3]
 // CHECK:STDOUT:   %.loc31_26.1: %struct_type.a.b.c = struct_literal (%int_1.loc31, %int_2.loc31, %int_3)
 // CHECK:STDOUT:   %Class.ref.loc31: type = name_ref Class, file.%Class.decl [concrete = constants.%Class]
 // CHECK:STDOUT:   %.loc31_26.2: ref %Class = temporary_storage
-// CHECK:STDOUT:   %.loc31_26.3: ref %Class = temporary %.loc31_26.2, <error>
-// CHECK:STDOUT:   %.loc31_28: ref %Class = converted %.loc31_26.1, %.loc31_26.3
+// CHECK:STDOUT:   %.loc31_26.3: ref %Class = temporary %.loc31_26.2, <error> [concrete = <error>]
+// CHECK:STDOUT:   %.loc31_28: ref %Class = converted %.loc31_26.1, %.loc31_26.3 [concrete = <error>]
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 1 - 1
toolchain/check/testdata/class/fail_self_param.carbon

@@ -42,7 +42,7 @@ var v: C(0);
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %C.decl: %C.type = class_decl @C [concrete = constants.%C.generic] {
 // CHECK:STDOUT:     %x.patt.loc15_22.1: <error> = symbolic_binding_pattern x, 0 [symbolic = %x.patt.loc15_22.2 (constants.%x.patt)]
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc15_22.1, runtime_param<none> [symbolic = %x.patt.loc15_22.2 (constants.%x.patt)]
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc15_22.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %self.ref: <error> = name_ref self, <error> [concrete = <error>]

+ 2 - 2
toolchain/check/testdata/class/no_prelude/import_access.carbon

@@ -488,7 +488,7 @@ private class Redecl {}
 // CHECK:STDOUT:   %default.import = import <none>
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %c.patt: <error> = binding_pattern c
-// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0
+// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %c.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc10: type = splice_block %ptr [concrete = <error>] {
@@ -526,7 +526,7 @@ private class Redecl {}
 // CHECK:STDOUT:   %Test.import = import Test
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %c.patt: <error> = binding_pattern c
-// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0
+// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %c.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc10: type = splice_block %ptr [concrete = <error>] {

+ 2 - 2
toolchain/check/testdata/class/no_prelude/name_poisoning.carbon

@@ -481,7 +481,7 @@ class C {
 // CHECK:STDOUT:   %C.decl.loc18: type = class_decl @C.2 [concrete = constants.%C.9f4] {} {}
 // CHECK:STDOUT:   %F2.decl: %F2.type = fn_decl @F2 [concrete = constants.%F2] {
 // CHECK:STDOUT:     %x.patt: <error> = binding_pattern x
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -680,7 +680,7 @@ class C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %x.patt: <error> = binding_pattern x
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %C.ref: <error> = name_ref C, <error> [concrete = <error>]

+ 1 - 1
toolchain/check/testdata/deduce/int_float.carbon

@@ -211,7 +211,7 @@ fn G(a: f64) -> Core.IntLiteral() {
 // CHECK:STDOUT:     %N.patt.loc9_6.1: Core.IntLiteral = symbolic_binding_pattern N, 0 [symbolic = %N.patt.loc9_6.2 (constants.%N.patt)]
 // CHECK:STDOUT:     %N.param_patt: Core.IntLiteral = value_param_pattern %N.patt.loc9_6.1, runtime_param<none> [symbolic = %N.patt.loc9_6.2 (constants.%N.patt)]
 // CHECK:STDOUT:     %n.patt: <error> = binding_pattern n
-// CHECK:STDOUT:     %n.param_patt: <error> = value_param_pattern %n.patt, runtime_param0
+// CHECK:STDOUT:     %n.param_patt: <error> = value_param_pattern %n.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:     %return.patt: Core.IntLiteral = return_slot_pattern
 // CHECK:STDOUT:     %return.param_patt: Core.IntLiteral = out_param_pattern %return.patt, runtime_param1
 // CHECK:STDOUT:   } {

+ 1 - 1
toolchain/check/testdata/function/declaration/fail_param_in_type.carbon

@@ -41,7 +41,7 @@ fn F(n: i32, a: array(i32, n)*);
 // CHECK:STDOUT:     %n.patt: %i32 = binding_pattern n
 // CHECK:STDOUT:     %n.param_patt: %i32 = value_param_pattern %n.patt, runtime_param0
 // CHECK:STDOUT:     %a.patt: <error> = binding_pattern a
-// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1
+// CHECK:STDOUT:     %a.param_patt: <error> = value_param_pattern %a.patt, runtime_param1 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %n.param: %i32 = value_param runtime_param0
 // CHECK:STDOUT:     %.loc15_9: type = splice_block %i32.loc15_9 [concrete = constants.%i32] {

+ 1 - 1
toolchain/check/testdata/function/definition/no_prelude/fail_decl_param_mismatch.carbon

@@ -144,7 +144,7 @@ fn K() -> {} { return {}; }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %.decl.loc36: %.type.b6a92a.3 = fn_decl @.3 [concrete = constants.%.d852be.3] {
 // CHECK:STDOUT:     %x.patt: <error> = binding_pattern x
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %x: <error> = bind_name x, %x.param

+ 2 - 2
toolchain/check/testdata/generic/complete_type.carbon

@@ -15,7 +15,7 @@ library "[[@TEST_NAME]]";
 class B;
 
 class A(T:! type) {
-  // CHECK:STDERR: fail_incomplete_in_class.carbon:[[@LINE+6]]:10: error: `T` evaluates to incomplete type `B` [IncompleteTypeInMonomorphization]
+  // CHECK:STDERR: fail_incomplete_in_class.carbon:[[@LINE+6]]:10: error: type `B` is incomplete [IncompleteTypeInMonomorphization]
   // CHECK:STDERR:   var v: T;
   // CHECK:STDERR:          ^
   // CHECK:STDERR: fail_incomplete_in_class.carbon:[[@LINE-6]]:1: note: class was forward declared here [ClassForwardDeclaredHere]
@@ -61,7 +61,7 @@ library "[[@TEST_NAME]]";
 class B;
 
 fn F(T:! type) {
-  // CHECK:STDERR: fail_incomplete_in_function_at_eof.carbon:[[@LINE+6]]:10: error: `T` evaluates to incomplete type `B` [IncompleteTypeInMonomorphization]
+  // CHECK:STDERR: fail_incomplete_in_function_at_eof.carbon:[[@LINE+6]]:10: error: type `B` is incomplete [IncompleteTypeInMonomorphization]
   // CHECK:STDERR:   var v: T;
   // CHECK:STDERR:          ^
   // CHECK:STDERR: fail_incomplete_in_function_at_eof.carbon:[[@LINE-6]]:1: note: class was forward declared here [ClassForwardDeclaredHere]

+ 3 - 4
toolchain/check/testdata/impl/assoc_const_self.carbon

@@ -62,9 +62,9 @@ impl C as I where .V = () {}
 library "[[@TEST_NAME]]";
 
 interface I(N:! Core.IntLiteral()) {
-  // CHECK:STDERR: fail_monomorphization_failure.carbon:[[@LINE+3]]:17: error: array bound of -1 is negative [ArrayBoundNegative]
+  // CHECK:STDERR: fail_monomorphization_failure.carbon:[[@LINE+3]]:11: error: array bound of -1 is negative [ArrayBoundNegative]
   // CHECK:STDERR:   let V:! array(Self, N);
-  // CHECK:STDERR:                 ^~~~
+  // CHECK:STDERR:           ^~~~~~~~~~~~~~
   let V:! array(Self, N);
 }
 
@@ -563,7 +563,6 @@ fn CallF() {
 // CHECK:STDOUT:   %.Self.as_type: type = facet_access_type %.Self [symbolic_self]
 // CHECK:STDOUT:   %.Self.as_wit: <witness> = facet_access_witness %.Self [symbolic_self]
 // CHECK:STDOUT:   %I.facet: %I.type.057 = facet_value %.Self.as_type, %.Self.as_wit [symbolic_self]
-// CHECK:STDOUT:   %impl.elem0: <error> = impl_witness_access %.Self.as_wit, element0 [symbolic_self]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -614,7 +613,7 @@ fn CallF() {
 // CHECK:STDOUT:     %.Self.as_type: type = facet_access_type %.Self.ref [symbolic_self = constants.%.Self.as_type]
 // CHECK:STDOUT:     %.loc15_24.2: type = converted %.Self.ref, %.Self.as_type [symbolic_self = constants.%.Self.as_type]
 // CHECK:STDOUT:     %.Self.as_wit: <witness> = facet_access_witness %.Self.ref [symbolic_self = constants.%.Self.as_wit]
-// CHECK:STDOUT:     %impl.elem0.loc15_24: <error> = impl_witness_access %.Self.as_wit, element0 [symbolic_self = constants.%impl.elem0]
+// CHECK:STDOUT:     %impl.elem0.loc15_24: <error> = impl_witness_access %.Self.as_wit, element0 [concrete = <error>]
 // CHECK:STDOUT:     %.loc15_30: %empty_tuple.type = tuple_literal ()
 // CHECK:STDOUT:     %.loc15_18: type = where_expr %.Self [concrete = <error>] {
 // CHECK:STDOUT:       requirement_rewrite %impl.elem0.loc15_24, <error>

+ 1 - 1
toolchain/check/testdata/impl/fail_call_invalid.carbon

@@ -106,7 +106,7 @@ fn InstanceCall(n: i32) {
 // CHECK:STDOUT: impl @impl.006: %i32 as %Simple.ref {
 // CHECK:STDOUT:   %G.decl: %G.type.c98 = fn_decl @G.2 [concrete = constants.%G.e73] {
 // CHECK:STDOUT:     %self.patt: <error> = binding_pattern self
-// CHECK:STDOUT:     %self.param_patt: <error> = value_param_pattern %self.patt, runtime_param0
+// CHECK:STDOUT:     %self.param_patt: <error> = value_param_pattern %self.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %self.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %Undeclared.ref: <error> = name_ref Undeclared, <error> [concrete = <error>]

+ 1 - 1
toolchain/check/testdata/impl/fail_self_type_mismatch.carbon

@@ -100,7 +100,7 @@ impl i32 as I {
 // CHECK:STDOUT:   %Self: %I.type = bind_symbolic_name Self, 0 [symbolic = constants.%Self]
 // CHECK:STDOUT:   %F.decl: %F.type.cf0 = fn_decl @F.1 [concrete = constants.%F.bc6] {
 // CHECK:STDOUT:     %c.patt: <error> = binding_pattern c
-// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0
+// CHECK:STDOUT:     %c.param_patt: <error> = value_param_pattern %c.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %c.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc29: type = splice_block %C [concrete = <error>] {

+ 1 - 1
toolchain/check/testdata/impl/fail_todo_use_assoc_const.carbon

@@ -177,7 +177,7 @@ impl () as I where .N = 2 {
 // CHECK:STDOUT:     %self.patt: @F.1.%Self.as_type.loc19_14.1 (%Self.as_type.3df) = binding_pattern self
 // CHECK:STDOUT:     %self.param_patt: @F.1.%Self.as_type.loc19_14.1 (%Self.as_type.3df) = value_param_pattern %self.patt, runtime_param0
 // CHECK:STDOUT:     %u.patt: <error> = binding_pattern u
-// CHECK:STDOUT:     %u.param_patt: <error> = value_param_pattern %u.patt, runtime_param1
+// CHECK:STDOUT:     %u.param_patt: <error> = value_param_pattern %u.patt, runtime_param1 [concrete = <error>]
 // CHECK:STDOUT:     %return.patt: <error> = return_slot_pattern
 // CHECK:STDOUT:     %return.param_patt: <error> = out_param_pattern %return.patt, runtime_param2
 // CHECK:STDOUT:   } {

+ 3 - 3
toolchain/check/testdata/impl/no_prelude/name_poisoning.carbon

@@ -472,7 +472,7 @@ class N.C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %F2.decl: %F2.type = fn_decl @F2 [concrete = constants.%F2] {
 // CHECK:STDOUT:     %x.patt.loc14_9.1: <error> = symbolic_binding_pattern x, 0 [symbolic = %x.patt.loc14_9.2 (constants.%x.patt.e01)]
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc14_9.1, runtime_param<none> [symbolic = %x.patt.loc14_9.2 (constants.%x.patt.e01)]
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc14_9.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -544,7 +544,7 @@ class N.C {
 // CHECK:STDOUT:   %I.decl.loc17: type = interface_decl @I.2 [concrete = constants.%I.type.4da] {} {}
 // CHECK:STDOUT:   %F2.decl: %F2.type = fn_decl @F2 [concrete = constants.%F2] {
 // CHECK:STDOUT:     %x.patt.loc23_9.1: <error> = symbolic_binding_pattern x, 0 [symbolic = %x.patt.loc23_9.2 (constants.%x.patt.e01)]
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc23_9.1, runtime_param<none> [symbolic = %x.patt.loc23_9.2 (constants.%x.patt.e01)]
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc23_9.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -811,7 +811,7 @@ class N.C {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %x.patt.loc13_8.1: <error> = symbolic_binding_pattern x, 0 [symbolic = %x.patt.loc13_8.2 (constants.%x.patt)]
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc13_8.1, runtime_param<none> [symbolic = %x.patt.loc13_8.2 (constants.%x.patt)]
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt.loc13_8.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %I.ref: <error> = name_ref I, <error> [concrete = <error>]

+ 2 - 2
toolchain/check/testdata/index/fail_array_large_index.carbon

@@ -129,7 +129,7 @@ var c: i32 = a[0x7FFF_FFFF];
 // CHECK:STDOUT:   %.loc17_16.1: %i32 = value_of_initializer %int.convert_checked.loc17 [concrete = constants.%int_1.5d2]
 // CHECK:STDOUT:   %.loc17_16.2: %i32 = converted %int_1, %.loc17_16.1 [concrete = constants.%int_1.5d2]
 // CHECK:STDOUT:   %.loc17_17.1: ref %i32 = array_index %a.ref.loc17, %.loc17_16.2 [concrete = <error>]
-// CHECK:STDOUT:   %.loc17_17.2: %i32 = bind_value %.loc17_17.1
+// CHECK:STDOUT:   %.loc17_17.2: %i32 = bind_value %.loc17_17.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, %.loc17_17.2
 // CHECK:STDOUT:   %a.ref.loc23: ref %array_type = name_ref a, file.%a
 // CHECK:STDOUT:   %int_2147483647: Core.IntLiteral = int_value 2147483647 [concrete = constants.%int_2147483647.d89]
@@ -142,7 +142,7 @@ var c: i32 = a[0x7FFF_FFFF];
 // CHECK:STDOUT:   %.loc23_16.1: %i32 = value_of_initializer %int.convert_checked.loc23 [concrete = constants.%int_2147483647.a74]
 // CHECK:STDOUT:   %.loc23_16.2: %i32 = converted %int_2147483647, %.loc23_16.1 [concrete = constants.%int_2147483647.a74]
 // CHECK:STDOUT:   %.loc23_27.1: ref %i32 = array_index %a.ref.loc23, %.loc23_16.2 [concrete = <error>]
-// CHECK:STDOUT:   %.loc23_27.2: %i32 = bind_value %.loc23_27.1
+// CHECK:STDOUT:   %.loc23_27.2: %i32 = bind_value %.loc23_27.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%c.var, %.loc23_27.2
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/index/fail_array_non_int_indexing.carbon

@@ -103,7 +103,7 @@ var b: i32 = a[2.6];
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
 // CHECK:STDOUT:   %.loc19_16: %i32 = converted %float, <error> [concrete = <error>]
 // CHECK:STDOUT:   %.loc19_19.1: ref %i32 = array_index %a.ref, <error> [concrete = <error>]
-// CHECK:STDOUT:   %.loc19_19.2: %i32 = bind_value %.loc19_19.1
+// CHECK:STDOUT:   %.loc19_19.2: %i32 = bind_value %.loc19_19.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, %.loc19_19.2
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/index/fail_array_out_of_bound_access.carbon

@@ -107,7 +107,7 @@ var b: i32 = a[1];
 // CHECK:STDOUT:   %.loc16_16.1: %i32 = value_of_initializer %int.convert_checked.loc16 [concrete = constants.%int_1.5d2]
 // CHECK:STDOUT:   %.loc16_16.2: %i32 = converted %int_1, %.loc16_16.1 [concrete = constants.%int_1.5d2]
 // CHECK:STDOUT:   %.loc16_17.1: ref %i32 = array_index %a.ref, %.loc16_16.2 [concrete = <error>]
-// CHECK:STDOUT:   %.loc16_17.2: %i32 = bind_value %.loc16_17.1
+// CHECK:STDOUT:   %.loc16_17.2: %i32 = bind_value %.loc16_17.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, %.loc16_17.2
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/index/fail_negative_indexing.carbon

@@ -134,7 +134,7 @@ var d: i32 = c[-10];
 // CHECK:STDOUT:   %.loc16_16.3: %i32 = value_of_initializer %int.convert_checked.loc16 [concrete = constants.%int_-10.c17]
 // CHECK:STDOUT:   %.loc16_16.4: %i32 = converted %int.snegate, %.loc16_16.3 [concrete = constants.%int_-10.c17]
 // CHECK:STDOUT:   %.loc16_19.1: ref %i32 = array_index %c.ref, %.loc16_16.4 [concrete = <error>]
-// CHECK:STDOUT:   %.loc16_19.2: %i32 = bind_value %.loc16_19.1
+// CHECK:STDOUT:   %.loc16_19.2: %i32 = bind_value %.loc16_19.1 [concrete = <error>]
 // CHECK:STDOUT:   assign file.%d.var, %.loc16_19.2
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/interface/no_prelude/fail_assoc_const_not_constant.carbon

@@ -41,8 +41,8 @@ alias UseOther = I.other;
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %I.decl: type = interface_decl @I [concrete = constants.%I.type] {} {}
 // CHECK:STDOUT:   %I.ref.loc24: type = name_ref I, %I.decl [concrete = constants.%I.type]
-// CHECK:STDOUT:   %a.ref: <error> = name_ref a, <unexpected>.inst19.loc20_7
-// CHECK:STDOUT:   %UseA: <error> = bind_alias UseA, <unexpected>.inst19.loc20_7
+// CHECK:STDOUT:   %a.ref: <error> = name_ref a, <unexpected>.inst19.loc20_7 [concrete = <error>]
+// CHECK:STDOUT:   %UseA: <error> = bind_alias UseA, <unexpected>.inst19.loc20_7 [concrete = <error>]
 // CHECK:STDOUT:   %I.ref.loc27: type = name_ref I, %I.decl [concrete = constants.%I.type]
 // CHECK:STDOUT:   %other.ref: <error> = name_ref other, <error> [concrete = <error>]
 // CHECK:STDOUT:   %UseOther: <error> = bind_alias UseOther, <error> [concrete = <error>]

+ 7 - 7
toolchain/check/testdata/interface/no_prelude/import_access.carbon

@@ -255,7 +255,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %default.import = import <none>
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %Def.ref: <error> = name_ref Def, <error> [concrete = <error>]
@@ -290,7 +290,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %Test.import = import Test
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -362,7 +362,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %default.import = import <none>
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %ForwardWithDef.ref: <error> = name_ref ForwardWithDef, <error> [concrete = <error>]
@@ -397,7 +397,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %Test.import = import Test
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.1: <error> = splice_block <error> [concrete = <error>] {
@@ -431,7 +431,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %default.import = import <none>
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc11: type = splice_block %ptr [concrete = <error>] {
@@ -471,7 +471,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %default.import = import <none>
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc10: type = splice_block %ptr [concrete = <error>] {
@@ -509,7 +509,7 @@ private interface Redecl {}
 // CHECK:STDOUT:   %Test.import = import Test
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %i.patt: <error> = binding_pattern i
-// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0
+// CHECK:STDOUT:     %i.param_patt: <error> = value_param_pattern %i.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %i.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %.loc10: type = splice_block %ptr [concrete = <error>] {

+ 2 - 2
toolchain/check/testdata/let/generic_import.carbon

@@ -108,8 +108,8 @@ var b: T = *a;
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %a.ref: <error> = name_ref a, file.%a
-// CHECK:STDOUT:   %.loc13: ref <error> = deref <error>
+// CHECK:STDOUT:   %a.ref: <error> = name_ref a, file.%a [concrete = <error>]
+// CHECK:STDOUT:   %.loc13: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, <error>
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/operators/builtin/fail_assignment_to_error.carbon

@@ -53,7 +53,7 @@ fn Main() {
 // CHECK:STDOUT:   %int_42.loc16: Core.IntLiteral = int_value 42 [concrete = constants.%int_42]
 // CHECK:STDOUT:   assign %undeclared.ref, <error>
 // CHECK:STDOUT:   %also_undeclared.ref: <error> = name_ref also_undeclared, <error> [concrete = <error>]
-// CHECK:STDOUT:   %.loc21: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc21: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %int_42.loc21: Core.IntLiteral = int_value 42 [concrete = constants.%int_42]
 // CHECK:STDOUT:   assign %.loc21, <error>
 // CHECK:STDOUT:   return

+ 4 - 4
toolchain/check/testdata/packages/fail_import_type_error.carbon

@@ -180,13 +180,13 @@ var d: i32 = d_ref;
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %a_ref.ref: <error> = name_ref a_ref, imports.%Implicit.a_ref
+// CHECK:STDOUT:   %a_ref.ref: <error> = name_ref a_ref, imports.%Implicit.a_ref [concrete = <error>]
 // CHECK:STDOUT:   assign file.%a.var, <error>
-// CHECK:STDOUT:   %b_ref.ref: <error> = name_ref b_ref, imports.%Implicit.b_ref
+// CHECK:STDOUT:   %b_ref.ref: <error> = name_ref b_ref, imports.%Implicit.b_ref [concrete = <error>]
 // CHECK:STDOUT:   assign file.%b.var, <error>
-// CHECK:STDOUT:   %c_ref.ref: <error> = name_ref c_ref, imports.%Implicit.c_ref
+// CHECK:STDOUT:   %c_ref.ref: <error> = name_ref c_ref, imports.%Implicit.c_ref [concrete = <error>]
 // CHECK:STDOUT:   assign file.%c.var, <error>
-// CHECK:STDOUT:   %d_ref.ref: <error> = name_ref d_ref, imports.%Implicit.d_ref
+// CHECK:STDOUT:   %d_ref.ref: <error> = name_ref d_ref, imports.%Implicit.d_ref [concrete = <error>]
 // CHECK:STDOUT:   assign file.%d.var, <error>
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/packages/no_prelude/core_name_poisoning.carbon

@@ -35,7 +35,7 @@ class r#Core {}
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %x.patt: <error> = binding_pattern x
-// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0
+// CHECK:STDOUT:     %x.param_patt: <error> = value_param_pattern %x.patt, runtime_param0 [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: <error> = value_param runtime_param0
 // CHECK:STDOUT:     %x: <error> = bind_name x, %x.param

+ 2 - 2
toolchain/check/testdata/pointer/fail_deref_error.carbon

@@ -63,9 +63,9 @@ let n2: i32 = undeclared->foo;
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %undeclared.ref.loc15: <error> = name_ref undeclared, <error> [concrete = <error>]
-// CHECK:STDOUT:   %.loc15: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc15: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %undeclared.ref.loc20: <error> = name_ref undeclared, <error> [concrete = <error>]
-// CHECK:STDOUT:   %.loc20: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc20: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/pointer/fail_deref_function.carbon

@@ -47,9 +47,9 @@ fn A() {
 // CHECK:STDOUT: fn @A() {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %A.ref.loc16: %A.type = name_ref A, file.%A.decl [concrete = constants.%A]
-// CHECK:STDOUT:   %.loc16: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc16: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %A.ref.loc21: %A.type = name_ref A, file.%A.decl [concrete = constants.%A]
-// CHECK:STDOUT:   %.loc21: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc21: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/pointer/fail_deref_namespace.carbon

@@ -51,9 +51,9 @@ fn F() {
 // CHECK:STDOUT: fn @F() {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %A.ref.loc18: <namespace> = name_ref A, file.%A [concrete = file.%A]
-// CHECK:STDOUT:   %.loc18: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc18: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %A.ref.loc23: <namespace> = name_ref A, file.%A [concrete = file.%A]
-// CHECK:STDOUT:   %.loc23: ref <error> = deref <error>
+// CHECK:STDOUT:   %.loc23: ref <error> = deref <error> [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 6 - 6
toolchain/check/testdata/pointer/fail_deref_not_pointer.carbon

@@ -84,27 +84,27 @@ fn Deref(n: i32) {
 // CHECK:STDOUT: fn @Deref(%n.param_patt: %i32) {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %n.ref.loc16: %i32 = name_ref n, %n
-// CHECK:STDOUT:   %.loc16: ref <error> = deref %n.ref.loc16
+// CHECK:STDOUT:   %.loc16: ref <error> = deref %n.ref.loc16 [concrete = <error>]
 // CHECK:STDOUT:   %n.ref.loc21: %i32 = name_ref n, %n
-// CHECK:STDOUT:   %.loc21: ref <error> = deref %n.ref.loc21
+// CHECK:STDOUT:   %.loc21: ref <error> = deref %n.ref.loc21 [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref.loc21: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   %.loc26_5.1: %empty_tuple.type = tuple_literal ()
 // CHECK:STDOUT:   %empty_tuple.loc26: %empty_tuple.type = tuple_value () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %.loc26_5.2: %empty_tuple.type = converted %.loc26_5.1, %empty_tuple.loc26 [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.loc26_3: ref <error> = deref %.loc26_5.2
+// CHECK:STDOUT:   %.loc26_3: ref <error> = deref %.loc26_5.2 [concrete = <error>]
 // CHECK:STDOUT:   %.loc31_4.1: %empty_tuple.type = tuple_literal ()
 // CHECK:STDOUT:   %empty_tuple.loc31: %empty_tuple.type = tuple_value () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %.loc31_4.2: %empty_tuple.type = converted %.loc31_4.1, %empty_tuple.loc31 [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.loc31_5: ref <error> = deref %.loc31_4.2
+// CHECK:STDOUT:   %.loc31_5: ref <error> = deref %.loc31_4.2 [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref.loc31: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   %.loc36_5.1: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %empty_struct.loc36: %empty_struct_type = struct_value () [concrete = constants.%empty_struct]
 // CHECK:STDOUT:   %.loc36_5.2: %empty_struct_type = converted %.loc36_5.1, %empty_struct.loc36 [concrete = constants.%empty_struct]
-// CHECK:STDOUT:   %.loc36_3: ref <error> = deref %.loc36_5.2
+// CHECK:STDOUT:   %.loc36_3: ref <error> = deref %.loc36_5.2 [concrete = <error>]
 // CHECK:STDOUT:   %.loc41_4.1: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %empty_struct.loc41: %empty_struct_type = struct_value () [concrete = constants.%empty_struct]
 // CHECK:STDOUT:   %.loc41_4.2: %empty_struct_type = converted %.loc41_4.1, %empty_struct.loc41 [concrete = constants.%empty_struct]
-// CHECK:STDOUT:   %.loc41_5: ref <error> = deref %.loc41_4.2
+// CHECK:STDOUT:   %.loc41_5: ref <error> = deref %.loc41_4.2 [concrete = <error>]
 // CHECK:STDOUT:   %foo.ref.loc41: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/pointer/fail_deref_type.carbon

@@ -52,7 +52,7 @@ var p2: i32->foo;
 // CHECK:STDOUT:   %.1: <error> = splice_block <error> [concrete = <error>] {
 // CHECK:STDOUT:     %int_32.loc18: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
 // CHECK:STDOUT:     %i32.loc18: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
-// CHECK:STDOUT:     %.loc18_8: ref <error> = deref %i32.loc18
+// CHECK:STDOUT:     %.loc18_8: ref <error> = deref %i32.loc18 [concrete = <error>]
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %p: <error> = bind_name p, <error>
 // CHECK:STDOUT:   name_binding_decl {
@@ -63,7 +63,7 @@ var p2: i32->foo;
 // CHECK:STDOUT:   %.2: <error> = splice_block <error> [concrete = <error>] {
 // CHECK:STDOUT:     %int_32.loc23: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
 // CHECK:STDOUT:     %i32.loc23: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
-// CHECK:STDOUT:     %.loc23_12: ref <error> = deref %i32.loc23
+// CHECK:STDOUT:     %.loc23_12: ref <error> = deref %i32.loc23 [concrete = <error>]
 // CHECK:STDOUT:     %foo.ref: <error> = name_ref foo, <error> [concrete = <error>]
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %p2: <error> = bind_name p2, <error>

+ 1 - 1
toolchain/check/testdata/struct/no_prelude/fail_nested_incomplete.carbon

@@ -62,7 +62,7 @@ var p: Incomplete* = &s.a;
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %s.ref: <error> = name_ref s, file.%s
+// CHECK:STDOUT:   %s.ref: <error> = name_ref s, file.%s [concrete = <error>]
 // CHECK:STDOUT:   %a.ref: <error> = name_ref a, <error> [concrete = <error>]
 // CHECK:STDOUT:   %addr: <error> = addr_of %a.ref [concrete = <error>]
 // CHECK:STDOUT:   assign file.%p.var, <error>

+ 1 - 1
toolchain/check/testdata/tuple/fail_nested_incomplete.carbon

@@ -79,7 +79,7 @@ var p: Incomplete* = &t[1];
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @__global_init() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %t.ref: <error> = name_ref t, file.%t
+// CHECK:STDOUT:   %t.ref: <error> = name_ref t, file.%t [concrete = <error>]
 // CHECK:STDOUT:   %int_1: Core.IntLiteral = int_value 1 [concrete = constants.%int_1]
 // CHECK:STDOUT:   %addr: <error> = addr_of <error> [concrete = <error>]
 // CHECK:STDOUT:   assign file.%p.var, <error>

+ 1 - 1
toolchain/check/testdata/where_expr/designator.carbon

@@ -275,7 +275,7 @@ class D {
 // CHECK:STDOUT:   %J.decl: type = interface_decl @J [concrete = constants.%J.type] {} {}
 // CHECK:STDOUT:   %PeriodMismatch.decl: %PeriodMismatch.type = fn_decl @PeriodMismatch [concrete = constants.%PeriodMismatch] {
 // CHECK:STDOUT:     %W.patt.loc12_19.1: <error> = symbolic_binding_pattern W, 0 [symbolic = %W.patt.loc12_19.2 (constants.%W.patt)]
-// CHECK:STDOUT:     %W.param_patt: <error> = value_param_pattern %W.patt.loc12_19.1, runtime_param<none> [symbolic = %W.patt.loc12_19.2 (constants.%W.patt)]
+// CHECK:STDOUT:     %W.param_patt: <error> = value_param_pattern %W.patt.loc12_19.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %W.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc12_25.1: type = splice_block %.loc12_25.2 [concrete = <error>] {

+ 1 - 1
toolchain/check/testdata/where_expr/equal_rewrite.carbon

@@ -1335,7 +1335,7 @@ let K: (E where .F = .Self.G) = bool;
 // CHECK:STDOUT:   %I.decl: type = interface_decl @I [concrete = constants.%I.type] {} {}
 // CHECK:STDOUT:   %RewriteTypeMismatch.decl: %RewriteTypeMismatch.type = fn_decl @RewriteTypeMismatch [concrete = constants.%RewriteTypeMismatch] {
 // CHECK:STDOUT:     %X.patt.loc16_24.1: <error> = symbolic_binding_pattern X, 0 [symbolic = %X.patt.loc16_24.2 (constants.%X.patt)]
-// CHECK:STDOUT:     %X.param_patt: <error> = value_param_pattern %X.patt.loc16_24.1, runtime_param<none> [symbolic = %X.patt.loc16_24.2 (constants.%X.patt)]
+// CHECK:STDOUT:     %X.param_patt: <error> = value_param_pattern %X.patt.loc16_24.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %X.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc16_30.1: type = splice_block %.loc16_30.2 [concrete = <error>] {

+ 2 - 2
toolchain/check/testdata/where_expr/fail_not_facet.carbon

@@ -67,7 +67,7 @@ var v: e where .x = 3;
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %T.patt.loc8_6.1: <error> = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc8_6.2 (constants.%T.patt)]
-// CHECK:STDOUT:     %T.param_patt: <error> = value_param_pattern %T.patt.loc8_6.1, runtime_param<none> [symbolic = %T.patt.loc8_6.2 (constants.%T.patt)]
+// CHECK:STDOUT:     %T.param_patt: <error> = value_param_pattern %T.patt.loc8_6.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %T.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc8_14.1: type = splice_block %.loc8_14.2 [concrete = <error>] {
@@ -121,7 +121,7 @@ var v: e where .x = 3;
 // CHECK:STDOUT:   %Core.import = import Core
 // CHECK:STDOUT:   %G.decl: %G.type = fn_decl @G [concrete = constants.%G] {
 // CHECK:STDOUT:     %U.patt.loc8_6.1: <error> = symbolic_binding_pattern U, 0 [symbolic = %U.patt.loc8_6.2 (constants.%U.patt)]
-// CHECK:STDOUT:     %U.param_patt: <error> = value_param_pattern %U.patt.loc8_6.1, runtime_param<none> [symbolic = %U.patt.loc8_6.2 (constants.%U.patt)]
+// CHECK:STDOUT:     %U.param_patt: <error> = value_param_pattern %U.patt.loc8_6.1, runtime_param<none> [concrete = <error>]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %U.param: <error> = value_param runtime_param<none>
 // CHECK:STDOUT:     %.loc8_23.1: type = splice_block %.loc8_23.2 [concrete = <error>] {

+ 2 - 0
toolchain/lower/constant.cpp

@@ -226,6 +226,8 @@ template <typename InstT>
 static auto MaybeEmitAsConstant(ConstantContext& context, InstT inst)
     -> llvm::Constant* {
   if constexpr (InstT::Kind.constant_kind() == SemIR::InstConstantKind::Never ||
+                InstT::Kind.constant_kind() ==
+                    SemIR::InstConstantKind::Indirect ||
                 InstT::Kind.constant_kind() ==
                     SemIR::InstConstantKind::SymbolicOnly) {
     CARBON_FATAL("Unexpected constant instruction kind {0}", inst);

+ 3 - 1
toolchain/lower/function_context.cpp

@@ -72,7 +72,9 @@ static auto LowerInstHelper(FunctionContext& context, SemIR::InstId inst_id,
         "instruction in lowered contexts. Instruction: {0}",
         inst);
   } else if constexpr (InstT::Kind.constant_kind() ==
-                       SemIR::InstConstantKind::Always) {
+                           SemIR::InstConstantKind::Always ||
+                       InstT::Kind.constant_kind() ==
+                           SemIR::InstConstantKind::Unique) {
     CARBON_FATAL("Missing constant value for constant instruction {0}", inst);
   } else if constexpr (InstT::Kind.is_type() == SemIR::InstIsType::Always) {
     // For instructions that are always of type `type`, produce the trivial

+ 4 - 0
toolchain/sem_ir/formatter.cpp

@@ -1349,6 +1349,10 @@ class FormatterImpl {
     FormatName(static_cast<InstId>(id));
   }
 
+  auto FormatName(DestInstId id) -> void {
+    FormatName(static_cast<InstId>(id));
+  }
+
   auto FormatName(SpecificId id) -> void {
     const auto& specific = sem_ir_->specifics().Get(id);
     FormatName(specific.generic_id);

+ 1 - 1
toolchain/sem_ir/id_kind.h

@@ -139,7 +139,7 @@ using IdKind = TypeEnum<
     // From base/value_store.h.
     IntId, RealId, FloatId, StringLiteralValueId,
     // From sem_ir/ids.h.
-    InstId, AbsoluteInstId, AnyRawId, ConstantId, EntityNameId,
+    InstId, AbsoluteInstId, DestInstId, AnyRawId, ConstantId, EntityNameId,
     CompileTimeBindIndex, RuntimeParamIndex, FacetTypeId, FunctionId, ClassId,
     InterfaceId, AssociatedConstantId, ImplId, GenericId, SpecificId,
     ImportIRId, ImportIRInstId, LocId, BoolValue, IntKind, NameId, NameScopeId,

+ 25 - 3
toolchain/sem_ir/ids.h

@@ -74,6 +74,28 @@ class AbsoluteInstId : public InstId {
   using InstId::InstId;
 };
 
+// An ID of an instruction that is used as the destination of an initializing
+// expression. This should only be used as the type of a field within a typed
+// instruction class.
+//
+// This behaves in most respects like an InstId field, but constant evaluation
+// of an instruction with a destination field will not evaluate this field, and
+// substitution will not substitute into it.
+//
+// TODO: Decide on how substitution should handle this. Multiple instructions
+// can refer to the same destination, so these don't have the tree structure
+// that substitution expects, but we might need to substitute into the result of
+// an instruction.
+class DestInstId : public InstId {
+ public:
+  // Support implicit conversion from InstId so that InstId and DestInstId
+  // have the same interface.
+  // NOLINTNEXTLINE(google-explicit-constructor)
+  constexpr DestInstId(InstId inst_id) : InstId(inst_id) {}
+
+  using InstId::InstId;
+};
+
 // The ID of a constant value of an expression. An expression is either:
 //
 // - a concrete constant, whose value does not depend on any generic parameters,
@@ -110,17 +132,17 @@ struct ConstantId : public IdBase<ConstantId> {
   using IdBase::IdBase;
 
   // Returns whether this represents a constant. Requires has_value.
-  auto is_constant() const -> bool {
+  constexpr auto is_constant() const -> bool {
     CARBON_DCHECK(has_value());
     return *this != ConstantId::NotConstant;
   }
   // Returns whether this represents a symbolic constant. Requires has_value.
-  auto is_symbolic() const -> bool {
+  constexpr auto is_symbolic() const -> bool {
     CARBON_DCHECK(has_value());
     return index <= FirstSymbolicIndex;
   }
   // Returns whether this represents a concrete constant. Requires has_value.
-  auto is_concrete() const -> bool {
+  constexpr auto is_concrete() const -> bool {
     CARBON_DCHECK(has_value());
     return index >= 0;
   }

+ 2 - 1
toolchain/sem_ir/inst.h

@@ -286,7 +286,8 @@ class Inst : public Printable<Inst> {
   // Raw constructor, used for testing.
   explicit Inst(InstKind kind, TypeId type_id, int32_t arg0, int32_t arg1)
       : Inst(kind.AsInt(), type_id, arg0, arg1) {}
-  explicit Inst(int32_t kind, TypeId type_id, int32_t arg0, int32_t arg1)
+  explicit constexpr Inst(int32_t kind, TypeId type_id, int32_t arg0,
+                          int32_t arg1)
       : kind_(kind), type_id_(type_id), arg0_(arg0), arg1_(arg1) {}
 
   int32_t kind_;

+ 40 - 24
toolchain/sem_ir/inst_kind.h

@@ -7,10 +7,7 @@
 
 #include <cstdint>
 
-#include "common/check.h"
 #include "common/enum_base.h"
-#include "llvm/ADT/FoldingSet.h"
-
 namespace Carbon::SemIR {
 
 // Whether an instruction defines a type.
@@ -35,28 +32,51 @@ enum class InstValueKind : int8_t {
   Typed,
 };
 
-// Whether an instruction can be used to define a constant value. This specifies
-// whether the instruction can be added to the `constants()` list. Note that
-// even instructions that cannot define a constant value can still have an
-// associated `constant_value()`, but the constant value will be a different
-// kind of instruction.
+// Whether an instruction can have a constant value, and whether it can be used
+// to define a constant value.
+//
+// This specifies whether an instruction of this kind can have a corresponding
+// constant value in the `constant_values()` list, and whether an instruction of
+// this kind can be added to the `constants()` list.
 enum class InstConstantKind : int8_t {
-  // This instruction never defines a constant value. For example,
-  // `UnaryOperatorNot` never defines a constant value; if its operand is a
-  // concrete constant, its constant value will instead be a `BoolLiteral`. This
-  // is also used for instructions that don't produce a value at all.
+  // This instruction is never constant. Its constant value is always
+  // `NotConstant`. This is also used for instructions that don't produce a
+  // value at all and aren't used as constants.
   Never,
-  // This instruction may be a symbolic constant, depending on its operands, but
-  // is never a concrete constant. For example, a `Call` instruction can be a
-  // symbolic constant but never a concrete constant.
+  // This instruction never defines a constant value, but can evaluate to a
+  // constant value of a different kind. For example, `UnaryOperatorNot` never
+  // defines a constant value; if its operand is a concrete constant, its
+  // constant value will instead be a `BoolLiteral`, and if its operand is not a
+  // concrete constant, the result is non-constant. This is the default.
+  Indirect,
+  // This instruction may define a symbolic constant, depending on its operands,
+  // but never a concrete constant. For example, a `Call` instruction can define
+  // a symbolic constant but never a concrete constant. The instruction may have
+  // a concrete constant value of a different kind.
   SymbolicOnly,
   // This instruction can define a symbolic or concrete constant, but might not
-  // have a constant value, depending on its operands. For example, a
-  // `TupleValue` can define a constant if its operands are constants.
+  // have a constant value, might have a constant value that is not defined by
+  // itself, or might result in a compile-time error, depending on its operands.
+  // For example, `ArrayType` is a compile-time constant if its operands are
+  // constant and its array bound is within a valid range.
   Conditional,
-  // This instruction always has a constant value of the same kind. For example,
-  // `IntValue`.
+  // This instruction defines a symbolic or concrete constant whenever its
+  // operands are constant. Otherwise, it is non-constant. For example, a
+  // `TupleValue` defines a constant if and only if its operands are constants.
+  // Constant evaluation support for types with this constant kind is provided
+  // automatically.
+  WheneverPossible,
+  // This instruction always has a constant value of the same kind. This is the
+  // same as `WheneverPossible`, except that the operands are known in advance
+  // to always be constant. For example, `IntValue`.
   Always,
+  // This instruction is itself a unique constant. This is used for declarations
+  // whose constant identity is simply themselves. The `ConstantId` for this
+  // instruction will always be a concrete constant whose `InstId` refers
+  // directly back to the instruction, rather than to a separate instrinction in
+  // the constants block.
+  // TODO: Decide if this is the model we want for these cases.
+  Unique,
 };
 
 // Whether an instruction is a terminator or part of the terminator sequence.
@@ -91,7 +111,7 @@ class InstKind : public CARBON_ENUM_BASE(InstKind) {
   struct DefinitionInfo {
     llvm::StringLiteral ir_name;
     InstIsType is_type = InstIsType::Never;
-    InstConstantKind constant_kind = InstConstantKind::Never;
+    InstConstantKind constant_kind = InstConstantKind::Indirect;
     TerminatorKind terminator_kind = TerminatorKind::NotTerminator;
     bool is_lowered = true;
     bool deduce_through = false;
@@ -143,10 +163,6 @@ class InstKind : public CARBON_ENUM_BASE(InstKind) {
     return definition_info(*this).deduce_through;
   }
 
-  // Compute a fingerprint for this instruction kind, allowing its use as part
-  // of the key in a `FoldingSet`.
-  auto Profile(llvm::FoldingSetNodeID& id) -> void { id.AddInteger(AsInt()); }
-
  private:
   // Returns the DefinitionInfo for the kind.
   static auto definition_info(InstKind kind) -> const DefinitionInfo&;

+ 138 - 92
toolchain/sem_ir/typed_insts.h

@@ -48,32 +48,6 @@
 
 namespace Carbon::SemIR {
 
-// Used for the type of patterns that do not match a fixed type.
-struct AutoType {
-  static constexpr auto Kind = InstKind::AutoType.Define<Parse::NoneNodeId>(
-      {.ir_name = "auto",
-       .is_type = InstIsType::Always,
-       .constant_kind = InstConstantKind::Always});
-  static constexpr auto SingletonInstId = MakeSingletonInstId<Kind>();
-  static constexpr auto SingletonTypeId =
-      TypeId::ForTypeConstant(ConstantId::ForConcreteConstant(SingletonInstId));
-
-  TypeId type_id;
-};
-
-// The type of bool literals and branch conditions, bool.
-struct BoolType {
-  static constexpr auto Kind = InstKind::BoolType.Define<Parse::NoneNodeId>(
-      {.ir_name = "bool",
-       .is_type = InstIsType::Always,
-       .constant_kind = InstConstantKind::Always});
-  // This is a singleton instruction. However, it may still evolve into a more
-  // standard type and be removed.
-  static constexpr auto SingletonInstId = MakeSingletonInstId<Kind>();
-
-  TypeId type_id;
-};
-
 // Common representation for declarations describing the foundation type of a
 // class -- either its adapted type or its base class.
 struct AnyFoundationDecl {
@@ -89,7 +63,7 @@ struct AnyFoundationDecl {
 struct AdaptDecl {
   static constexpr auto Kind = InstKind::AdaptDecl.Define<Parse::AdaptDeclId>(
       {.ir_name = "adapt_decl",
-       .constant_kind = InstConstantKind::Always,
+       .constant_kind = InstConstantKind::Unique,
        .is_lowered = false});
 
   // No type_id; this is not a value.
@@ -101,7 +75,8 @@ struct AdaptDecl {
 struct AddrOf {
   // Parse node is usually Parse::PrefixOperatorAmpId.
   static constexpr auto Kind = InstKind::AddrOf.Define<Parse::NodeId>(
-      {.ir_name = "addr_of", .constant_kind = InstConstantKind::Conditional});
+      {.ir_name = "addr_of",
+       .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   InstId lvalue_id;
@@ -111,7 +86,9 @@ struct AddrOf {
 // generally be a pattern inst.
 struct AddrPattern {
   static constexpr auto Kind = InstKind::AddrPattern.Define<Parse::AddrId>(
-      {.ir_name = "addr_pattern", .is_lowered = false});
+      {.ir_name = "addr_pattern",
+       .constant_kind = InstConstantKind::Never,
+       .is_lowered = false});
 
   TypeId type_id;
   // The `self` binding.
@@ -153,7 +130,7 @@ struct AnyAggregateInit {
   InstKind kind;
   TypeId type_id;
   InstBlockId elements_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // Common representation for all kinds of aggregate value.
@@ -175,7 +152,7 @@ struct ArrayInit {
 
   TypeId type_id;
   InstBlockId inits_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // An array of `element_type_id` values, sized to `bound_id`.
@@ -205,8 +182,8 @@ struct AsCompatible {
 // `InitializeFrom`.
 struct Assign {
   // TODO: Make Parse::NodeId more specific.
-  static constexpr auto Kind =
-      InstKind::Assign.Define<Parse::NodeId>({.ir_name = "assign"});
+  static constexpr auto Kind = InstKind::Assign.Define<Parse::NodeId>(
+      {.ir_name = "assign", .constant_kind = InstConstantKind::Never});
 
   // Assignments are statements, and so have no type.
   InstId lhs_id;
@@ -218,7 +195,9 @@ struct AssociatedConstantDecl {
   static constexpr auto Kind =
       InstKind::AssociatedConstantDecl
           .Define<Parse::CompileTimeBindingPatternId>(
-              {.ir_name = "assoc_const_decl", .is_lowered = false});
+              {.ir_name = "assoc_const_decl",
+               .constant_kind = InstConstantKind::Unique,
+               .is_lowered = false});
 
   TypeId type_id;
   AssociatedConstantId assoc_const_id;
@@ -246,7 +225,7 @@ struct AssociatedEntityType {
       InstKind::AssociatedEntityType.Define<Parse::NoneNodeId>(
           {.ir_name = "assoc_entity_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional});
+           .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   // The interface in which the entity was declared.
@@ -254,12 +233,25 @@ struct AssociatedEntityType {
   TypeId interface_type_id;
 };
 
+// Used for the type of patterns that do not match a fixed type.
+struct AutoType {
+  static constexpr auto Kind = InstKind::AutoType.Define<Parse::NoneNodeId>(
+      {.ir_name = "auto",
+       .is_type = InstIsType::Always,
+       .constant_kind = InstConstantKind::Always});
+  static constexpr auto SingletonInstId = MakeSingletonInstId<Kind>();
+  static constexpr auto SingletonTypeId =
+      TypeId::ForTypeConstant(ConstantId::ForConcreteConstant(SingletonInstId));
+
+  TypeId type_id;
+};
+
 // A base in a class, of the form `base: base_type;`. A base class is an
 // element of the derived class, and the type of the `BaseDecl` instruction is
 // an `UnboundElementType`.
 struct BaseDecl {
   static constexpr auto Kind = InstKind::BaseDecl.Define<Parse::BaseDeclId>(
-      {.ir_name = "base_decl", .constant_kind = InstConstantKind::Always});
+      {.ir_name = "base_decl", .constant_kind = InstConstantKind::Unique});
 
   TypeId type_id;
   InstId base_type_inst_id;
@@ -304,8 +296,8 @@ struct BindAlias {
 // Binds a name, such as `x` in `var x: i32`.
 struct BindName {
   // TODO: Make Parse::NodeId more specific.
-  static constexpr auto Kind =
-      InstKind::BindName.Define<Parse::NodeId>({.ir_name = "bind_name"});
+  static constexpr auto Kind = InstKind::BindName.Define<Parse::NodeId>(
+      {.ir_name = "bind_name", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   EntityNameId entity_name_id;
@@ -350,7 +342,9 @@ struct AnyBindingPattern {
 // Represents a non-symbolic binding pattern.
 struct BindingPattern {
   static constexpr auto Kind = InstKind::BindingPattern.Define<Parse::NodeId>(
-      {.ir_name = "binding_pattern", .is_lowered = false});
+      {.ir_name = "binding_pattern",
+       .constant_kind = InstConstantKind::Never,
+       .is_lowered = false});
 
   TypeId type_id;
   EntityNameId entity_name_id;
@@ -371,8 +365,8 @@ struct SymbolicBindingPattern {
 
 // Reads an argument from `BranchWithArg`.
 struct BlockArg {
-  static constexpr auto Kind =
-      InstKind::BlockArg.Define<Parse::NodeId>({.ir_name = "block_arg"});
+  static constexpr auto Kind = InstKind::BlockArg.Define<Parse::NodeId>(
+      {.ir_name = "block_arg", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   LabelId block_id;
@@ -388,13 +382,26 @@ struct BoolLiteral {
   BoolValue value;
 };
 
+// The type of bool literals and branch conditions, bool.
+struct BoolType {
+  static constexpr auto Kind = InstKind::BoolType.Define<Parse::NoneNodeId>(
+      {.ir_name = "bool",
+       .is_type = InstIsType::Always,
+       .constant_kind = InstConstantKind::Always});
+  // This is a singleton instruction. However, it may still evolve into a more
+  // standard type and be removed.
+  static constexpr auto SingletonInstId = MakeSingletonInstId<Kind>();
+
+  TypeId type_id;
+};
+
 // For member access such as `object.MethodName`, combines a member function
 // with the value to use for `self`. This is a callable structure; `Call` will
 // handle the argument assignment.
 struct BoundMethod {
   static constexpr auto Kind = InstKind::BoundMethod.Define<Parse::NodeId>(
       {.ir_name = "bound_method",
-       .constant_kind = InstConstantKind::Conditional});
+       .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   // The object argument in the bound method, which will be used to initialize
@@ -435,7 +442,9 @@ struct AnyBranch {
 struct Branch {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::Branch.Define<Parse::NodeId>(
-      {.ir_name = "br", .terminator_kind = TerminatorKind::Terminator});
+      {.ir_name = "br",
+       .constant_kind = InstConstantKind::Never,
+       .terminator_kind = TerminatorKind::Terminator});
 
   // Branches don't produce a value, so have no type.
   LabelId target_id;
@@ -445,7 +454,9 @@ struct Branch {
 struct BranchIf {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::BranchIf.Define<Parse::NodeId>(
-      {.ir_name = "br", .terminator_kind = TerminatorKind::TerminatorSequence});
+      {.ir_name = "br",
+       .constant_kind = InstConstantKind::Never,
+       .terminator_kind = TerminatorKind::TerminatorSequence});
 
   // Branches don't produce a value, so have no type.
   LabelId target_id;
@@ -457,7 +468,9 @@ struct BranchIf {
 struct BranchWithArg {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::BranchWithArg.Define<Parse::NodeId>(
-      {.ir_name = "br", .terminator_kind = TerminatorKind::Terminator});
+      {.ir_name = "br",
+       .constant_kind = InstConstantKind::Never,
+       .terminator_kind = TerminatorKind::Terminator});
 
   // Branches don't produce a value, so have no type.
   LabelId target_id;
@@ -517,7 +530,7 @@ struct ClassInit {
 
   TypeId type_id;
   InstBlockId elements_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // The type for a class, either non-generic or specific.
@@ -572,7 +585,9 @@ struct Converted {
       InstKind::Converted.Define<Parse::NodeId>({.ir_name = "converted"});
 
   TypeId type_id;
-  InstId original_id;
+  // The operand prior to being converted. This is tracked only for tooling
+  // purposes and has no associated semantics.
+  AbsoluteInstId original_id;
   InstId result_id;
 };
 
@@ -674,7 +689,7 @@ struct FacetValue {
 struct FieldDecl {
   static constexpr auto Kind =
       InstKind::FieldDecl.Define<Parse::VarBindingPatternId>(
-          {.ir_name = "field_decl", .constant_kind = InstConstantKind::Always});
+          {.ir_name = "field_decl", .constant_kind = InstConstantKind::Unique});
 
   TypeId type_id;
   NameId name_id;
@@ -741,7 +756,7 @@ struct FunctionType {
       InstKind::FunctionType.Define<Parse::AnyFunctionDeclId>(
           {.ir_name = "fn_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional});
+           .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   FunctionId function_id;
@@ -756,7 +771,7 @@ struct FunctionTypeWithSelfType {
       InstKind::FunctionTypeWithSelfType.Define<Parse::NoneNodeId>(
           {.ir_name = "fn_type_with_self_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional,
+           .constant_kind = InstConstantKind::WheneverPossible,
            .is_lowered = false});
 
   TypeId type_id;
@@ -776,7 +791,7 @@ struct GenericClassType {
       InstKind::GenericClassType.Define<Parse::NoneNodeId>(
           {.ir_name = "generic_class_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional});
+           .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   ClassId class_id;
@@ -791,7 +806,7 @@ struct GenericInterfaceType {
       InstKind::GenericInterfaceType.Define<Parse::NoneNodeId>(
           {.ir_name = "generic_interface_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional});
+           .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   InterfaceId interface_id;
@@ -802,7 +817,9 @@ struct GenericInterfaceType {
 struct ImplDecl {
   static constexpr auto Kind = InstKind::ImplDecl.Define<Parse::AnyImplDeclId>(
       {.ir_name = "impl_decl",
-       .constant_kind = InstConstantKind::Always,
+       // TODO: Modeling impls as unique doesn't properly handle impl
+       // redeclarations.
+       .constant_kind = InstConstantKind::Unique,
        .is_lowered = false});
 
   // No type: an impl declaration is not a value.
@@ -816,7 +833,7 @@ struct ImplDecl {
 struct ImplWitness {
   static constexpr auto Kind = InstKind::ImplWitness.Define<Parse::NodeId>(
       {.ir_name = "impl_witness",
-       .constant_kind = InstConstantKind::Conditional,
+       .constant_kind = InstConstantKind::WheneverPossible,
        // TODO: For dynamic dispatch, we might want to lower witness tables as
        // constants.
        .is_lowered = false});
@@ -845,14 +862,18 @@ struct ImplWitnessAccess {
 struct ImportCppDecl {
   static constexpr auto Kind =
       InstKind::ImportCppDecl.Define<Parse::ImportDeclId>(
-          {.ir_name = "import_cpp", .is_lowered = false});
+          {.ir_name = "import_cpp",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 };
 
 // An `import` declaration. This is mainly for `import` diagnostics, and a 1:1
 // correspondence with actual `import`s isn't guaranteed.
 struct ImportDecl {
   static constexpr auto Kind = InstKind::ImportDecl.Define<Parse::ImportDeclId>(
-      {.ir_name = "import", .is_lowered = false});
+      {.ir_name = "import",
+       .constant_kind = InstConstantKind::Never,
+       .is_lowered = false});
 
   NameId package_id;
 };
@@ -901,7 +922,7 @@ struct InitializeFrom {
 
   TypeId type_id;
   InstId src_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // An interface declaration.
@@ -968,7 +989,8 @@ struct IntType {
 struct NameBindingDecl {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::NameBindingDecl.Define<Parse::NodeId>(
-      {.ir_name = "name_binding_decl"});
+      {.ir_name = "name_binding_decl",
+       .constant_kind = InstConstantKind::Never});
 
   InstBlockId pattern_block_id;
 };
@@ -989,7 +1011,10 @@ struct NameRef {
 struct Namespace {
   static constexpr auto Kind =
       InstKind::Namespace.Define<Parse::AnyNamespaceId>(
-          {.ir_name = "namespace", .constant_kind = InstConstantKind::Always});
+          {.ir_name = "namespace",
+           // TODO: Modeling namespaces as unique doesn't properly handle
+           // namespace redeclarations.
+           .constant_kind = InstConstantKind::Unique});
   // The file's package namespace is a well-known instruction to help `package.`
   // qualified names. It will always be immediately after singletons.
   static constexpr InstId PackageInstId = InstId(SingletonInstKinds.size());
@@ -1035,8 +1060,8 @@ struct AnyParam {
 // An output parameter. See AnyParam for member documentation.
 struct OutParam {
   // TODO: Make Parse::NodeId more specific.
-  static constexpr auto Kind =
-      InstKind::OutParam.Define<Parse::NodeId>({.ir_name = "out_param"});
+  static constexpr auto Kind = InstKind::OutParam.Define<Parse::NodeId>(
+      {.ir_name = "out_param", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   RuntimeParamIndex runtime_index;
@@ -1046,8 +1071,8 @@ struct OutParam {
 // A by-value parameter. See AnyParam for member documentation.
 struct ValueParam {
   // TODO: Make Parse::NodeId more specific.
-  static constexpr auto Kind =
-      InstKind::ValueParam.Define<Parse::NodeId>({.ir_name = "value_param"});
+  static constexpr auto Kind = InstKind::ValueParam.Define<Parse::NodeId>(
+      {.ir_name = "value_param", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   RuntimeParamIndex runtime_index;
@@ -1071,7 +1096,9 @@ struct AnyParamPattern {
 struct OutParamPattern {
   static constexpr auto Kind =
       InstKind::OutParamPattern.Define<Parse::ReturnTypeId>(
-          {.ir_name = "out_param_pattern", .is_lowered = false});
+          {.ir_name = "out_param_pattern",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   TypeId type_id;
   InstId subpattern_id;
@@ -1097,7 +1124,7 @@ struct PointerType {
       InstKind::PointerType.Define<Parse::PostfixOperatorStarId>(
           {.ir_name = "ptr_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional,
+           .constant_kind = InstConstantKind::WheneverPossible,
            .deduce_through = true});
 
   TypeId type_id;
@@ -1126,7 +1153,9 @@ struct Return {
   static constexpr auto Kind =
       InstKind::Return.Define<Parse::NodeIdOneOf<Parse::FunctionDefinitionId,
                                                  Parse::ReturnStatementId>>(
-          {.ir_name = "return", .terminator_kind = TerminatorKind::Terminator});
+          {.ir_name = "return",
+           .constant_kind = InstConstantKind::Never,
+           .terminator_kind = TerminatorKind::Terminator});
 
   // This is a statement, so has no type.
 };
@@ -1135,20 +1164,22 @@ struct Return {
 struct ReturnExpr {
   static constexpr auto Kind =
       InstKind::ReturnExpr.Define<Parse::ReturnStatementId>(
-          {.ir_name = "return", .terminator_kind = TerminatorKind::Terminator});
+          {.ir_name = "return",
+           .constant_kind = InstConstantKind::Never,
+           .terminator_kind = TerminatorKind::Terminator});
 
   // This is a statement, so has no type.
   InstId expr_id;
   // The return slot, if any. `None` if we're not returning through memory.
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // The return slot of a function declaration, as exposed in the function body.
 // This acts as an output parameter, analogous to `BindName` for input
 // parameters.
 struct ReturnSlot {
-  static constexpr auto Kind =
-      InstKind::ReturnSlot.Define<Parse::NodeId>({.ir_name = "return_slot"});
+  static constexpr auto Kind = InstKind::ReturnSlot.Define<Parse::NodeId>(
+      {.ir_name = "return_slot", .constant_kind = InstConstantKind::Never});
 
   // The type of the value that will be stored in this slot (i.e. the return
   // type of the function).
@@ -1169,7 +1200,9 @@ struct ReturnSlot {
 struct ReturnSlotPattern {
   static constexpr auto Kind =
       InstKind::ReturnSlotPattern.Define<Parse::ReturnTypeId>(
-          {.ir_name = "return_slot_pattern", .is_lowered = false});
+          {.ir_name = "return_slot_pattern",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   // The type of the value that will be stored in this slot (i.e. the return
   // type of the function).
@@ -1185,7 +1218,9 @@ struct ReturnSlotPattern {
 struct RequirementEquivalent {
   static constexpr auto Kind =
       InstKind::RequirementEquivalent.Define<Parse::RequirementEqualEqualId>(
-          {.ir_name = "requirement_equivalent", .is_lowered = false});
+          {.ir_name = "requirement_equivalent",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   // No type since not an expression
   InstId lhs_id;
@@ -1196,7 +1231,9 @@ struct RequirementEquivalent {
 struct RequirementImpls {
   static constexpr auto Kind =
       InstKind::RequirementImpls.Define<Parse::RequirementImplsId>(
-          {.ir_name = "requirement_impls", .is_lowered = false});
+          {.ir_name = "requirement_impls",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   // No type since not an expression
   InstId lhs_id;
@@ -1207,7 +1244,9 @@ struct RequirementImpls {
 struct RequirementRewrite {
   static constexpr auto Kind =
       InstKind::RequirementRewrite.Define<Parse::RequirementEqualId>(
-          {.ir_name = "requirement_rewrite", .is_lowered = false});
+          {.ir_name = "requirement_rewrite",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   // No type since not an expression
   InstId lhs_id;
@@ -1241,7 +1280,7 @@ struct SpecificConstant {
 struct SpecificFunction {
   static constexpr auto Kind = InstKind::SpecificFunction.Define<Parse::NodeId>(
       {.ir_name = "specific_function",
-       .constant_kind = InstConstantKind::Conditional});
+       .constant_kind = InstConstantKind::WheneverPossible});
 
   // Always the builtin SpecificFunctionType.
   TypeId type_id;
@@ -1275,7 +1314,7 @@ struct SpliceBlock {
       InstKind::SpliceBlock.Define<Parse::NodeId>({.ir_name = "splice_block"});
 
   TypeId type_id;
-  InstBlockId block_id;
+  AbsoluteInstBlockId block_id;
   InstId result_id;
 };
 
@@ -1323,14 +1362,15 @@ struct StructInit {
 
   TypeId type_id;
   InstBlockId elements_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // A literal struct value, such as `{.a = 1, .b = 2}`.
 struct StructLiteral {
   static constexpr auto Kind =
       InstKind::StructLiteral.Define<Parse::StructLiteralId>(
-          {.ir_name = "struct_literal"});
+          {.ir_name = "struct_literal",
+           .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   InstBlockId elements_id;
@@ -1342,7 +1382,7 @@ struct StructType {
       InstKind::StructType.Define<Parse::StructTypeLiteralId>(
           {.ir_name = "struct_type",
            .is_type = InstIsType::Always,
-           .constant_kind = InstConstantKind::Conditional,
+           .constant_kind = InstConstantKind::WheneverPossible,
            .deduce_through = true});
 
   TypeId type_id;
@@ -1353,7 +1393,7 @@ struct StructType {
 struct StructValue {
   static constexpr auto Kind = InstKind::StructValue.Define<Parse::NodeId>(
       {.ir_name = "struct_value",
-       .constant_kind = InstConstantKind::Conditional});
+       .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   InstBlockId elements_id;
@@ -1365,7 +1405,7 @@ struct Temporary {
       InstKind::Temporary.Define<Parse::NodeId>({.ir_name = "temporary"});
 
   TypeId type_id;
-  InstId storage_id;
+  DestInstId storage_id;
   InstId init_id;
 };
 
@@ -1373,7 +1413,8 @@ struct Temporary {
 struct TemporaryStorage {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::TemporaryStorage.Define<Parse::NodeId>(
-      {.ir_name = "temporary_storage"});
+      {.ir_name = "temporary_storage",
+       .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
 };
@@ -1398,14 +1439,15 @@ struct TupleInit {
 
   TypeId type_id;
   InstBlockId elements_id;
-  InstId dest_id;
+  DestInstId dest_id;
 };
 
 // A literal tuple value.
 struct TupleLiteral {
   static constexpr auto Kind =
       InstKind::TupleLiteral.Define<Parse::TupleLiteralId>(
-          {.ir_name = "tuple_literal"});
+          {.ir_name = "tuple_literal",
+           .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   InstBlockId elements_id;
@@ -1415,7 +1457,9 @@ struct TupleLiteral {
 struct TuplePattern {
   static constexpr auto Kind =
       InstKind::TuplePattern.Define<Parse::TuplePatternId>(
-          {.ir_name = "tuple_pattern", .is_lowered = false});
+          {.ir_name = "tuple_pattern",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   TypeId type_id;
   InstBlockId elements_id;
@@ -1426,7 +1470,7 @@ struct TupleType {
   static constexpr auto Kind = InstKind::TupleType.Define<Parse::NoneNodeId>(
       {.ir_name = "tuple_type",
        .is_type = InstIsType::Always,
-       .constant_kind = InstConstantKind::Conditional,
+       .constant_kind = InstConstantKind::WheneverPossible,
        .deduce_through = true});
 
   TypeId type_id;
@@ -1437,7 +1481,7 @@ struct TupleType {
 struct TupleValue {
   static constexpr auto Kind = InstKind::TupleValue.Define<Parse::NodeId>(
       {.ir_name = "tuple_value",
-       .constant_kind = InstConstantKind::Conditional,
+       .constant_kind = InstConstantKind::WheneverPossible,
        .deduce_through = true});
 
   TypeId type_id;
@@ -1476,7 +1520,7 @@ struct UnboundElementType {
       Parse::NodeIdOneOf<Parse::BaseDeclId, Parse::VarBindingPatternId>>(
       {.ir_name = "unbound_element_type",
        .is_type = InstIsType::Always,
-       .constant_kind = InstConstantKind::Conditional});
+       .constant_kind = InstConstantKind::WheneverPossible});
 
   TypeId type_id;
   // The class that a value of this type is an element of.
@@ -1491,7 +1535,7 @@ struct UnboundElementType {
 // form a reference to the array object.
 struct ValueAsRef {
   static constexpr auto Kind = InstKind::ValueAsRef.Define<Parse::IndexExprId>(
-      {.ir_name = "value_as_ref"});
+      {.ir_name = "value_as_ref", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;
   InstId value_id;
@@ -1514,7 +1558,9 @@ struct ValueOfInitializer {
 struct VarPattern {
   static constexpr auto Kind =
       InstKind::VarPattern.Define<Parse::VariablePatternId>(
-          {.ir_name = "var_pattern", .is_lowered = false});
+          {.ir_name = "var_pattern",
+           .constant_kind = InstConstantKind::Never,
+           .is_lowered = false});
 
   TypeId type_id;
   InstId subpattern_id;
@@ -1523,8 +1569,8 @@ struct VarPattern {
 // Tracks storage for a `var` pattern.
 struct VarStorage {
   // TODO: Make Parse::NodeId more specific.
-  static constexpr auto Kind =
-      InstKind::VarStorage.Define<Parse::NodeId>({.ir_name = "var"});
+  static constexpr auto Kind = InstKind::VarStorage.Define<Parse::NodeId>(
+      {.ir_name = "var", .constant_kind = InstConstantKind::Never});
 
   TypeId type_id;