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

Add 'let' handling for ImportRefUsed. (#3700)

- File::StringifyTypeExpr now has a case that hits the ImportRefUsed
TODO, so implementing that. I think the `static` approach will be
helpful in ensuring there aren't access bugs, particularly when future
support is added.

- `let` wasn't adding to exports because it doesn't use
`decl_name_stack` the way `var` does. This now adds to exports, but we
might want to unify logic for issues such as this.

- BuildImportRefUsedValueRepr is now called for another inst kind, and
it seemed like calling back to BuildValueRepr was the best way to
resolve this. I don't *think* that's going to cause recursion.
Jon Ross-Perkins 2 лет назад
Родитель
Сommit
f76bc733f4

+ 14 - 12
toolchain/check/context.cpp

@@ -712,17 +712,9 @@ class TypeCompleter {
     return value_rep;
   };
 
-  auto BuildImportRefUsedValueRepr(SemIR::TypeId type_id,
-                                   SemIR::ImportRefUsed import_ref) const
-      -> SemIR::ValueRepr {
-    CARBON_CHECK(import_ref.inst_id.is_builtin())
-        << "TODO: Handle non-builtin ImportRefUsed cases, such as functions, "
-           "classes, and interfaces";
-
-    const auto& import_ir = context_.import_irs().Get(import_ref.ir_id);
-    auto import_inst = import_ir->insts().Get(import_ref.inst_id);
-
-    switch (import_inst.As<SemIR::Builtin>().builtin_kind) {
+  auto BuildBuiltinValueRepr(SemIR::TypeId type_id,
+                             SemIR::Builtin builtin) const -> SemIR::ValueRepr {
+    switch (builtin.builtin_kind) {
       case SemIR::BuiltinKind::TypeType:
       case SemIR::BuiltinKind::Error:
       case SemIR::BuiltinKind::Invalid:
@@ -743,6 +735,16 @@ class TypeCompleter {
     llvm_unreachable("All builtin kinds were handled above");
   }
 
+  auto BuildImportRefUsedValueRepr(SemIR::TypeId type_id,
+                                   SemIR::ImportRefUsed import_ref) const
+      -> SemIR::ValueRepr {
+    const auto& import_ir = context_.import_irs().Get(import_ref.ir_id);
+    auto import_inst = import_ir->insts().Get(import_ref.inst_id);
+    CARBON_CHECK(import_inst.kind() != SemIR::InstKind::ImportRefUsed)
+        << "If ImportRefUsed can point at another, this would be recursive.";
+    return BuildValueRepr(type_id, import_inst);
+  }
+
   auto BuildStructOrTupleValueRepr(std::size_t num_elements,
                                    SemIR::TypeId elementwise_rep,
                                    bool same_as_object_rep) const
@@ -928,7 +930,7 @@ class TypeCompleter {
         return MakeEmptyValueRepr();
 
       case SemIR::Builtin::Kind:
-        CARBON_FATAL() << "Builtins should be named as ImportRefUsed";
+        return BuildBuiltinValueRepr(type_id, inst.As<SemIR::Builtin>());
 
       case SemIR::BindSymbolicName::Kind:
       case SemIR::PointerType::Kind:

+ 3 - 0
toolchain/check/handle_let.cpp

@@ -71,6 +71,9 @@ auto HandleLetDecl(Context& context, Parse::LetDeclId parse_node) -> bool {
   // Add the name of the binding to the current scope.
   auto name_id = context.bind_names().Get(bind_name.bind_name_id).name_id;
   context.AddNameToLookup(name_id, pattern_id);
+  if (context.scope_stack().PeekNameScopeId() == SemIR::NameScopeId::Package) {
+    context.AddExport(pattern_id);
+  }
   return true;
 }
 

+ 3 - 2
toolchain/check/import.cpp

@@ -21,9 +21,10 @@ static auto GetImportName(const SemIR::File& import_sem_ir,
                           SemIR::Inst import_inst)
     -> std::pair<SemIR::NameId, SemIR::NameScopeId> {
   switch (import_inst.kind()) {
-    case SemIR::InstKind::BindName: {
+    case SemIR::InstKind::BindName:
+    case SemIR::InstKind::BindSymbolicName: {
       const auto& bind_name = import_sem_ir.bind_names().Get(
-          import_inst.As<SemIR::BindName>().bind_name_id);
+          import_inst.As<SemIR::AnyBindName>().bind_name_id);
       return {bind_name.name_id, bind_name.enclosing_scope_id};
     }
 

+ 56 - 45
toolchain/check/import_ref.cpp

@@ -55,6 +55,23 @@ class ImportRefResolver {
     return constant_id;
   }
 
+  // Wraps constant evaluation with logic to handle types.
+  auto ResolveType(SemIR::TypeId import_type_id) -> SemIR::TypeId {
+    if (!import_type_id.is_valid()) {
+      return import_type_id;
+    }
+
+    auto import_type_inst_id = import_ir_.types().GetInstId(import_type_id);
+    CARBON_CHECK(import_type_inst_id.is_valid());
+
+    if (import_type_inst_id.is_builtin()) {
+      // Builtins don't require constant resolution; we can use them directly.
+      return context_.GetBuiltinType(import_type_inst_id.builtin_kind());
+    } else {
+      return context_.GetTypeIdForTypeConstant(Resolve(import_type_inst_id));
+    }
+  }
+
  private:
   // Returns the ConstantId for an instruction, or adds it to the stack and
   // returns Invalid if the ConstantId is not ready.
@@ -104,6 +121,21 @@ class ImportRefResolver {
       case SemIR::InstKind::TupleType:
         return TryResolveTypedInst(inst.As<SemIR::TupleType>());
 
+      case SemIR::InstKind::BindName:
+      case SemIR::InstKind::BindSymbolicName:
+        // Can use TryEvalInst because the resulting constant doesn't really use
+        // `inst`.
+        return TryEvalInst(context_, inst_id, inst);
+
+      case SemIR::InstKind::ClassDecl:
+      case SemIR::InstKind::InterfaceDecl:
+        // TODO: Not implemented.
+        return SemIR::ConstantId::Error;
+
+      case SemIR::InstKind::FunctionDecl:
+        // TODO: Allowed to work for testing, but not really implemented.
+        return SemIR::ConstantId::NotConstant;
+
       default:
         context_.TODO(
             Parse::NodeId::Invalid,
@@ -219,57 +251,36 @@ class ImportRefResolver {
 auto TryResolveImportRefUnused(Context& context, SemIR::InstId inst_id)
     -> void {
   auto inst = context.insts().Get(inst_id);
-  auto unused_inst = inst.TryAs<SemIR::ImportRefUnused>();
-  if (!unused_inst) {
+  auto import_ref = inst.TryAs<SemIR::ImportRefUnused>();
+  if (!import_ref) {
     return;
   }
 
-  const SemIR::File& import_ir = *context.import_irs().Get(unused_inst->ir_id);
-  auto import_inst = import_ir.insts().Get(unused_inst->inst_id);
-
-  // TODO: Types need to be specifically addressed here to prevent crashes in
-  // constant evaluation while support is incomplete. Functions are also
-  // incomplete, but are allowed to fail differently because they aren't a type.
-  // The partial function support is useful for some namespace validation.
-  if (import_inst.Is<SemIR::ClassDecl>() ||
-      import_inst.Is<SemIR::InterfaceDecl>()) {
-    context.TODO(
-        Parse::NodeId::Invalid,
-        llvm::formatv("TryResolveImportRefUnused on {0}", import_inst.kind())
-            .str());
-    context.ReplaceInstBeforeConstantUse(
-        inst_id, {SemIR::ImportRefUsed{SemIR::TypeId::Error, unused_inst->ir_id,
-                                       unused_inst->inst_id}});
-    return;
-  }
+  const SemIR::File& import_ir = *context.import_irs().Get(import_ref->ir_id);
+  auto& import_ir_constant_values =
+      context.import_ir_constant_values()[import_ref->ir_id.index];
+  auto import_inst = import_ir.insts().Get(import_ref->inst_id);
 
-  // If the type ID isn't a normal value, forward it directly.
-  if (!import_inst.type_id().is_valid()) {
-    context.ReplaceInstBeforeConstantUse(
-        inst_id,
-        {SemIR::ImportRefUsed{import_inst.type_id(), unused_inst->ir_id,
-                              unused_inst->inst_id}});
-    return;
-  }
+  ImportRefResolver resolver(context, import_ir, import_ir_constant_values);
+  auto type_id = resolver.ResolveType(import_inst.type_id());
+  auto constant_id = resolver.Resolve(import_ref->inst_id);
 
-  auto import_type_inst_id = import_ir.types().GetInstId(import_inst.type_id());
-  CARBON_CHECK(import_type_inst_id.is_valid());
-
-  auto type_id = SemIR::TypeId::Invalid;
-  if (import_type_inst_id.is_builtin()) {
-    // Builtins don't require constant resolution; we can use them directly.
-    type_id = context.GetBuiltinType(import_type_inst_id.builtin_kind());
-  } else {
-    ImportRefResolver resolver(
-        context, import_ir,
-        context.import_ir_constant_values()[unused_inst->ir_id.index]);
-    type_id =
-        context.GetTypeIdForTypeConstant(resolver.Resolve(import_type_inst_id));
+  // TODO: Once ClassDecl/InterfaceDecl are supported (no longer return Error),
+  // remove this.
+  if (constant_id == SemIR::ConstantId::Error) {
+    type_id = SemIR::TypeId::Error;
   }
-  // TODO: Add breadcrumbs for lowering.
-  context.ReplaceInstBeforeConstantUse(
-      inst_id, {SemIR::ImportRefUsed{type_id, unused_inst->ir_id,
-                                     unused_inst->inst_id}});
+
+  // Replace the ImportRefUnused instruction with an ImportRefUsed. This doesn't
+  // use ReplaceInstBeforeConstantUse because it would trigger TryEvalInst, and
+  // we're instead doing constant evaluation here in order to minimize recursion
+  // risks.
+  context.sem_ir().insts().Set(
+      inst_id,
+      SemIR::ImportRefUsed{type_id, import_ref->ir_id, import_ref->inst_id});
+
+  // Store the constant for both the ImportRefUsed and imported instruction.
+  context.constant_values().Set(inst_id, constant_id);
 }
 
 }  // namespace Carbon::Check

+ 8 - 10
toolchain/check/testdata/class/fail_todo_import.carbon

@@ -3,8 +3,6 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDERR: b.carbon: ERROR: Semantics TODO: `TryResolveImportRefUnused on ClassDecl`.
-// CHECK:STDERR: b.carbon: ERROR: Semantics TODO: `TryResolveImportRefUnused on ClassDecl`.
 // CHECK:STDERR: b.carbon: ERROR: Semantics TODO: `TryResolveInst on ClassType`.
 // CHECK:STDERR: b.carbon: ERROR: Semantics TODO: `TryResolveInst on ClassType`.
 
@@ -114,23 +112,23 @@ var d: (ForwardDeclared,) = d_ref;
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
 // CHECK:STDOUT:   package: <namespace> = namespace {.Empty = %import_ref.1, .ForwardDeclared = %import_ref.2, .a_ref = %import_ref.3, .b_ref = %import_ref.4, .c_ref = %import_ref.5, .Run = %Run, .a = %a, .b = %b, .c = %c, .d = %d} [template]
-// CHECK:STDOUT:   %import_ref.1: <error> = import_ref ir1, inst+1, used
-// CHECK:STDOUT:   %import_ref.2: <error> = import_ref ir1, inst+4, used
+// CHECK:STDOUT:   %import_ref.1: <error> = import_ref ir1, inst+1, used [template = <error>]
+// CHECK:STDOUT:   %import_ref.2: <error> = import_ref ir1, inst+4, used [template = <error>]
 // CHECK:STDOUT:   %import_ref.3: ref <error> = import_ref ir1, inst+12, used
 // CHECK:STDOUT:   %import_ref.4: ref <error> = import_ref ir1, inst+20, used
 // CHECK:STDOUT:   %import_ref.5: ref <error> = import_ref ir1, inst+30, used
 // CHECK:STDOUT:   %Run: <function> = fn_decl @Run [template]
-// CHECK:STDOUT:   %Empty.ref: <error> = name_ref Empty, %import_ref.1
+// CHECK:STDOUT:   %Empty.ref: <error> = name_ref Empty, %import_ref.1 [template = <error>]
 // CHECK:STDOUT:   %a.var: ref <error> = var a
 // CHECK:STDOUT:   %a: ref <error> = bind_name a, %a.var
-// CHECK:STDOUT:   %ForwardDeclared.ref.loc13: <error> = name_ref ForwardDeclared, %import_ref.2
+// CHECK:STDOUT:   %ForwardDeclared.ref.loc13: <error> = name_ref ForwardDeclared, %import_ref.2 [template = <error>]
 // CHECK:STDOUT:   %b.var: ref <error> = var b
 // CHECK:STDOUT:   %b: ref <error> = bind_name b, %b.var
-// CHECK:STDOUT:   %ForwardDeclared.ref.loc14: <error> = name_ref ForwardDeclared, %import_ref.2
+// CHECK:STDOUT:   %ForwardDeclared.ref.loc14: <error> = name_ref ForwardDeclared, %import_ref.2 [template = <error>]
 // CHECK:STDOUT:   %.loc14: type = ptr_type <error> [template = <error>]
 // CHECK:STDOUT:   %c.var: ref <error> = var c
 // CHECK:STDOUT:   %c: ref <error> = bind_name c, %c.var
-// CHECK:STDOUT:   %ForwardDeclared.ref.loc18: <error> = name_ref ForwardDeclared, %import_ref.2
+// CHECK:STDOUT:   %ForwardDeclared.ref.loc18: <error> = name_ref ForwardDeclared, %import_ref.2 [template = <error>]
 // CHECK:STDOUT:   %.loc18: <error> = tuple_literal (%ForwardDeclared.ref.loc18)
 // CHECK:STDOUT:   %d.var: ref <error> = var d
 // CHECK:STDOUT:   %d: ref <error> = bind_name d, %d.var
@@ -138,12 +136,12 @@ var d: (ForwardDeclared,) = d_ref;
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @Run() {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %Empty.ref: <error> = name_ref Empty, file.%import_ref.1
+// CHECK:STDOUT:   %Empty.ref: <error> = name_ref Empty, file.%import_ref.1 [template = <error>]
 // CHECK:STDOUT:   %x.var: ref <error> = var x
 // CHECK:STDOUT:   %x: ref <error> = bind_name x, %x.var
 // CHECK:STDOUT:   %.loc7: {} = struct_literal ()
 // CHECK:STDOUT:   assign %x.var, <error>
-// CHECK:STDOUT:   %ForwardDeclared.ref: <error> = name_ref ForwardDeclared, file.%import_ref.2
+// CHECK:STDOUT:   %ForwardDeclared.ref: <error> = name_ref ForwardDeclared, file.%import_ref.2 [template = <error>]
 // CHECK:STDOUT:   %y.var: ref <error> = var y
 // CHECK:STDOUT:   %y: ref <error> = bind_name y, %y.var
 // CHECK:STDOUT:   %.loc8: {} = struct_literal ()

+ 43 - 0
toolchain/check/testdata/let/fail_generic_import.carbon

@@ -0,0 +1,43 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+// --- implicit.carbon
+
+package Implicit api;
+
+let T:! type = i32;
+
+// --- implicit.impl.carbon
+
+package Implicit impl;
+
+// TODO: Should this be valid?
+// CHECK:STDERR: implicit.impl.carbon:[[@LINE+3]]:1: ERROR: Cannot implicitly convert from `i32` to `T`.
+// CHECK:STDERR: let a: T = 0;
+// CHECK:STDERR: ^~~~~~~~~~~~~
+let a: T = 0;
+
+// CHECK:STDOUT: --- implicit.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {} [template]
+// CHECK:STDOUT:   %T: type = bind_symbolic_name T, i32 [symbolic]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- implicit.impl.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: i32 = int_literal 0 [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {.T = %import_ref} [template]
+// CHECK:STDOUT:   %import_ref: type = import_ref ir1, inst+1, used [symbolic]
+// CHECK:STDOUT:   %T.ref: type = name_ref T, %import_ref [symbolic = %import_ref]
+// CHECK:STDOUT:   %.loc8: i32 = int_literal 0 [template = constants.%.1]
+// CHECK:STDOUT:   %a: T = bind_name a, <error>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 54 - 0
toolchain/check/testdata/let/generic_import.carbon

@@ -0,0 +1,54 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+// --- implicit.carbon
+
+package Implicit api;
+
+let T:! type = i32;
+
+// --- implicit.impl.carbon
+
+package Implicit impl;
+
+var a: T*;
+var b: T = *a;
+
+// CHECK:STDOUT: --- implicit.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {} [template]
+// CHECK:STDOUT:   %T: type = bind_symbolic_name T, i32 [symbolic]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- implicit.impl.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: type = ptr_type T [symbolic]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {.T = %import_ref, .a = %a, .b = %b} [template]
+// CHECK:STDOUT:   %import_ref: type = import_ref ir1, inst+1, used [symbolic]
+// CHECK:STDOUT:   %T.ref.loc4: type = name_ref T, %import_ref [symbolic = %import_ref]
+// CHECK:STDOUT:   %.loc4: type = ptr_type T [symbolic = constants.%.1]
+// CHECK:STDOUT:   %a.var: ref T* = var a
+// CHECK:STDOUT:   %a: ref T* = bind_name a, %a.var
+// CHECK:STDOUT:   %T.ref.loc5: type = name_ref T, %import_ref [symbolic = %import_ref]
+// CHECK:STDOUT:   %b.var: ref T = var b
+// CHECK:STDOUT:   %b: ref T = bind_name b, %b.var
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @__global_init() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %a.ref: ref T* = name_ref a, file.%a
+// CHECK:STDOUT:   %.loc5_13: T* = bind_value %a.ref
+// CHECK:STDOUT:   %.loc5_12.1: ref T = deref %.loc5_13
+// CHECK:STDOUT:   %.loc5_12.2: T = bind_value %.loc5_12.1
+// CHECK:STDOUT:   assign file.%b.var, %.loc5_12.2
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 39 - 0
toolchain/check/testdata/let/import.carbon

@@ -0,0 +1,39 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+// --- implicit.carbon
+
+package Implicit api;
+
+let a:! bool = true;
+
+// --- implicit.impl.carbon
+
+package Implicit impl;
+
+let b:! bool = a;
+
+// CHECK:STDOUT: --- implicit.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: bool = bool_literal true [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {} [template]
+// CHECK:STDOUT:   %.loc4: bool = bool_literal true [template = constants.%.1]
+// CHECK:STDOUT:   %a: bool = bind_symbolic_name a, %.loc4 [symbolic]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- implicit.impl.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace {.a = %import_ref} [template]
+// CHECK:STDOUT:   %import_ref: bool = import_ref ir1, inst+1, used [symbolic]
+// CHECK:STDOUT:   %a.ref: bool = name_ref a, %import_ref [symbolic = %import_ref]
+// CHECK:STDOUT:   %b: bool = bind_symbolic_name b, %a.ref [symbolic]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 53 - 33
toolchain/sem_ir/file.cpp

@@ -261,25 +261,27 @@ static auto GetTypePrecedence(InstKind kind) -> int {
   }
 }
 
-auto File::StringifyType(TypeId type_id) const -> std::string {
-  return StringifyTypeExpr(types().GetInstId(type_id));
-}
-
-auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
+// Implements File::StringifyTypeExpr. Static to prevent accidental use of
+// member functions while traversing IRs.
+static auto StringifyTypeExprImpl(const SemIR::File& outer_sem_ir,
+                                  InstId outer_inst_id) {
   std::string str;
   llvm::raw_string_ostream out(str);
 
   struct Step {
+    // The instruction's file.
+    const File& sem_ir;
     // The instruction to print.
     InstId inst_id;
     // The index into inst_id to print. Not used by all types.
     int index = 0;
 
     auto Next() const -> Step {
-      return {.inst_id = inst_id, .index = index + 1};
+      return {.sem_ir = sem_ir, .inst_id = inst_id, .index = index + 1};
     }
   };
-  llvm::SmallVector<Step> steps = {{.inst_id = outer_inst_id}};
+  llvm::SmallVector<Step> steps = {
+      Step{.sem_ir = outer_sem_ir, .inst_id = outer_inst_id}};
 
   while (!steps.empty()) {
     auto step = steps.pop_back_val();
@@ -294,29 +296,35 @@ auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
       continue;
     }
 
-    auto inst = insts().Get(step.inst_id);
+    const auto& sem_ir = step.sem_ir;
+    // Helper for instructions with the current sem_ir.
+    auto push_inst_id = [&](InstId inst_id) {
+      steps.push_back({.sem_ir = sem_ir, .inst_id = inst_id});
+    };
+
+    auto inst = sem_ir.insts().Get(step.inst_id);
     switch (inst.kind()) {
       case ArrayType::Kind: {
         auto array = inst.As<ArrayType>();
         if (step.index == 0) {
           out << "[";
           steps.push_back(step.Next());
-          steps.push_back(
-              {.inst_id = types().GetInstId(array.element_type_id)});
+          push_inst_id(sem_ir.types().GetInstId(array.element_type_id));
         } else if (step.index == 1) {
-          out << "; " << GetArrayBoundValue(array.bound_id) << "]";
+          out << "; " << sem_ir.GetArrayBoundValue(array.bound_id) << "]";
         }
         break;
       }
       case BindSymbolicName::Kind: {
         auto name_id = inst.As<BindSymbolicName>().bind_name_id;
-        out << names().GetFormatted(bind_names().Get(name_id).name_id);
+        out << sem_ir.names().GetFormatted(
+            sem_ir.bind_names().Get(name_id).name_id);
         break;
       }
       case ClassType::Kind: {
         auto class_name_id =
-            classes().Get(inst.As<ClassType>().class_id).name_id;
-        out << names().GetFormatted(class_name_id);
+            sem_ir.classes().Get(inst.As<ClassType>().class_id).name_id;
+        out << sem_ir.names().GetFormatted(class_name_id);
         break;
       }
       case ConstType::Kind: {
@@ -325,44 +333,48 @@ auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
 
           // Add parentheses if required.
           auto inner_type_inst_id =
-              types().GetInstId(inst.As<ConstType>().inner_id);
-          if (GetTypePrecedence(insts().Get(inner_type_inst_id).kind()) <
+              sem_ir.types().GetInstId(inst.As<ConstType>().inner_id);
+          if (GetTypePrecedence(sem_ir.insts().Get(inner_type_inst_id).kind()) <
               GetTypePrecedence(inst.kind())) {
             out << "(";
             steps.push_back(step.Next());
           }
 
-          steps.push_back({.inst_id = inner_type_inst_id});
+          push_inst_id(inner_type_inst_id);
         } else if (step.index == 1) {
           out << ")";
         }
         break;
       }
-      case ImportRefUsed::Kind:
-        out << "<TODO: ImportRefUsed " << step.inst_id << ">";
+      case ImportRefUsed::Kind: {
+        auto import_ref = inst.As<ImportRefUsed>();
+        steps.push_back({.sem_ir = *sem_ir.import_irs().Get(import_ref.ir_id),
+                         .inst_id = import_ref.inst_id});
         break;
+      }
       case InterfaceType::Kind: {
-        auto interface_name_id =
-            interfaces().Get(inst.As<InterfaceType>().interface_id).name_id;
-        out << names().GetFormatted(interface_name_id);
+        auto interface_name_id = sem_ir.interfaces()
+                                     .Get(inst.As<InterfaceType>().interface_id)
+                                     .name_id;
+        out << sem_ir.names().GetFormatted(interface_name_id);
         break;
       }
       case NameRef::Kind: {
-        out << names().GetFormatted(inst.As<NameRef>().name_id);
+        out << sem_ir.names().GetFormatted(inst.As<NameRef>().name_id);
         break;
       }
       case PointerType::Kind: {
         if (step.index == 0) {
           steps.push_back(step.Next());
-          steps.push_back({.inst_id = types().GetInstId(
-                               inst.As<PointerType>().pointee_id)});
+          push_inst_id(
+              sem_ir.types().GetInstId(inst.As<PointerType>().pointee_id));
         } else if (step.index == 1) {
           out << "*";
         }
         break;
       }
       case StructType::Kind: {
-        auto refs = inst_blocks().Get(inst.As<StructType>().fields_id);
+        auto refs = sem_ir.inst_blocks().Get(inst.As<StructType>().fields_id);
         if (refs.empty()) {
           out << "{}";
           break;
@@ -376,17 +388,17 @@ auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
         }
 
         steps.push_back(step.Next());
-        steps.push_back({.inst_id = refs[step.index]});
+        push_inst_id(refs[step.index]);
         break;
       }
       case StructTypeField::Kind: {
         auto field = inst.As<StructTypeField>();
-        out << "." << names().GetFormatted(field.name_id) << ": ";
-        steps.push_back({.inst_id = types().GetInstId(field.field_type_id)});
+        out << "." << sem_ir.names().GetFormatted(field.name_id) << ": ";
+        push_inst_id(sem_ir.types().GetInstId(field.field_type_id));
         break;
       }
       case TupleType::Kind: {
-        auto refs = type_blocks().Get(inst.As<TupleType>().elements_id);
+        auto refs = sem_ir.type_blocks().Get(inst.As<TupleType>().elements_id);
         if (refs.empty()) {
           out << "()";
           break;
@@ -404,15 +416,15 @@ auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
           break;
         }
         steps.push_back(step.Next());
-        steps.push_back({.inst_id = types().GetInstId(refs[step.index])});
+        push_inst_id(sem_ir.types().GetInstId(refs[step.index]));
         break;
       }
       case UnboundElementType::Kind: {
         if (step.index == 0) {
           out << "<unbound element of class ";
           steps.push_back(step.Next());
-          steps.push_back({.inst_id = types().GetInstId(
-                               inst.As<UnboundElementType>().class_type_id)});
+          push_inst_id(sem_ir.types().GetInstId(
+              inst.As<UnboundElementType>().class_type_id));
         } else {
           out << ">";
         }
@@ -480,6 +492,14 @@ auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
   return str;
 }
 
+auto File::StringifyType(TypeId type_id) const -> std::string {
+  return StringifyTypeExprImpl(*this, types().GetInstId(type_id));
+}
+
+auto File::StringifyTypeExpr(InstId outer_inst_id) const -> std::string {
+  return StringifyTypeExprImpl(*this, outer_inst_id);
+}
+
 auto GetExprCategory(const File& file, InstId inst_id) -> ExprCategory {
   const File* ir = &file;