Import Tint changes from Dawn

Changes:
  - c970e806dbe9fe921cad7716199e1e6fa2b9be6e [ir] Emit `var` and `let` into the IR by dan sinclair <dsinclair@chromium.org>
  - 642a4f1d8c94e684c21a7fc47b75cfeee8ff656d [ir] Make dump output more consistent. by dan sinclair <dsinclair@chromium.org>
  - f26b1269bdc7a450c02aa7d198028d2501fe3678 [ir] Move `ir::Bitcast` to inherit `ir::Call`. by dan sinclair <dsinclair@chromium.org>
  - 4a2e0ad36b0ed0738a1c32fd1ddf5268231e749b [ir] Make `ir::Discard` a child of `ir::Call`. by dan sinclair <dsinclair@chromium.org>
  - bc6720b9f6554c4ee4800adeb818cc007729103c tint/type: Remove Source from Struct & StructMember by Ben Clayton <bclayton@google.com>
  - bc9e422728eb3c8d4d5b27d8394262066ba40cd4 tint: Use type::Struct where possible by Ben Clayton <bclayton@google.com>
  - 576ba1c4939288fdc1ae723f173d155a63c7c8e7 tint: Add StructMember attributes to sem. by Ben Clayton <bclayton@google.com>
  - 333cea405c5e41a7ee2cca852eee914fd724113a tint/resolver: Clean up attribute resolving by Ben Clayton <bclayton@google.com>
  - fe8a76cbbc447db0ebbe7da4818accca70900b13 [ir] Use the const eval results for expressions. by dan sinclair <dsinclair@chromium.org>
  - f00679fd72baa267cb780e2065a988b2115890bc [ir] Make ir::Instruction a ir::Value. by dan sinclair <dsinclair@chromium.org>
  - 5e344a338fcfe0737ad3933953a6dbc7164cd15c [ir] Split AST and SEM sources out of core ir. by dan sinclair <dsinclair@chromium.org>
  - 9d3af6521bf1be1bda98477d7db5c124883e1dab tint/ir: Add GN option for building the IR by James Price <jrprice@google.com>
  - 47dd30117d7d1bc425a189409f0d85ab804a36a2 tint/resolver: Resolve builtin structs by Ben Clayton <bclayton@google.com>
  - d3b09b90e31e468746d5ec643a6a6c30e4554123 tint/resolver: Add builtin_structs.h / .cc by Ben Clayton <bclayton@google.com>
  - 72d1ea4ac2b5640d9490cca6243be48da171a2bf tint/resolver: Remove duplicate nullptr check by James Price <jrprice@google.com>
  - 135ab2b39f178a2ecefd2911e82fc79eb385a43b [ir] Rename instr. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: c970e806dbe9fe921cad7716199e1e6fa2b9be6e
Change-Id: Ifa429a18a4655d1fe7ddae825bbcb2b0bbbf04f1
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/129760
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 492e355..c6affb1 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -85,6 +85,12 @@
     defines += [ "TINT_BUILD_SYNTAX_TREE_WRITER=0" ]
   }
 
+  if (tint_build_ir) {
+    defines += [ "TINT_BUILD_IR=1" ]
+  } else {
+    defines += [ "TINT_BUILD_IR=0" ]
+  }
+
   include_dirs = [
     "${tint_root_dir}/",
     "${tint_root_dir}/include/",
@@ -281,6 +287,8 @@
     "clone_context.cc",
     "program.cc",
     "program_builder.cc",
+    "resolver/builtin_structs.cc",
+    "resolver/builtin_structs.h",
     "resolver/const_eval.cc",
     "resolver/const_eval.h",
     "resolver/ctor_conv_intrinsic.cc",
@@ -739,6 +747,7 @@
     "builtin/extension.h",
     "builtin/function.cc",
     "builtin/function.h",
+    "builtin/interpolation.h",
     "builtin/interpolation_sampling.cc",
     "builtin/interpolation_sampling.h",
     "builtin/interpolation_type.cc",
@@ -1096,6 +1105,87 @@
   ]
 }
 
+libtint_source_set("libtint_ir_builder_src") {
+  sources = [
+    "ir/builder_impl.cc",
+    "ir/builder_impl.h",
+    "ir/converter.cc",
+    "ir/converter.h",
+  ]
+  deps = [
+    ":libtint_ast_src",
+    ":libtint_constant_src",
+    ":libtint_ir_src",
+    ":libtint_program_src",
+    ":libtint_sem_src",
+    ":libtint_type_src",
+    ":libtint_utils_src",
+  ]
+}
+
+libtint_source_set("libtint_ir_src") {
+  sources = [
+    "ir/binary.cc",
+    "ir/binary.h",
+    "ir/bitcast.cc",
+    "ir/bitcast.h",
+    "ir/block.cc",
+    "ir/block.h",
+    "ir/builder.cc",
+    "ir/builder.h",
+    "ir/builtin.cc",
+    "ir/builtin.h",
+    "ir/call.cc",
+    "ir/call.h",
+    "ir/constant.cc",
+    "ir/constant.h",
+    "ir/construct.cc",
+    "ir/construct.h",
+    "ir/convert.cc",
+    "ir/convert.h",
+    "ir/debug.cc",
+    "ir/debug.h",
+    "ir/disassembler.cc",
+    "ir/disassembler.h",
+    "ir/discard.cc",
+    "ir/discard.h",
+    "ir/flow_node.cc",
+    "ir/flow_node.h",
+    "ir/function.cc",
+    "ir/function.h",
+    "ir/if.cc",
+    "ir/if.h",
+    "ir/instruction.cc",
+    "ir/instruction.h",
+    "ir/loop.cc",
+    "ir/loop.h",
+    "ir/module.cc",
+    "ir/module.h",
+    "ir/store.cc",
+    "ir/store.h",
+    "ir/switch.cc",
+    "ir/switch.h",
+    "ir/terminator.cc",
+    "ir/terminator.h",
+    "ir/unary.cc",
+    "ir/unary.h",
+    "ir/user_call.cc",
+    "ir/user_call.h",
+    "ir/value.cc",
+    "ir/value.h",
+    "ir/var.cc",
+    "ir/var.h",
+  ]
+
+  deps = [
+    ":libtint_builtins_src",
+    ":libtint_constant_src",
+    ":libtint_symbols_src",
+    ":libtint_type_src",
+    ":libtint_utils_src",
+  ]
+}
+
 source_set("libtint") {
   public_deps = [
     ":libtint_ast_src",
@@ -1143,6 +1233,15 @@
     public_deps += [ ":libtint_syntax_tree_writer_src" ]
   }
 
+  if (tint_build_ir) {
+    assert(!build_with_chromium,
+           "tint_build_ir cannot be used when building Chromium")
+    public_deps += [
+      ":libtint_ir_builder_src",
+      ":libtint_ir_src",
+    ]
+  }
+
   configs += [ ":tint_common_config" ]
   public_configs = [ ":tint_public_config" ]
 
@@ -1414,6 +1513,7 @@
       "resolver/attribute_validation_test.cc",
       "resolver/bitcast_validation_test.cc",
       "resolver/builtin_enum_test.cc",
+      "resolver/builtin_structs_test.cc",
       "resolver/builtin_test.cc",
       "resolver/builtin_validation_test.cc",
       "resolver/builtins_validation_test.cc",
@@ -2016,6 +2116,24 @@
     ]
   }
 
+  tint_unittests_source_set("tint_unittests_ir_src") {
+    sources = [
+      "ir/binary_test.cc",
+      "ir/bitcast_test.cc",
+      "ir/builder_impl_test.cc",
+      "ir/constant_test.cc",
+      "ir/discard_test.cc",
+      "ir/store_test.cc",
+      "ir/test_helper.h",
+      "ir/unary_test.cc",
+    ]
+
+    deps = [
+      ":libtint_ir_builder_src",
+      ":libtint_ir_src",
+    ]
+  }
+
   if (build_with_chromium) {
     tint_unittests_source_set("tint_unittests_fuzzer_src") {
       sources = [ "fuzzers/random_generator_test.cc" ]
@@ -2077,6 +2195,10 @@
       deps += [ ":tint_unittests_glsl_writer_src" ]
     }
 
+    if (tint_build_ir) {
+      deps += [ ":tint_unittests_ir_src" ]
+    }
+
     if (build_with_chromium) {
       deps += [ ":tint_unittests_fuzzer_src" ]
     }
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index f715428..2e1ded8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -264,6 +264,8 @@
   reflection.h
   reader/reader.cc
   reader/reader.h
+  resolver/builtin_structs.cc
+  resolver/builtin_structs.h
   resolver/const_eval.cc
   resolver/const_eval.h
   resolver/dependency_graph.cc
@@ -716,6 +718,8 @@
     ir/construct.h
     ir/convert.cc
     ir/convert.h
+    ir/converter.cc
+    ir/converter.h
     ir/debug.cc
     ir/debug.h
     ir/disassembler.cc
@@ -734,8 +738,6 @@
     ir/loop.h
     ir/module.cc
     ir/module.h
-    ir/runtime.cc
-    ir/runtime.h
     ir/store.cc
     ir/store.h
     ir/switch.cc
@@ -748,6 +750,8 @@
     ir/user_call.h
     ir/value.cc
     ir/value.h
+    ir/var.cc
+    ir/var.h
   )
 endif()
 
@@ -911,6 +915,7 @@
     resolver/attribute_validation_test.cc
     resolver/bitcast_validation_test.cc
     resolver/builtin_enum_test.cc
+    resolver/builtin_structs_test.cc
     resolver/builtin_test.cc
     resolver/builtin_validation_test.cc
     resolver/builtins_validation_test.cc
@@ -1420,7 +1425,6 @@
       ir/builder_impl_test.cc
       ir/constant_test.cc
       ir/discard_test.cc
-      ir/runtime_test.cc
       ir/store_test.cc
       ir/test_helper.h
       ir/unary_test.cc
diff --git a/src/tint/builtin/access_bench.cc b/src/tint/builtin/access_bench.cc
index 91f808f..58a5f47 100644
--- a/src/tint/builtin/access_bench.cc
+++ b/src/tint/builtin/access_bench.cc
@@ -43,7 +43,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(AccessParser);
 
diff --git a/src/tint/builtin/address_space_bench.cc b/src/tint/builtin/address_space_bench.cc
index 18cc9d2..81b27eb 100644
--- a/src/tint/builtin/address_space_bench.cc
+++ b/src/tint/builtin/address_space_bench.cc
@@ -94,7 +94,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(AddressSpaceParser);
 
diff --git a/src/tint/builtin/attribute_bench.cc b/src/tint/builtin/attribute_bench.cc
index 5d12ab3..a25fbf1 100644
--- a/src/tint/builtin/attribute_bench.cc
+++ b/src/tint/builtin/attribute_bench.cc
@@ -143,7 +143,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(AttributeParser);
 
diff --git a/src/tint/builtin/builtin.cc b/src/tint/builtin/builtin.cc
index 085a266..8f26436 100644
--- a/src/tint/builtin/builtin.cc
+++ b/src/tint/builtin/builtin.cc
@@ -28,6 +28,84 @@
 /// @param str the string to parse
 /// @returns the parsed enum, or Builtin::kUndefined if the string could not be parsed.
 Builtin ParseBuiltin(std::string_view str) {
+    if (str == "__atomic_compare_exchange_result_i32") {
+        return Builtin::kAtomicCompareExchangeResultI32;
+    }
+    if (str == "__atomic_compare_exchange_result_u32") {
+        return Builtin::kAtomicCompareExchangeResultU32;
+    }
+    if (str == "__frexp_result_abstract") {
+        return Builtin::kFrexpResultAbstract;
+    }
+    if (str == "__frexp_result_f16") {
+        return Builtin::kFrexpResultF16;
+    }
+    if (str == "__frexp_result_f32") {
+        return Builtin::kFrexpResultF32;
+    }
+    if (str == "__frexp_result_vec2_abstract") {
+        return Builtin::kFrexpResultVec2Abstract;
+    }
+    if (str == "__frexp_result_vec2_f16") {
+        return Builtin::kFrexpResultVec2F16;
+    }
+    if (str == "__frexp_result_vec2_f32") {
+        return Builtin::kFrexpResultVec2F32;
+    }
+    if (str == "__frexp_result_vec3_abstract") {
+        return Builtin::kFrexpResultVec3Abstract;
+    }
+    if (str == "__frexp_result_vec3_f16") {
+        return Builtin::kFrexpResultVec3F16;
+    }
+    if (str == "__frexp_result_vec3_f32") {
+        return Builtin::kFrexpResultVec3F32;
+    }
+    if (str == "__frexp_result_vec4_abstract") {
+        return Builtin::kFrexpResultVec4Abstract;
+    }
+    if (str == "__frexp_result_vec4_f16") {
+        return Builtin::kFrexpResultVec4F16;
+    }
+    if (str == "__frexp_result_vec4_f32") {
+        return Builtin::kFrexpResultVec4F32;
+    }
+    if (str == "__modf_result_abstract") {
+        return Builtin::kModfResultAbstract;
+    }
+    if (str == "__modf_result_f16") {
+        return Builtin::kModfResultF16;
+    }
+    if (str == "__modf_result_f32") {
+        return Builtin::kModfResultF32;
+    }
+    if (str == "__modf_result_vec2_abstract") {
+        return Builtin::kModfResultVec2Abstract;
+    }
+    if (str == "__modf_result_vec2_f16") {
+        return Builtin::kModfResultVec2F16;
+    }
+    if (str == "__modf_result_vec2_f32") {
+        return Builtin::kModfResultVec2F32;
+    }
+    if (str == "__modf_result_vec3_abstract") {
+        return Builtin::kModfResultVec3Abstract;
+    }
+    if (str == "__modf_result_vec3_f16") {
+        return Builtin::kModfResultVec3F16;
+    }
+    if (str == "__modf_result_vec3_f32") {
+        return Builtin::kModfResultVec3F32;
+    }
+    if (str == "__modf_result_vec4_abstract") {
+        return Builtin::kModfResultVec4Abstract;
+    }
+    if (str == "__modf_result_vec4_f16") {
+        return Builtin::kModfResultVec4F16;
+    }
+    if (str == "__modf_result_vec4_f32") {
+        return Builtin::kModfResultVec4F32;
+    }
     if (str == "__packed_vec3") {
         return Builtin::kPackedVec3;
     }
@@ -245,6 +323,58 @@
     switch (value) {
         case Builtin::kUndefined:
             return out << "undefined";
+        case Builtin::kAtomicCompareExchangeResultI32:
+            return out << "__atomic_compare_exchange_result_i32";
+        case Builtin::kAtomicCompareExchangeResultU32:
+            return out << "__atomic_compare_exchange_result_u32";
+        case Builtin::kFrexpResultAbstract:
+            return out << "__frexp_result_abstract";
+        case Builtin::kFrexpResultF16:
+            return out << "__frexp_result_f16";
+        case Builtin::kFrexpResultF32:
+            return out << "__frexp_result_f32";
+        case Builtin::kFrexpResultVec2Abstract:
+            return out << "__frexp_result_vec2_abstract";
+        case Builtin::kFrexpResultVec2F16:
+            return out << "__frexp_result_vec2_f16";
+        case Builtin::kFrexpResultVec2F32:
+            return out << "__frexp_result_vec2_f32";
+        case Builtin::kFrexpResultVec3Abstract:
+            return out << "__frexp_result_vec3_abstract";
+        case Builtin::kFrexpResultVec3F16:
+            return out << "__frexp_result_vec3_f16";
+        case Builtin::kFrexpResultVec3F32:
+            return out << "__frexp_result_vec3_f32";
+        case Builtin::kFrexpResultVec4Abstract:
+            return out << "__frexp_result_vec4_abstract";
+        case Builtin::kFrexpResultVec4F16:
+            return out << "__frexp_result_vec4_f16";
+        case Builtin::kFrexpResultVec4F32:
+            return out << "__frexp_result_vec4_f32";
+        case Builtin::kModfResultAbstract:
+            return out << "__modf_result_abstract";
+        case Builtin::kModfResultF16:
+            return out << "__modf_result_f16";
+        case Builtin::kModfResultF32:
+            return out << "__modf_result_f32";
+        case Builtin::kModfResultVec2Abstract:
+            return out << "__modf_result_vec2_abstract";
+        case Builtin::kModfResultVec2F16:
+            return out << "__modf_result_vec2_f16";
+        case Builtin::kModfResultVec2F32:
+            return out << "__modf_result_vec2_f32";
+        case Builtin::kModfResultVec3Abstract:
+            return out << "__modf_result_vec3_abstract";
+        case Builtin::kModfResultVec3F16:
+            return out << "__modf_result_vec3_f16";
+        case Builtin::kModfResultVec3F32:
+            return out << "__modf_result_vec3_f32";
+        case Builtin::kModfResultVec4Abstract:
+            return out << "__modf_result_vec4_abstract";
+        case Builtin::kModfResultVec4F16:
+            return out << "__modf_result_vec4_f16";
+        case Builtin::kModfResultVec4F32:
+            return out << "__modf_result_vec4_f32";
         case Builtin::kPackedVec3:
             return out << "__packed_vec3";
         case Builtin::kArray:
diff --git a/src/tint/builtin/builtin.h b/src/tint/builtin/builtin.h
index da5226d..d891294 100644
--- a/src/tint/builtin/builtin.h
+++ b/src/tint/builtin/builtin.h
@@ -30,6 +30,32 @@
 /// An enumerator of builtin builtin.
 enum class Builtin {
     kUndefined,
+    kAtomicCompareExchangeResultI32,
+    kAtomicCompareExchangeResultU32,
+    kFrexpResultAbstract,
+    kFrexpResultF16,
+    kFrexpResultF32,
+    kFrexpResultVec2Abstract,
+    kFrexpResultVec2F16,
+    kFrexpResultVec2F32,
+    kFrexpResultVec3Abstract,
+    kFrexpResultVec3F16,
+    kFrexpResultVec3F32,
+    kFrexpResultVec4Abstract,
+    kFrexpResultVec4F16,
+    kFrexpResultVec4F32,
+    kModfResultAbstract,
+    kModfResultF16,
+    kModfResultF32,
+    kModfResultVec2Abstract,
+    kModfResultVec2F16,
+    kModfResultVec2F32,
+    kModfResultVec3Abstract,
+    kModfResultVec3F16,
+    kModfResultVec3F32,
+    kModfResultVec4Abstract,
+    kModfResultVec4F16,
+    kModfResultVec4F32,
     kPackedVec3,
     kArray,
     kAtomic,
@@ -113,6 +139,32 @@
 Builtin ParseBuiltin(std::string_view str);
 
 constexpr const char* kBuiltinStrings[] = {
+    "__atomic_compare_exchange_result_i32",
+    "__atomic_compare_exchange_result_u32",
+    "__frexp_result_abstract",
+    "__frexp_result_f16",
+    "__frexp_result_f32",
+    "__frexp_result_vec2_abstract",
+    "__frexp_result_vec2_f16",
+    "__frexp_result_vec2_f32",
+    "__frexp_result_vec3_abstract",
+    "__frexp_result_vec3_f16",
+    "__frexp_result_vec3_f32",
+    "__frexp_result_vec4_abstract",
+    "__frexp_result_vec4_f16",
+    "__frexp_result_vec4_f32",
+    "__modf_result_abstract",
+    "__modf_result_f16",
+    "__modf_result_f32",
+    "__modf_result_vec2_abstract",
+    "__modf_result_vec2_f16",
+    "__modf_result_vec2_f32",
+    "__modf_result_vec3_abstract",
+    "__modf_result_vec3_f16",
+    "__modf_result_vec3_f32",
+    "__modf_result_vec4_abstract",
+    "__modf_result_vec4_f16",
+    "__modf_result_vec4_f32",
     "__packed_vec3",
     "array",
     "atomic",
diff --git a/src/tint/builtin/builtin_bench.cc b/src/tint/builtin/builtin_bench.cc
index 1f309f5..4804ff7 100644
--- a/src/tint/builtin/builtin_bench.cc
+++ b/src/tint/builtin/builtin_bench.cc
@@ -31,496 +31,678 @@
 
 void BuiltinParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "__acked_veccc",
-        "_pac3ed_v3",
-        "__packeV_vec3",
+        "__atomic_compareexchangeccresult_i32",
+        "__atoml3_compare_exchane_resulti2",
+        "__atomic_compare_Vxchange_result_i32",
+        "__atomic_compare_exchange_result_i32",
+        "__atomic_com1are_exchange_result_i32",
+        "__atomic_qqompare_exchage_resulJ_i32",
+        "__atllmic_compare_exchange_result_i377",
+        "__atomicppcompareqqexchange_reslt_uHH2",
+        "__atomi_compare_exchavge_cesult_3",
+        "__atomic_copare_eGbhange_result_u32",
+        "__atomic_compare_exchange_result_u32",
+        "__atomic_coiipare_exvhange_result_u32",
+        "__atomic_compaWWe_excha8ge_result_u32",
+        "__atomic_comparxxMexchage_result_u32",
+        "__fXexp_resgglt_bstract",
+        "V_frexp_resul_abuXrct",
+        "__frexp_result_abstra3t",
+        "__frexp_result_abstract",
+        "__frexp_resElt_abstract",
+        "__frexTT_Pesult_abstract",
+        "__frexp_resulxxddabstrct",
+        "44_frexp_result_f16",
+        "_VVfrexp_resulSS_f16",
+        "__frexp_reRult_fR6",
+        "__frexp_result_f16",
+        "__frFxp_re9ut_f16",
+        "__frep_result_f16",
+        "__frRRVH_rOOsultf16",
+        "__frepyresult_f32",
+        "_nrr77rexp_result_fGll",
+        "__4rex00_result_f32",
+        "__frexp_result_f32",
+        "__oorep_reult_f2",
+        "__fzzexp_result_3",
+        "__iir11x_respplt_f3",
+        "__frexp_resuXXt_vec2_abstract",
+        "55n99frexp_result_vec2_abstraIIt",
+        "__fHHexpSSaresrrlt_Yec2_abstract",
+        "__frexp_result_vec2_abstract",
+        "__freHp_resutve2_abstkkact",
+        "jfrexpgresult_veRR2_abstrac",
+        "__frexp_resul_vec2_absbrac",
+        "_jfrexp_result_vec2_f16",
+        "__frexp_resultvec2_f16",
+        "__freqpresultvec2_f16",
+        "__frexp_result_vec2_f16",
+        "__frexNN_result_vec_f16",
+        "__frexp_resvvlt_vc2_f1",
+        "__frexp_esult_vec2_f1QQ",
+        "__rerp_result_ffec2_f2",
+        "__frexp_result_vjc2_f32",
+        "__frewwp_reul2_vec2_NN82",
+        "__frexp_result_vec2_f32",
+        "__frexpresult_vec2_f32",
+        "__frexp_result_vec2_frr2",
+        "_Gfrexp_result_vec2_f32",
+        "__frexp_resulFF_vec3_abstract",
+        "_frexp_resultvec3_Estract",
+        "__fexp_result_vec3_abstrract",
+        "__frexp_result_vec3_abstract",
+        "frexp_result_vec3_abstract",
+        "D_rexp_resXlt_veJJ3_abstract",
+        "_frexp_resut_v8c_abstract",
+        "_frexp_rsl1k_vec3_f16",
+        "__frexp_reslt_vec3_f16",
+        "__frexJ_reult_vec3_f16",
+        "__frexp_result_vec3_f16",
+        "c_frexp_result_vec3_f16",
+        "__frexp_result_vec3Of16",
+        "___frexp_reKKultvvvec3_f1tt",
+        "8_frexp_reult_vxxc3_f32",
+        "_frexp_resul___veFqqf32",
+        "_qqfrexp_result_vec_f32",
+        "__frexp_result_vec3_f32",
+        "33_fOexp_result_ve3_6632",
+        "__oorexQQ_rttsult_ve639f32",
+        "__rexp_result_vec3_f662",
+        "__frexp_reszzlt_Oc4xabstrac66",
+        "__frexp_resyylt_vec4_abstract",
+        "__frexp_resut_vecHH_aZsracZ",
+        "__frexp_result_vec4_abstract",
+        "_WWfrex44_resulq_vec4_astract",
+        "__frexp_rsult_veOO4_abstract",
+        "__frexp_resultoovc4_abstYct",
+        "_frexp_esultvec4_f16",
+        "__Frexp_result_ec4_f16",
+        "__frewp_resut_vec4_f16",
+        "__frexp_result_vec4_f16",
+        "__frexp_reslt_veK4fG16",
+        "__fqexp_result_veKK4_f16",
+        "_F3rexp_result_vec4_f1mm",
+        "__frexp_result_ec4_f32",
+        "__frexp_result_qe4_f32",
+        "__frbbxp_result_vec4_b2",
+        "__frexp_result_vec4_f32",
+        "__frexp_reslt_iiec4_f2",
+        "__frexO_resulq_vec4_f32",
+        "__frexp_resulTT_vec4vvf32",
+        "__modf_resulFF_abstract",
+        "fm00df_rePult_abstraQt",
+        "__modf_result_abstPact",
+        "__modf_result_abstract",
+        "_modf_result_abstssac77",
+        "__modf_resulC_bbRbstract",
+        "__modf_result_abstracXX",
+        "__OOofCCresuOOt_f16",
+        "_smodf_resuutfL6",
+        "__modX_result_f16",
+        "__modf_result_f16",
+        "__modf_reult_f16",
+        "__modf_resqqO16",
+        "__modf22result_f16",
+        "__modf_X0eszzlt_fy",
+        "_VVmPf_result_f3i",
+        "__monnfCresultf32",
+        "__modf_result_f32",
+        "_HHAmodf_resqltf32",
+        "__modf_resut_f32",
+        "__modresuft_f3KK",
+        "__modlPrsultggvec2_astract",
+        "__odf_result_vec2_abstract",
+        "__mocTf_result_vNc2_abstra4t",
+        "__modf_result_vec2_abstract",
+        "__modf77result_vec2_plbtract",
+        "__mdf_resultNNvec2zabstgact",
+        "_modf_bbesult_vuuc2_abtraXXt",
+        "__modf_esult_vec2_f16",
+        "__mQdf_esuKt_vec_8816",
+        "q_m9dfresult_vec2_f16",
+        "__modf_result_vec2_f16",
+        "__11odf_result_vec2_f16",
+        "_iimodf_result_vF222f16",
+        "_77modf_result_vec2f16",
+        "__odfNNr2sult_vec2_f32",
+        "__modf_rVVsult_vec2_f32",
+        "__modf_Fesult_vewW2_f311",
+        "__modf_result_vec2_f32",
+        "__modf_rwwsult_vec_f32",
+        "__modf_result_Dec2_f32",
+        "__modf_result_ec2_f3K",
+        "__modf_resul1PP_vech_abstfact",
+        "__modf_result_vec_abstract",
+        "__YYodf_result_vec3_abstract",
+        "__modf_result_vec3_abstract",
+        "__mHHdfresult_kkec3_abstract",
+        "__modf_result_vec3rrabstract",
+        "__modf_ssesulWW_vec_abstract",
+        "__mYdf_reslt_vec3_f16",
+        "q_modLrfsult_vec3_f16",
+        "uu_vvo22f_rfsult_ec3_f16",
+        "__modf_result_vec3_f16",
+        "__mdf_reslt_vec3_f16",
+        "__modfYYresult_ve3f16",
+        "__modfEr77sult_vec3_yY16",
+        "__odf_desuMot_vec3_f32",
+        "__mMMf_result_vec3_f32",
+        "__modf_result_vec3_f355",
+        "__modf_result_vec3_f32",
+        "__modf_rest_vec3Nf32",
+        "_m33df_result_Oec3_f32",
+        "__modf_re3ult_vec3_f32",
+        "__momf_esult_Iec4_abstract",
+        "__modf_resultrvec4_absnnracK",
+        "__modf_eslt_ve4_absXXact",
+        "__modf_result_vec4_abstract",
+        "__modf_rsult_pLLI4_abstract",
+        "_modf_resflt_vec4_bstract",
+        "_Ymodf_resultURDec4_abtract",
+        "__hodf_result_vec4_f16",
+        "__moquu_rslt_vec4_f1II",
+        "__modf_result_vecH_f16",
+        "__modf_result_vec4_f16",
+        "__oQQf_resultvvvc4_f16",
+        "__modf_eeult66ec4_f16",
+        "_Omodf_r7sut_vec4_W16",
+        "__modf_DDes0lt_v55c4_f32",
+        "__modf_rIIsult_Hec4_f32",
+        "_modf_result_vec4_f32",
+        "__modf_result_vec4_f32",
+        "_modf_result_vrc4_f32",
+        "_lmodf_result_vec4_f32",
+        "tt_modfGeslt_vec4_fJJ2",
+        "__paked_vyc3",
+        "_packed_vec3",
+        "__pIIckedBBvec3",
         "__packed_vec3",
-        "__pa1ked_vec3",
-        "_qqJcked_vec3",
-        "__pack77d_vllc3",
-        "arqHapp",
-        "vy",
-        "Grby",
+        "__8aTTked_v33c3",
+        "dnnUUpackeSSY_vec3",
+        "xC_5ackedZvec3",
+        "kkrraq",
+        "a005iy",
+        "anIIray",
         "array",
-        "arviay",
-        "ar8WWy",
-        "Mxxra",
-        "atXggi",
-        "Xoic",
-        "ato3ic",
+        "ccrW",
+        "rKK",
+        "arr66y",
+        "aKKoPi",
+        "atxxmc",
+        "atoqic",
         "atomic",
-        "aEomic",
-        "toTTiPP",
-        "ddtoxxi",
-        "44ool",
-        "VVSSol",
-        "RoRl",
+        "rMoyyiSS",
+        "utom",
+        "oic",
+        "5oFFl",
+        "borz4l",
+        "WW",
         "bool",
-        "oFl",
-        "boo",
-        "ORVHl",
-        "y1",
-        "l77rrn6",
-        "4016",
+        "ZJJCoX",
+        "boPP",
+        "bocl",
+        "fll66",
+        "91yy",
+        "f1KK",
         "f16",
-        "5",
-        "u16",
-        "f",
-        "f3kk",
-        "fi",
-        "f3XX",
+        "x_",
+        "K",
+        "kVz",
+        "K3S",
+        "f2",
+        "fVV",
         "f32",
-        "55399II",
-        "frSSHHa",
-        "U",
-        "jV3",
-        "",
-        "GG",
+        "IAU2",
+        "j",
+        "Y4",
+        "i2",
+        "1xx",
+        "ccm",
         "i32",
-        "2",
-        "",
-        "jj",
-        "a2xrf",
-        "mat2j2",
-        "m82wNN2",
-        "mat2x2",
+        "iJJ",
+        "UfCDD",
+        "i3g",
+        "CCtx",
         "mt2x2",
-        "rrat2x2",
-        "mGt2x2",
-        "mat2x2FF",
-        "at2f",
-        "marrx2f",
+        "mat2x__",
+        "mat2x2",
+        "attxPP",
+        "mdd32x2",
+        "yyK2x2",
+        "m2uu",
+        "ma0nnx2i",
+        "KanuuCC2f",
         "mat2x2f",
-        "t2x2f",
-        "Da2xJJf",
-        "ma82",
-        "m11k2",
-        "matx2h",
-        "maJx2h",
+        "mlX2x2f",
+        "oat2pp2f",
+        "wwat22f",
+        "matguum",
+        "mt2ma2",
+        "Tat2xZRRh",
         "mat2x2h",
-        "mat2c2h",
-        "mat2x2O",
-        "KK_atvvtt2h",
-        "5txxx8",
-        "a__xqq",
-        "maqq2x",
+        "ma8T2xOh",
+        "m0at2x2h",
+        "mBBt2x2h",
+        "Matpp",
+        "Oat2x3",
+        "GGG2x3",
         "mat2x3",
-        "ma32x66",
-        "mttQQo2x3",
-        "mat66x",
-        "mtOxzz66",
-        "mat2yy3f",
-        "ZaHH3Z",
-        "mat2x3f",
-        "4WWt2q3f",
-        "mOO2x3f",
-        "oatY3f",
+        "mHHt2113",
+        "mateF63",
         "matx",
-        "ma2xFh",
-        "at2x3w",
+        "mat2ii3l",
+        "mt2x3f",
+        "IIvvt2x39",
+        "mat2x3f",
+        "mat23f",
+        "mat2h3f",
+        "mllt2xPzz",
+        "t3h",
+        "mtffxqqh",
+        "mtJJx3dd",
         "mat2x3h",
-        "fGtxKh",
-        "matqKx3h",
-        "matmmxFh",
-        "at2x4",
-        "matxq",
-        "mb2bb4",
+        "mzz2X3h",
+        "matx32",
+        "maN2yy3h",
+        "atxO",
+        "rauExP",
+        "meet22dd",
         "mat2x4",
-        "it2x4",
-        "mOO2xq",
-        "mat2Tvv4",
-        "maFF2x4f",
-        "Pa00xQf",
-        "mPt2x4f",
+        "maV92",
+        "maI2x1",
+        "mab2x4",
+        "matzf",
+        "mao2ii4f",
+        "mat45",
         "mat2x4f",
-        "ma772xss",
-        "RRCbb2x4f",
-        "mXXt2x4f",
-        "qaCC2xOOh",
-        "ma2s4L",
-        "mXt2x4h",
+        "at2xSf",
+        "mat22f",
+        "maG1C4f",
+        "maff284h",
+        "t2x4h",
+        "SSatJJx4h",
         "mat2x4h",
-        "mat24h",
-        "qa2O4",
-        "mat2x22h",
-        "mzzyt3x",
-        "atiVP2",
-        "mt3Cnn",
+        "atx9h",
+        "maJJbbTT4h",
+        "66a2xh",
+        "ma663u",
+        "Wa3x2",
+        "ma32",
         "mat3x2",
-        "AtqqHH2",
-        "at3x2",
-        "mafKK",
-        "ltgg2f",
-        "mat3xf",
-        "NTTtcx4f",
+        "ma3x2",
+        "rat3x2",
+        "m2t3xB",
+        "matxBBf",
+        "maRR3xf",
+        "maVV0Lf",
         "mat3x2f",
-        "ma7ppl2f",
-        "mNNt3xg",
-        "uub3XX2f",
-        "matx2h",
-        "Qt882h",
-        "mt9q2h",
+        "a3OOK2f",
+        "magw3xf",
+        "hht3L2f",
+        "aKii3xh",
+        "ma3x2h",
+        "UUa3882",
         "mat3x2h",
-        "m11t3x2h",
-        "22at3iih",
-        "at3x277",
-        "NNa323",
-        "VVat3x3",
-        "ma11F3w3",
+        "rrvvt3x2h",
+        "m3xwmm",
+        "j443x2h",
+        "matXx3",
+        "m8t3x3",
+        "mat3vEE",
         "mat3x3",
-        "matww3",
-        "mat3D3",
-        "maKx3",
-        "mat31PPhf",
-        "mat33f",
-        "mYYt3x3f",
-        "mat3x3f",
-        "mttHH3kk",
-        "mat3rr3f",
-        "WWas3x3f",
-        "Yt3x3h",
-        "mt3qfh",
-        "mav223xuh",
-        "mat3x3h",
-        "t3x3h",
-        "YYat3h",
-        "may3x3EYY",
-        "Moatd4",
-        "mt3xMM",
-        "m55t3x4",
-        "mat3x4",
-        "maN4",
-        "ma33x4",
+        "mzzi3x",
+        "maGGQ3JJ3",
+        "mat3ss3",
+        "matKxPf",
+        "mat3ttf",
         "mt3x3",
-        "mm66Issf",
-        "mat3x1f",
-        "Xt3x4",
+        "mat3x3f",
+        "mMMt3x3f",
+        "maJ03x3f",
+        "V8x3",
+        "maKggx3hh",
+        "maf3x3h",
+        "matQ7x3h",
+        "mat3x3h",
+        "mat3YYh",
+        "mak3x3",
+        "man3x2",
+        "mFFx4",
+        "GGatPPuUU",
+        "mEEFa4",
+        "mat3x4",
+        "mBet3dd4",
+        "55atExcc",
+        "txKK",
+        "mat3x4R",
+        "maDx49",
+        "mt3x4f",
         "mat3x4f",
-        "LatIx4f",
-        "at3ff",
-        "mYtURD4",
-        "mah3x4h",
-        "uuIqt3x",
-        "maH3x4h",
+        "aaat3I",
+        "m77t3x4f",
+        "matIx4f",
+        "md3x4h",
+        "mat34h",
+        "mtt4h",
         "mat3x4h",
-        "at3QQvv",
-        "at66eh",
-        "ma7O4h",
-        "m0t55DD2",
-        "IIaH4x2",
-        "mat4x",
+        "ma3XX3x4h",
+        "Eat34h",
+        "maXX3x4",
+        "mxBt4x2",
+        "Wt4x",
+        "mat66x2",
         "mat4x2",
-        "mt4r2",
-        "mat4xl",
-        "mGttx2",
-        "mat4y2",
-        "mt4x2f",
-        "IIaBB4x2f",
+        "atTv0",
+        "kt",
+        "mpt4x",
+        "at112f",
+        "EaJ4yBBf",
+        "mqIm4x2f",
         "mat4x2f",
-        "TTat4x833",
-        "ddUUnntYYx2f",
-        "m5CCxxdZ",
-        "matkkq2h",
-        "005itpxh",
-        "maIInnx2h",
+        "ma4xFf",
+        "Yt4x2f",
+        "mHHtDh2f",
+        "Ht22h",
+        "matx2",
+        "matx2h",
         "mat4x2h",
-        "Ka4Wcc",
-        "m42KK",
-        "mat66x2h",
-        "mKKtPx",
-        "maxx43",
-        "matqx3",
+        "matx2h",
+        "matddx2h",
+        "Oat4x2h",
+        "bbtB3",
+        "m00tx3",
+        "hat4x3",
         "mat4x3",
-        "rMtyyxSS",
-        "uat4",
-        "tx3",
-        "ma5F4x3f",
-        "rra444z3f",
-        "matWW",
+        "matgYx",
+        "Oat4x3",
+        "mhx3",
+        "fpaEEx3f",
+        "mavx3f",
+        "mzztx3f",
         "mat4x3f",
-        "CatZJXx3f",
-        "maPPx3f",
-        "mat4c3f",
-        "matPPll6h",
-        "mat993yy",
-        "mat4JKKh",
+        "ma4x3f",
+        "OOaJxii",
+        "mft4G3f",
+        "mat4x322T",
+        "datlx3h",
+        "bat4x3h",
         "mat4x3h",
-        "mat4_h",
-        "ayK3h",
-        "mzt4V3k",
-        "qaSKx4",
-        "mat44",
-        "ma4xVV",
+        "BBatx3h",
+        "PPIXt4S3h",
+        "mjjt4x3h",
+        "macc4_4",
+        "SS6zz4xx",
+        "mtx",
         "mat4x4",
-        "mAAt4xI",
-        "jb44",
-        "t4YYx",
-        "mao4x4",
-        "mtx114f",
-        "mtmxccf",
+        "mxxtvN",
+        "AA00t44",
+        "tyexy",
+        "mabWWx0f",
+        "ttatMMxmf",
+        "madf",
         "mat4x4f",
-        "aJJ4x4f",
-        "fCCDD4x4U",
-        "mgt4x4f",
-        "CCx4h",
-        "mat4x66",
-        "maN4M4h",
+        "mat_4f",
+        "Vat4EE4f",
+        "mat44f",
+        "mRIxah",
+        "ma4mmh",
+        "at4x4p",
         "mat4x4h",
-        "mattth",
-        "maKWxh",
-        "mateezx4h",
-        "",
-        "w9",
-        "4tnn",
+        "mat4xh",
+        "aaxh",
+        "mad4x4h",
+        "pCPtd",
+        "p",
+        "5tr",
         "ptr",
-        "tll",
-        "4to",
-        "wEgg",
-        "gamler",
-        "spleS",
-        "aampl",
+        "ff99j",
+        "YYvXR",
+        "r",
+        "XX8m5le",
+        "mpler",
+        "sccmlppr",
         "sampler",
-        "TamplZRRr",
-        "sa8TplOr",
-        "m0ampler",
-        "sampler_Bmomparison",
-        "Mamper_ppomarison",
-        "samper_compOOrison",
+        "sampver",
+        "EESSmplr",
+        "smplr",
+        "samplecomp_risa",
+        "sampler_co_prwwson",
+        "samplerdd99omparison",
         "sampler_comparison",
-        "sampler_compGrGGon",
-        "sHHm11ler_comparison",
-        "sa6ler_FFeemparison",
-        "texure_1",
-        "tKiilure_1d",
-        "exture_1d",
+        "ampler_o99paPPison",
+        "saplerKKcomparison",
+        "saMpler_oomDDarison",
+        "teiie_1B",
+        "txureq1d",
+        "txt00rLL_d",
         "texture_1d",
-        "99etvIIre_1d",
-        "texture_d",
-        "texture_hd",
-        "llxzzure_PPd",
-        "exue2d",
-        "tffqqtre_2d",
+        "tnxture_16vv",
+        "trrxtur_nd",
+        "xxture_eed",
+        "CCNOxture_2d",
+        "txture_2d",
+        "tex4uae_2d",
         "texture_2d",
-        "texturJdd_d",
-        "trXXtu_2zz",
-        "textu2e2d",
-        "tNyyture_2d_array",
-        "txture_2d_rOOa",
-        "textureErduaZPay",
+        "extuNNe_2NN",
+        "texture2d",
+        "tuxtre2d",
+        "teYYtuAe_2d_arESy",
+        "texture_2d_0rray",
+        "texture_2d_aarray",
         "texture_2d_array",
-        "exl22re_2dd_areeay",
-        "mextureVV_ar9ay",
-        "teIItu1_2d_array",
-        "tebture_3d",
-        "ie7ure3d",
-        "teotiire_3d",
+        "texturmm_2d_arra",
+        "texture_2d_aray",
+        "teEuUUe_2darray",
+        "tKKture_Dd",
+        "text__r0_3d",
+        "tAtuel3p",
         "texture_3d",
-        "extre_35",
-        "textre_iS",
-        "t22xtur_3",
-        "teC711recuGe",
-        "texture8cffbe",
-        "textue_cue",
+        "textue_3d",
+        "texturBB_3d",
+        "nnbb99re_3d",
+        "AAEExture_cub",
+        "t66Ttu5e_cube",
+        "textuHe_cube",
         "texture_cube",
-        "texture_SSJJbe",
-        "textrecu9e",
-        "TTeJJbture_cube",
-        "t66ture_cube_aray",
-        "textur66_cubu_arra",
-        "textureWubeyarray",
+        "textrexxHcub",
+        "tzx0uryy_cnbe",
+        "texture_cue",
+        "texurH_kube_array",
+        "exture_cube_array",
+        "ooexrrre_cbe_array",
         "texture_cube_array",
-        "texture_cube_ara",
-        "texture_ube_array",
-        "rexture_cube_array",
-        "tex2ure_depth_2B",
-        "texture_dpBBh_2d",
-        "texture_dpth_RRd",
+        "textre_cubJJarray",
+        "tCCxtu0e_cube_arry",
+        "texturAAcxbe99aFray",
+        "textcre_depth_2d",
+        "texture_Septh_2d",
+        "textureodpthBB2d",
         "texture_depth_2d",
-        "tLL0Vure_deth_2d",
-        "tetKKredOOpth_2d",
-        "textgwre_dpth_2d",
-        "textue_depthLh2d_arpay",
-        "texture_depEh2diiKrray",
-        "texture_dept_2d_array",
+        "texture_dept_2d",
+        "textummedepth_2d",
+        "toxture_ggeQQtPP2d",
+        "tetur_dptB_2d_rray",
+        "texNure_deKKh2d_arrlly",
+        "teture_dpth_2d_arrray",
         "texture_depth_2d_array",
-        "t88xtuUUe_deph_2d_rray",
-        "texrruvve_depth_2d_array",
-        "texture_depmm_2d_wray",
-        "tjture_d44pth_cube",
-        "texture_depth_cXbe",
-        "t8xture_depth_cube",
+        "texture_epth_ppd_array",
+        "teyturPP_depth_2d_array",
+        "texture_ZZptcc_2d_arry",
+        "txtue_depth_cube",
+        "texture00depth_cube",
+        "texPPuBB_deJth_cusse",
         "texture_depth_cube",
-        "textur_devvth_cubEE",
-        "tzxturi99epth_cube",
-        "teQQtuJJGe_nepth_cuAe",
-        "texture_depth_cusse_array",
-        "texture_Pepth_cKbe_array",
-        "texture_dppp_cube_attray",
+        "texJJre_dffpwwh_fube",
+        "textIre_depXXh_cub",
+        "textur_ph_cue",
+        "textue_depth_cube_array",
+        "tKKxtue_depth_cube_array",
+        "teture_d44ptm_cube_adray",
         "texture_depth_cube_array",
-        "exture_deth_cube_array",
-        "texture_depth_MMube_array",
-        "tJJxture_depth_cube_a0ray",
-        "textu8_dpth_mulisampled_2V",
-        "texture_dhhpth_mKltisggmpled_2d",
-        "texture_depth_multisampledf2d",
+        "pexture_deoth_cube_array",
+        "thhHxtureNdepth_cubejarray",
+        "texwwuUUe_depthEc33be_array",
+        "texture_dept_multiuuampled_2",
+        "ddextKre_depth_ultisampcerr_2d",
+        "textuPPe_depr_multttsample2_2d",
         "texture_depth_multisampled_2d",
-        "te77ture_depth_multisamQled_2d",
-        "teture_depthYYmultisampled_2d",
-        "texture_deptk_multiampled_Sd",
-        "txturn_ext2rnal",
-        "txture_FFternal",
-        "texUPPIre_GGxuernal",
+        "1exture_depth_wwsltisampled_2d",
+        "textuce_depth_mnnltisamp11ed_2d",
+        "texture_depth_multisapled_2d",
+        "texture_externl",
+        "teSS66ue_exaaeInal",
+        "textuEEe_extenal",
         "texture_external",
-        "taxtuvEE_externl",
-        "textureexBddernDDl",
-        "tEEMtur_e55tccrnal",
-        "texturemuKKtisample_d",
-        "texture_multisRmpled_2d",
-        "texturemulDisampl9d_2d",
+        "ccexture_exVerIRl",
+        "te9tue_extrnal",
+        "taaxture_exterha",
+        "texture_multisamLLeS_2d",
+        "tefurmm_mutisampled_2d",
+        "texture_mul4isampld_qV",
         "texture_multisampled_2d",
-        "teture_multisampled_2d",
-        "textuIa_multisampld_2d",
-        "texture_multisamp77ed_2d",
-        "texIure_storage_1d",
-        "texture_storagedd",
-        "texture_storae_1d",
+        "texture_multisa_pled_d",
+        "texure_multisampledQd",
+        "texRRuremultisampledEd2d",
+        "textur_st9rage_1d",
+        "tCCx0ure_strag_1",
+        "textuezstorae_1d",
         "texture_storage_1d",
-        "texture_strate_d",
-        "texture33stoXXcge_1d",
-        "texturestorage_1E",
-        "textuXXestorage_2d",
-        "texture_stoBaxxe_2d",
-        "texte_storWge_2G",
+        "texccure_storage_1d",
+        "textureOQQ_orge_1d",
+        "teturettstrage_1d",
+        "textCCrepzzstEr33ge_2d",
+        "textudde_storaghh_2d",
+        "_etur77_66torage_2d",
         "texture_storage_2d",
-        "texture_s66orage_2d",
-        "textvTr_so0age_2d",
-        "textureorgek2d",
-        "textppre_stoae_2d_array",
-        "textre_stora11e_d_array",
-        "textureystorBEgeJ2d_array",
+        "texture_storaPe_2d",
+        "twxture_storage_2d",
+        "textur_straguu_2",
+        "textureXXstorage_6d_array",
+        "extuRRestorage_2d_aray",
+        "txtre_storage_2dVVarr1",
         "texture_storage_2d_array",
-        "texture_mtorage_2dxIqrray",
-        "teture_storageF2d_array",
-        "textur_Ytorage_2d_array",
-        "heDture_sHHorage_3d",
-        "texturstorage23H",
-        "teture_strage_3d",
+        "GGexture_storHHge_2d_array",
+        "MFFxt7re_storage_2d_array",
+        "texture_storage_d_array",
+        "3xTugge_stoage_3d",
+        "texturP_QtKKrag1__d",
+        "textre_storageE3d",
         "texture_storage_3d",
-        "texture_storage_d",
-        "texturestorage_3d",
-        "ddexture_storage_3d",
-        "uPO",
-        "ba",
-        "u02",
+        "tMture_storage_d",
+        "t77xture_sGGorSSe_3d",
+        "txtttre_storage_3FF",
+        "uZZss2",
+        "u2",
+        "u3l",
         "u32",
-        "h32",
-        "gYY",
-        "O32",
-        "eh",
-        "ppfe2",
-        "vev",
+        "u3h",
+        "uTT",
+        "ww2",
+        "vKvjj",
+        "vYY",
+        "EcI2",
         "vec2",
-        "vzz2",
-        "vc2",
-        "OOii",
-        "vGc2f",
-        "22ecTTf",
-        "dlc2f",
+        "vecQQ",
+        "Pc",
+        "veffH",
+        "vec2n",
+        "g6F2f",
+        "vssh8f",
         "vec2f",
-        "vecbf",
-        "ec2BB",
-        "IIScXPP",
-        "jjec2h",
-        "cc_c2h",
-        "zz6xx2h",
+        "veFllf",
+        "00e2j",
+        "gec2f",
+        "vece",
+        "ffc2h",
+        "ve",
         "vec2h",
-        "c2",
-        "4xx2N",
-        "p0AAeh",
-        "vey2",
-        "vbWW0i",
-        "meMMtti",
+        "ve2h",
+        "vqc2h",
+        "AAe",
+        "ec2i",
+        "vec2j",
+        "ZZec2i",
         "vec2i",
-        "di",
-        "vvc_",
-        "VEEc2i",
-        "vec24",
-        "VVeX2u",
-        "veVou",
+        "PPecII2",
+        "ZZec2i",
+        "vnnc2i",
+        "HekkZ222",
+        "ec2",
+        "RcNQQ",
         "vec2u",
-        "ve2u",
-        "ecKKt",
-        "eG",
-        "ea3",
-        "OOc",
-        "G",
+        "eDu",
+        "s3c2cu",
+        "vRR2u",
+        "vlJJ3",
+        "MM",
+        "vT63",
         "vec3",
-        "ve53",
-        "9fjec3",
-        "vvXcRY",
-        "ccf",
-        "v8XX5",
-        "ec3",
+        "QQec3",
+        "vuA",
+        "e3",
+        "yeq3",
+        "vec3xx",
+        "crr",
         "vec3f",
-        "ppc3cc",
-        "vecvf",
-        "eEE3SS",
-        "vec",
-        "eh",
-        "ec3ww",
+        "v99cf",
+        "vecf",
+        "ecHl",
+        "e_h",
+        "uec3",
+        "vc3h",
         "vec3h",
-        "vecd99h",
-        "ve99P",
-        "KKec3",
-        "ooMcDD",
-        "vei",
-        "vqi",
+        "EEtmec3h",
+        "vec",
+        "ec3rr",
+        "xc3i",
+        "vezz",
+        "vec3e",
         "vec3i",
-        "veL30",
-        "vncvv66",
-        "vrrn3",
-        "vxxce",
-        "NCCOc3u",
-        "vc3u",
+        "uc3Zp",
+        "00uc7TT",
+        "vvJJ",
+        "vecQu",
+        "ve3R",
+        "e",
         "vec3u",
-        "aec4u",
-        "NNc3NN",
-        "ve3u",
-        "vc",
-        "vAYS4",
-        "vec0",
+        "veprPP",
+        "xxeDD88u",
+        "lldmYYqqu",
+        "FFec4",
+        "rGecNN",
+        "Mecl",
         "vec4",
-        "vecaa",
-        "mmcq",
-        "vc4",
-        "vE4U",
-        "veKD4",
-        "v0t4__",
+        "c",
+        "qxl4",
+        "ve4",
+        "ae44f",
+        "vec4WW",
+        "vecjj",
         "vec4f",
-        "cpA",
-        "ec4f",
-        "vBBc4f",
-        "vbnn99",
-        "EEcAAh",
-        "v5c66h",
+        "vjjc4f",
+        "vj1f",
+        "vc4f",
+        "vec499",
+        "vyVV4h",
+        "ec4xZ",
         "vec4h",
-        "vHc4h",
-        "vecxh",
-        "vzyn40",
-        "ve4i",
-        "kH4i",
-        "veci",
+        "v33vvh",
+        "vecs9",
+        "veF4",
+        "uec4i",
+        "eIKK",
+        "ve4J",
         "vec4i",
-        "oo4rr",
-        "JJc4",
-        "vcCC0",
-        "xAA99F",
-        "veccu",
-        "vec4S",
+        "vSSCCXXi",
+        "JecWW6ZZ",
+        "ecd5",
+        "vBBcBU",
+        "JJ0c411",
+        "vecttu",
         "vec4u",
-        "vocBB",
-        "ec4u",
-        "veemm",
+        "vttc",
+        "veL4u",
+        "v1c4u",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
@@ -528,7 +710,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(BuiltinParser);
 
diff --git a/src/tint/builtin/builtin_test.cc b/src/tint/builtin/builtin_test.cc
index 9ff8d99..ab8c4c6 100644
--- a/src/tint/builtin/builtin_test.cc
+++ b/src/tint/builtin/builtin_test.cc
@@ -43,6 +43,32 @@
 }
 
 static constexpr Case kValidCases[] = {
+    {"__atomic_compare_exchange_result_i32", Builtin::kAtomicCompareExchangeResultI32},
+    {"__atomic_compare_exchange_result_u32", Builtin::kAtomicCompareExchangeResultU32},
+    {"__frexp_result_abstract", Builtin::kFrexpResultAbstract},
+    {"__frexp_result_f16", Builtin::kFrexpResultF16},
+    {"__frexp_result_f32", Builtin::kFrexpResultF32},
+    {"__frexp_result_vec2_abstract", Builtin::kFrexpResultVec2Abstract},
+    {"__frexp_result_vec2_f16", Builtin::kFrexpResultVec2F16},
+    {"__frexp_result_vec2_f32", Builtin::kFrexpResultVec2F32},
+    {"__frexp_result_vec3_abstract", Builtin::kFrexpResultVec3Abstract},
+    {"__frexp_result_vec3_f16", Builtin::kFrexpResultVec3F16},
+    {"__frexp_result_vec3_f32", Builtin::kFrexpResultVec3F32},
+    {"__frexp_result_vec4_abstract", Builtin::kFrexpResultVec4Abstract},
+    {"__frexp_result_vec4_f16", Builtin::kFrexpResultVec4F16},
+    {"__frexp_result_vec4_f32", Builtin::kFrexpResultVec4F32},
+    {"__modf_result_abstract", Builtin::kModfResultAbstract},
+    {"__modf_result_f16", Builtin::kModfResultF16},
+    {"__modf_result_f32", Builtin::kModfResultF32},
+    {"__modf_result_vec2_abstract", Builtin::kModfResultVec2Abstract},
+    {"__modf_result_vec2_f16", Builtin::kModfResultVec2F16},
+    {"__modf_result_vec2_f32", Builtin::kModfResultVec2F32},
+    {"__modf_result_vec3_abstract", Builtin::kModfResultVec3Abstract},
+    {"__modf_result_vec3_f16", Builtin::kModfResultVec3F16},
+    {"__modf_result_vec3_f32", Builtin::kModfResultVec3F32},
+    {"__modf_result_vec4_abstract", Builtin::kModfResultVec4Abstract},
+    {"__modf_result_vec4_f16", Builtin::kModfResultVec4F16},
+    {"__modf_result_vec4_f32", Builtin::kModfResultVec4F32},
     {"__packed_vec3", Builtin::kPackedVec3},
     {"array", Builtin::kArray},
     {"atomic", Builtin::kAtomic},
@@ -116,216 +142,294 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"__acked_veccc", Builtin::kUndefined},
-    {"_pac3ed_v3", Builtin::kUndefined},
-    {"__packeV_vec3", Builtin::kUndefined},
-    {"arra1", Builtin::kUndefined},
-    {"qqrJy", Builtin::kUndefined},
-    {"arrll7y", Builtin::kUndefined},
-    {"atppmHHc", Builtin::kUndefined},
-    {"cto", Builtin::kUndefined},
-    {"abGmi", Builtin::kUndefined},
-    {"bovii", Builtin::kUndefined},
-    {"boWWl", Builtin::kUndefined},
-    {"Mxxl", Builtin::kUndefined},
-    {"fgg", Builtin::kUndefined},
-    {"X", Builtin::kUndefined},
-    {"316", Builtin::kUndefined},
-    {"fE2", Builtin::kUndefined},
-    {"fPTT", Builtin::kUndefined},
-    {"dxx2", Builtin::kUndefined},
-    {"4432", Builtin::kUndefined},
-    {"iSVV2", Builtin::kUndefined},
-    {"RR2", Builtin::kUndefined},
-    {"at292", Builtin::kUndefined},
-    {"mat2x", Builtin::kUndefined},
-    {"Vat2OR2", Builtin::kUndefined},
-    {"ma2xyf", Builtin::kUndefined},
-    {"llnarr2772f", Builtin::kUndefined},
-    {"mat24200", Builtin::kUndefined},
-    {"a2ooh", Builtin::kUndefined},
-    {"zz2x2h", Builtin::kUndefined},
-    {"miitppx1", Builtin::kUndefined},
-    {"maXX2x3", Builtin::kUndefined},
-    {"55IIt2nn99", Builtin::kUndefined},
-    {"aHHrrt2xSS", Builtin::kUndefined},
-    {"makkf", Builtin::kUndefined},
-    {"jatgRx", Builtin::kUndefined},
-    {"mb2x3", Builtin::kUndefined},
-    {"mat2xjh", Builtin::kUndefined},
-    {"at2x3h", Builtin::kUndefined},
-    {"q2x3h", Builtin::kUndefined},
-    {"mNN2x4", Builtin::kUndefined},
-    {"mavv4", Builtin::kUndefined},
-    {"maQQx4", Builtin::kUndefined},
-    {"maffxr", Builtin::kUndefined},
-    {"mat2xjf", Builtin::kUndefined},
-    {"mNNw2x48", Builtin::kUndefined},
-    {"matx4h", Builtin::kUndefined},
-    {"mrrt2x4h", Builtin::kUndefined},
-    {"Gat2x4h", Builtin::kUndefined},
-    {"matFFx2", Builtin::kUndefined},
-    {"mtx", Builtin::kUndefined},
-    {"mrrt3x", Builtin::kUndefined},
+    {"__atomic_compareexchangeccresult_i32", Builtin::kUndefined},
+    {"__atoml3_compare_exchane_resulti2", Builtin::kUndefined},
+    {"__atomic_compare_Vxchange_result_i32", Builtin::kUndefined},
+    {"__atomic_com1are_exchange_result_u32", Builtin::kUndefined},
+    {"__atomic_qqompare_exchage_resulJ_u32", Builtin::kUndefined},
+    {"__atllmic_compare_exchange_result_u377", Builtin::kUndefined},
+    {"qpp_frexp_resultHHbstract", Builtin::kUndefined},
+    {"__fep_esulv_abstract", Builtin::kUndefined},
+    {"__Gbexp_resul_abstract", Builtin::kUndefined},
+    {"_vfrexp_resiilt_f16", Builtin::kUndefined},
+    {"__fr8xp_resultWWf16", Builtin::kUndefined},
+    {"__frxp_result_fMxx", Builtin::kUndefined},
+    {"gg_fXexp_reslt_f32", Builtin::kUndefined},
+    {"__frXxpresul_V32", Builtin::kUndefined},
+    {"__frexp_r3sult_f32", Builtin::kUndefined},
+    {"__frexpEresult_vec2_abstract", Builtin::kUndefined},
+    {"__frex_rPPsult_vTTc2_abstract", Builtin::kUndefined},
+    {"__frexp_resuddt_ec2_xxbstract", Builtin::kUndefined},
+    {"__frexp_result_ve442_f16", Builtin::kUndefined},
+    {"_SSfrexp_resulVV_vec2_f16", Builtin::kUndefined},
+    {"__fRxpRr22sult_vec2_f16", Builtin::kUndefined},
+    {"__frexp_res9lt_vec_fF2", Builtin::kUndefined},
+    {"__frexp_result_ve2_f32", Builtin::kUndefined},
+    {"_OOfrexp_result_VeHRRf32", Builtin::kUndefined},
+    {"__frexp_reyult_vec3_absract", Builtin::kUndefined},
+    {"__frexp_re77ulll_vecG_arrnstract", Builtin::kUndefined},
+    {"__4rexp_result_vec3_00bstract", Builtin::kUndefined},
+    {"__oorxp_result_vec316", Builtin::kUndefined},
+    {"zz_frexp_esult_ec3_f16", Builtin::kUndefined},
+    {"__iirex11_result_vp3_f16", Builtin::kUndefined},
+    {"__frXXxp_result_vec3_f32", Builtin::kUndefined},
+    {"__fnnexp99resIIlt_vec3_f355", Builtin::kUndefined},
+    {"__faSSerrp_result_vHHc3_fY2", Builtin::kUndefined},
+    {"__freHp_resutve4_abstkkact", Builtin::kUndefined},
+    {"jfrexpgresult_veRR4_abstrac", Builtin::kUndefined},
+    {"__frexp_resul_vec4_absbrac", Builtin::kUndefined},
+    {"_jfrexp_result_vec4_f16", Builtin::kUndefined},
+    {"__frexp_resultvec4_f16", Builtin::kUndefined},
+    {"__freqpresultvec4_f16", Builtin::kUndefined},
+    {"__frexNN_result_vec_f32", Builtin::kUndefined},
+    {"__frexp_resvvlt_vc4_f3", Builtin::kUndefined},
+    {"__frexp_esult_vec4_f3QQ", Builtin::kUndefined},
+    {"rmodf_reffultabstract", Builtin::kUndefined},
+    {"__jodf_result_abstract", Builtin::kUndefined},
+    {"_mNNwdf_r2sult8abstract", Builtin::kUndefined},
+    {"__mdf_result_f16", Builtin::kUndefined},
+    {"__modrr_result_f16", Builtin::kUndefined},
+    {"__mGdf_result_f16", Builtin::kUndefined},
+    {"__modf_resulFF_f32", Builtin::kUndefined},
+    {"__modf_eult_E3", Builtin::kUndefined},
+    {"__odf_resurrt_f32", Builtin::kUndefined},
+    {"__modf_reslt_vec_abstract", Builtin::kUndefined},
+    {"__modfJJresuDt_Xc2_abstract", Builtin::kUndefined},
+    {"_modf_reslt_vec28abstrct", Builtin::kUndefined},
+    {"__odf_reult_vkc211f1", Builtin::kUndefined},
+    {"__mdf_result_vec2_f16", Builtin::kUndefined},
+    {"__modf_resuJt_vec2_f6", Builtin::kUndefined},
+    {"__modf_result_vec2cf32", Builtin::kUndefined},
+    {"__modf_result_vec2_fO2", Builtin::kUndefined},
+    {"KK_movvf_result_vec2_ftt__", Builtin::kUndefined},
+    {"xx_modf_r8sult_vec3_abtr5ct", Builtin::kUndefined},
+    {"__modf_resuFt_vec3_aqt__act", Builtin::kUndefined},
+    {"__modf_result_vec3_aqqstrac", Builtin::kUndefined},
+    {"__odf_33esult_vec3_f1O6", Builtin::kUndefined},
+    {"_ttm6df_resQQlt_ooec9_f16", Builtin::kUndefined},
+    {"_modf_resu66t_vec3_f16", Builtin::kUndefined},
+    {"__mdf_resultOvxc3_f36zz", Builtin::kUndefined},
+    {"__modf_resuyyt_vec3_f32", Builtin::kUndefined},
+    {"__mod_resul_vecZHHf32", Builtin::kUndefined},
+    {"__modf_reqult_44ec4WWbstract", Builtin::kUndefined},
+    {"__mof_result_vec4_abstrOOct", Builtin::kUndefined},
+    {"__modYooresult_vh4_bstract", Builtin::kUndefined},
+    {"__modf_relt_ve4_f16", Builtin::kUndefined},
+    {"__modf_result_ve4Ff16", Builtin::kUndefined},
+    {"__modf_result_wec4_f1", Builtin::kUndefined},
+    {"__Kdff_rGsult_vec4_f2", Builtin::kUndefined},
+    {"__modf_reKKulq_vec4_f32", Builtin::kUndefined},
+    {"__modf_resummt3vec4_f3F", Builtin::kUndefined},
+    {"__packed_ec3", Builtin::kUndefined},
+    {"__packed_ecq", Builtin::kUndefined},
+    {"_backed_bbec3", Builtin::kUndefined},
+    {"iira", Builtin::kUndefined},
+    {"aqOOy", Builtin::kUndefined},
+    {"arvvTTy", Builtin::kUndefined},
+    {"atomFFc", Builtin::kUndefined},
+    {"aoQ00P", Builtin::kUndefined},
+    {"atPmic", Builtin::kUndefined},
+    {"bos77", Builtin::kUndefined},
+    {"CoRbbl", Builtin::kUndefined},
+    {"booXX", Builtin::kUndefined},
+    {"qOOO6", Builtin::kUndefined},
+    {"fs", Builtin::kUndefined},
+    {"f1X", Builtin::kUndefined},
+    {"f3", Builtin::kUndefined},
+    {"q", Builtin::kUndefined},
+    {"f322", Builtin::kUndefined},
+    {"0yz2", Builtin::kUndefined},
+    {"iVP", Builtin::kUndefined},
+    {"Cnn", Builtin::kUndefined},
+    {"AtqqHH2", Builtin::kUndefined},
+    {"at2x2", Builtin::kUndefined},
+    {"mafKK", Builtin::kUndefined},
+    {"ltgg2f", Builtin::kUndefined},
+    {"mat2xf", Builtin::kUndefined},
+    {"NTTtcx4f", Builtin::kUndefined},
+    {"ma7ppl2h", Builtin::kUndefined},
+    {"mNNt2xg", Builtin::kUndefined},
+    {"uub2XX2h", Builtin::kUndefined},
+    {"mt2x3", Builtin::kUndefined},
+    {"m88xK", Builtin::kUndefined},
+    {"maqx3", Builtin::kUndefined},
+    {"m11t2x3f", Builtin::kUndefined},
+    {"22at2iif", Builtin::kUndefined},
+    {"at2x377", Builtin::kUndefined},
+    {"m2t2xNh", Builtin::kUndefined},
+    {"mVVt2x3h", Builtin::kUndefined},
+    {"FaWW2w11h", Builtin::kUndefined},
+    {"matww4", Builtin::kUndefined},
+    {"mat2D4", Builtin::kUndefined},
+    {"maKx4", Builtin::kUndefined},
+    {"mat21PPhf", Builtin::kUndefined},
+    {"mat24f", Builtin::kUndefined},
+    {"mYYt2x4f", Builtin::kUndefined},
+    {"mttHH4kk", Builtin::kUndefined},
+    {"mat2rr4h", Builtin::kUndefined},
+    {"WWas2x4h", Builtin::kUndefined},
+    {"maYx2", Builtin::kUndefined},
+    {"mq3f2", Builtin::kUndefined},
+    {"vvafu222", Builtin::kUndefined},
     {"t3x2f", Builtin::kUndefined},
-    {"Da3xJJf", Builtin::kUndefined},
-    {"ma82", Builtin::kUndefined},
-    {"m11k2", Builtin::kUndefined},
-    {"matx2h", Builtin::kUndefined},
-    {"maJx2h", Builtin::kUndefined},
-    {"cat3x3", Builtin::kUndefined},
-    {"mat3O3", Builtin::kUndefined},
-    {"ttKavv3x__", Builtin::kUndefined},
-    {"xx83x3f", Builtin::kUndefined},
-    {"__qatF3", Builtin::kUndefined},
-    {"matqx3f", Builtin::kUndefined},
-    {"33atOx3h", Builtin::kUndefined},
-    {"mtt63x9oQQ", Builtin::kUndefined},
-    {"ma3x66h", Builtin::kUndefined},
-    {"66aOzx4", Builtin::kUndefined},
-    {"myyt3x4", Builtin::kUndefined},
-    {"HHZx4", Builtin::kUndefined},
-    {"4WWt3q4f", Builtin::kUndefined},
-    {"mOO3x4f", Builtin::kUndefined},
-    {"oatY4f", Builtin::kUndefined},
-    {"matx", Builtin::kUndefined},
-    {"ma3xFh", Builtin::kUndefined},
-    {"at3x4w", Builtin::kUndefined},
-    {"ma4Gf", Builtin::kUndefined},
-    {"qatKKx2", Builtin::kUndefined},
-    {"mmmt4x2", Builtin::kUndefined},
-    {"at4x2f", Builtin::kUndefined},
-    {"mt4x2q", Builtin::kUndefined},
-    {"mat4xbb", Builtin::kUndefined},
-    {"mi4x2h", Builtin::kUndefined},
-    {"maOO4xq", Builtin::kUndefined},
-    {"matTvvx2h", Builtin::kUndefined},
-    {"mat4FF3", Builtin::kUndefined},
-    {"mtQ00P", Builtin::kUndefined},
-    {"maP4x3", Builtin::kUndefined},
-    {"ma774xss", Builtin::kUndefined},
-    {"RRCbb4x3f", Builtin::kUndefined},
-    {"mXXt4x3f", Builtin::kUndefined},
-    {"qaCC4xOOh", Builtin::kUndefined},
-    {"ma4s3L", Builtin::kUndefined},
-    {"mXt4x3h", Builtin::kUndefined},
-    {"mat4x", Builtin::kUndefined},
-    {"qqt4", Builtin::kUndefined},
-    {"mat4x22", Builtin::kUndefined},
-    {"myzz40XX", Builtin::kUndefined},
-    {"matVViP", Builtin::kUndefined},
-    {"mannC4f", Builtin::kUndefined},
-    {"atx4AHHq", Builtin::kUndefined},
-    {"may4x4", Builtin::kUndefined},
-    {"aOOOZZh", Builtin::kUndefined},
-    {"V", Builtin::kUndefined},
-    {"ptf__", Builtin::kUndefined},
-    {"4lMT", Builtin::kUndefined},
-    {"sNNmplg", Builtin::kUndefined},
-    {"uubpXXer", Builtin::kUndefined},
-    {"samler", Builtin::kUndefined},
-    {"m88ler_cQmparisoK", Builtin::kUndefined},
-    {"qa9ler_comparison", Builtin::kUndefined},
-    {"sampler_comparis11n", Builtin::kUndefined},
-    {"teiiu22eF1d", Builtin::kUndefined},
-    {"tex77ur_1d", Builtin::kUndefined},
-    {"te2urNN_1d", Builtin::kUndefined},
-    {"texturVV_2d", Builtin::kUndefined},
-    {"WWFxtu11e_wd", Builtin::kUndefined},
-    {"txture_2ww", Builtin::kUndefined},
-    {"texture_2d_arrDy", Builtin::kUndefined},
-    {"teKtre_2d_array", Builtin::kUndefined},
-    {"texhure_2fra11raPP", Builtin::kUndefined},
-    {"texture3d", Builtin::kUndefined},
-    {"texture_3YY", Builtin::kUndefined},
-    {"HHtxtrkk_3d", Builtin::kUndefined},
-    {"texrrure_cube", Builtin::kUndefined},
-    {"tssxturWW_cue", Builtin::kUndefined},
-    {"teYure_cube", Builtin::kUndefined},
-    {"txture_Lufe_arraq", Builtin::kUndefined},
-    {"te22ture_uuubevvfray", Builtin::kUndefined},
-    {"texturecube_aray", Builtin::kUndefined},
-    {"texture_Yepth_2", Builtin::kUndefined},
-    {"teytYYEe_77epth_2d", Builtin::kUndefined},
-    {"teMture_deootd2d", Builtin::kUndefined},
-    {"texMMre_depth_2d_array", Builtin::kUndefined},
-    {"texture_depth_2d_arra55", Builtin::kUndefined},
-    {"texture_deh_2d_aNray", Builtin::kUndefined},
-    {"te3ture_dpth_cO3be", Builtin::kUndefined},
-    {"texture_depth_cub3", Builtin::kUndefined},
-    {"Iexturedepth_cume", Builtin::kUndefined},
-    {"texture_depthnncube_Krrry", Builtin::kUndefined},
-    {"texture_dth_XXube_rra", Builtin::kUndefined},
-    {"textIre_depph_ubeLLarray", Builtin::kUndefined},
-    {"txtfre_depthmultisampled_2d", Builtin::kUndefined},
-    {"texURuYe_Depthmultisampled_2d", Builtin::kUndefined},
-    {"texture_depth_multisamphed_2d", Builtin::kUndefined},
-    {"teqtureuIIextnal", Builtin::kUndefined},
-    {"texture_externaH", Builtin::kUndefined},
-    {"texre_externaQvv", Builtin::kUndefined},
-    {"textureemultismp66ed_d", Builtin::kUndefined},
-    {"tW7trO_multisampled_2d", Builtin::kUndefined},
-    {"texture_mult550ampled_2DD", Builtin::kUndefined},
-    {"teHture_storIIge_1d", Builtin::kUndefined},
-    {"textue_storage_1d", Builtin::kUndefined},
-    {"rexture_storae_1d", Builtin::kUndefined},
-    {"texture_stolage_2d", Builtin::kUndefined},
-    {"txture_JJtGrgtt_2d", Builtin::kUndefined},
-    {"yexture_storage2d", Builtin::kUndefined},
-    {"texture_storage_2d_rray", Builtin::kUndefined},
-    {"texture_IItorage_2d_BBrray", Builtin::kUndefined},
-    {"33exture_TTtorge_Kd_ar88ay", Builtin::kUndefined},
-    {"texSnnYUUure_storage_3d", Builtin::kUndefined},
-    {"textuxe_5torCCdZ_3d", Builtin::kUndefined},
-    {"tkkxture_storaqe_3d", Builtin::kUndefined},
-    {"5i00", Builtin::kUndefined},
-    {"unII2", Builtin::kUndefined},
-    {"cc", Builtin::kUndefined},
-    {"KK", Builtin::kUndefined},
-    {"66ec2", Builtin::kUndefined},
-    {"PPEK", Builtin::kUndefined},
-    {"vexxf", Builtin::kUndefined},
-    {"qec2f", Builtin::kUndefined},
-    {"veSyMMr", Builtin::kUndefined},
-    {"v2u", Builtin::kUndefined},
-    {"ec", Builtin::kUndefined},
-    {"5eFF2h", Builtin::kUndefined},
-    {"rrecz44", Builtin::kUndefined},
-    {"vWW", Builtin::kUndefined},
-    {"XJecCZZ", Builtin::kUndefined},
-    {"vePP2", Builtin::kUndefined},
-    {"vec2c", Builtin::kUndefined},
-    {"ve6ll2u", Builtin::kUndefined},
-    {"vey99", Builtin::kUndefined},
-    {"vKKc3", Builtin::kUndefined},
-    {"x_3", Builtin::kUndefined},
-    {"Ky3", Builtin::kUndefined},
-    {"zek3f", Builtin::kUndefined},
-    {"veKSf", Builtin::kUndefined},
-    {"vc3h", Builtin::kUndefined},
-    {"ec3VV", Builtin::kUndefined},
-    {"IAAc3h", Builtin::kUndefined},
-    {"jbR", Builtin::kUndefined},
-    {"veY4", Builtin::kUndefined},
-    {"ec3i", Builtin::kUndefined},
-    {"vc911", Builtin::kUndefined},
-    {"mmccu", Builtin::kUndefined},
-    {"vJJcu", Builtin::kUndefined},
-    {"lDCfcU", Builtin::kUndefined},
-    {"veg4", Builtin::kUndefined},
-    {"CC", Builtin::kUndefined},
-    {"ec4f", Builtin::kUndefined},
-    {"vIc__f", Builtin::kUndefined},
-    {"ePPtt", Builtin::kUndefined},
-    {"v3dc4h", Builtin::kUndefined},
-    {"vcyyh", Builtin::kUndefined},
-    {"u4", Builtin::kUndefined},
-    {"v03nni", Builtin::kUndefined},
-    {"Cuuecnv", Builtin::kUndefined},
-    {"vX4ll", Builtin::kUndefined},
-    {"vocppu", Builtin::kUndefined},
-    {"vwwc4", Builtin::kUndefined},
-    {"veuug", Builtin::kUndefined},
+    {"YYat3f", Builtin::kUndefined},
+    {"may3x2EYY", Builtin::kUndefined},
+    {"da3xMoh", Builtin::kUndefined},
+    {"matMMx2", Builtin::kUndefined},
+    {"mat3x55h", Builtin::kUndefined},
+    {"maN3", Builtin::kUndefined},
+    {"ma33x3", Builtin::kUndefined},
+    {"mt3x3", Builtin::kUndefined},
+    {"mm66Issf", Builtin::kUndefined},
+    {"mat3x1f", Builtin::kUndefined},
+    {"Xt3x3", Builtin::kUndefined},
+    {"LatIx3h", Builtin::kUndefined},
+    {"at3fh", Builtin::kUndefined},
+    {"mYtURD3", Builtin::kUndefined},
+    {"mah3x4", Builtin::kUndefined},
+    {"muqII4", Builtin::kUndefined},
+    {"mat3xH", Builtin::kUndefined},
+    {"at3QQvv", Builtin::kUndefined},
+    {"at66ef", Builtin::kUndefined},
+    {"ma7O4f", Builtin::kUndefined},
+    {"m55t3x0DD", Builtin::kUndefined},
+    {"maH3x4II", Builtin::kUndefined},
+    {"at3x4", Builtin::kUndefined},
+    {"ma994x2", Builtin::kUndefined},
+    {"mWWt4Gt2", Builtin::kUndefined},
+    {"ay42", Builtin::kUndefined},
+    {"mt4x2f", Builtin::kUndefined},
+    {"IIaBB4x2f", Builtin::kUndefined},
+    {"TTat4x833", Builtin::kUndefined},
+    {"ddUUnntYYx2h", Builtin::kUndefined},
+    {"m5CCxxdZ", Builtin::kUndefined},
+    {"matkkq2h", Builtin::kUndefined},
+    {"5iitp00", Builtin::kUndefined},
+    {"mnntIIx3", Builtin::kUndefined},
+    {"ccaKx", Builtin::kUndefined},
+    {"m43KK", Builtin::kUndefined},
+    {"mat66x3f", Builtin::kUndefined},
+    {"Et4PP3K", Builtin::kUndefined},
+    {"xxatx3h", Builtin::kUndefined},
+    {"qat4x3h", Builtin::kUndefined},
+    {"MMayySrxh", Builtin::kUndefined},
+    {"uat4", Builtin::kUndefined},
+    {"tx4", Builtin::kUndefined},
+    {"ma54FF4", Builtin::kUndefined},
+    {"rra444z4f", Builtin::kUndefined},
+    {"matWW", Builtin::kUndefined},
+    {"CatZJXx4f", Builtin::kUndefined},
+    {"maPPx4h", Builtin::kUndefined},
+    {"mat4c4h", Builtin::kUndefined},
+    {"matPPll6h", Builtin::kUndefined},
+    {"9tyy", Builtin::kUndefined},
+    {"ptKK", Builtin::kUndefined},
+    {"x_", Builtin::kUndefined},
+    {"ayKer", Builtin::kUndefined},
+    {"szmpVek", Builtin::kUndefined},
+    {"sampqeK", Builtin::kUndefined},
+    {"sampler_comparisn", Builtin::kUndefined},
+    {"sapler_comparisVVn", Builtin::kUndefined},
+    {"samplerIcompaAUison", Builtin::kUndefined},
+    {"jexurbRd", Builtin::kUndefined},
+    {"exure_YYd", Builtin::kUndefined},
+    {"exture_1d", Builtin::kUndefined},
+    {"texxxur_1d", Builtin::kUndefined},
+    {"tJxucce_2d", Builtin::kUndefined},
+    {"texure_JJd", Builtin::kUndefined},
+    {"lDexture_fCC_arraU", Builtin::kUndefined},
+    {"tegture_2d_array", Builtin::kUndefined},
+    {"teCCure2d_arra", Builtin::kUndefined},
+    {"textue_3d", Builtin::kUndefined},
+    {"tIx__ure_3d", Builtin::kUndefined},
+    {"texurettPP", Builtin::kUndefined},
+    {"tddx3ure_cube", Builtin::kUndefined},
+    {"teKyyur_cube", Builtin::kUndefined},
+    {"teturecub", Builtin::kUndefined},
+    {"textinne_c03e_array", Builtin::kUndefined},
+    {"nextCCruuvcubK_array", Builtin::kUndefined},
+    {"tXxturellcbe_array", Builtin::kUndefined},
+    {"tppxture_depth_2d", Builtin::kUndefined},
+    {"txture_deptww_2d", Builtin::kUndefined},
+    {"gexturedemmthuu2", Builtin::kUndefined},
+    {"texmmre_depthaa2daray", Builtin::kUndefined},
+    {"texture_RRepth_Td_ccZray", Builtin::kUndefined},
+    {"text88re_depthTOd_array", Builtin::kUndefined},
+    {"texture_depth_cm00e", Builtin::kUndefined},
+    {"texture_Bmepth_cube", Builtin::kUndefined},
+    {"Mextre_ppeph_cube", Builtin::kUndefined},
+    {"texturOO_depth_cub_array", Builtin::kUndefined},
+    {"GeGGture_depthcube_array", Builtin::kUndefined},
+    {"texture11Hdepth_cube_array", Builtin::kUndefined},
+    {"textu6e_FFepth_multiameeled_2d", Builtin::kUndefined},
+    {"texture_epth_mltisampled_2d", Builtin::kUndefined},
+    {"texture_depth_mullKsaiipled_2d", Builtin::kUndefined},
+    {"texture_extenal", Builtin::kUndefined},
+    {"IIext99reexvvernal", Builtin::kUndefined},
+    {"texture_externl", Builtin::kUndefined},
+    {"texture_mhltisampled_2d", Builtin::kUndefined},
+    {"texturemPllltisampzzed_2d", Builtin::kUndefined},
+    {"exture_mltisamed_2d", Builtin::kUndefined},
+    {"texture_qqtoragff_1", Builtin::kUndefined},
+    {"textre_JJddorage_1W", Builtin::kUndefined},
+    {"XXrxture_storae1zz", Builtin::kUndefined},
+    {"texturestorag2_2d", Builtin::kUndefined},
+    {"yyNxture_storage_2d", Builtin::kUndefined},
+    {"etue_storage_2OO", Builtin::kUndefined},
+    {"reutuPe_storZgeE2d_array", Builtin::kUndefined},
+    {"texlure_storddeee_d_22rray", Builtin::kUndefined},
+    {"texture_mtorage_2V_a9ra", Builtin::kUndefined},
+    {"teII1re_storage_3d", Builtin::kUndefined},
+    {"texture_storagb_3d", Builtin::kUndefined},
+    {"texizrestorge73d", Builtin::kUndefined},
+    {"u3oi", Builtin::kUndefined},
+    {"3", Builtin::kUndefined},
+    {"S2", Builtin::kUndefined},
+    {"e22", Builtin::kUndefined},
+    {"1eC2", Builtin::kUndefined},
+    {"vf8c2", Builtin::kUndefined},
+    {"c2f", Builtin::kUndefined},
+    {"JJecSSf", Builtin::kUndefined},
+    {"92f", Builtin::kUndefined},
+    {"vbbJJ2TT", Builtin::kUndefined},
+    {"e66h", Builtin::kUndefined},
+    {"u662h", Builtin::kUndefined},
+    {"vW2i", Builtin::kUndefined},
+    {"v2i", Builtin::kUndefined},
+    {"veci", Builtin::kUndefined},
+    {"rec2u", Builtin::kUndefined},
+    {"2ec2B", Builtin::kUndefined},
+    {"vcBBu", Builtin::kUndefined},
+    {"veRR", Builtin::kUndefined},
+    {"VLL0", Builtin::kUndefined},
+    {"KOe3", Builtin::kUndefined},
+    {"vgwcf", Builtin::kUndefined},
+    {"vLphf", Builtin::kUndefined},
+    {"eiiEf", Builtin::kUndefined},
+    {"ec3h", Builtin::kUndefined},
+    {"UU883", Builtin::kUndefined},
+    {"rrecvvh", Builtin::kUndefined},
+    {"ecmm", Builtin::kUndefined},
+    {"vec4j", Builtin::kUndefined},
+    {"vec3X", Builtin::kUndefined},
+    {"vec38", Builtin::kUndefined},
+    {"vecvEE", Builtin::kUndefined},
+    {"z99ci", Builtin::kUndefined},
+    {"JJGeQQ4", Builtin::kUndefined},
+    {"ssec4", Builtin::kUndefined},
+    {"PecK", Builtin::kUndefined},
+    {"tpc4f", Builtin::kUndefined},
+    {"vec", Builtin::kUndefined},
+    {"MMec4f", Builtin::kUndefined},
+    {"vJJc40", Builtin::kUndefined},
+    {"8c", Builtin::kUndefined},
+    {"vecggKh", Builtin::kUndefined},
+    {"vecfi", Builtin::kUndefined},
+    {"vec47Q", Builtin::kUndefined},
+    {"veY4i", Builtin::kUndefined},
+    {"keSu", Builtin::kUndefined},
+    {"n422", Builtin::kUndefined},
+    {"vFFu", Builtin::kUndefined},
 };
 
 using BuiltinParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/builtin/builtin_value_bench.cc b/src/tint/builtin/builtin_value_bench.cc
index 4d2e562..28c01ac 100644
--- a/src/tint/builtin/builtin_value_bench.cc
+++ b/src/tint/builtin/builtin_value_bench.cc
@@ -129,7 +129,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(BuiltinValueParser);
 
diff --git a/src/tint/builtin/diagnostic_rule_bench.cc b/src/tint/builtin/diagnostic_rule_bench.cc
index fb60580..efa6977 100644
--- a/src/tint/builtin/diagnostic_rule_bench.cc
+++ b/src/tint/builtin/diagnostic_rule_bench.cc
@@ -41,7 +41,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(CoreDiagnosticRuleParser);
 
@@ -56,7 +56,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(ChromiumDiagnosticRuleParser);
 
diff --git a/src/tint/builtin/diagnostic_severity_bench.cc b/src/tint/builtin/diagnostic_severity_bench.cc
index 13c3ff1..35fe046 100644
--- a/src/tint/builtin/diagnostic_severity_bench.cc
+++ b/src/tint/builtin/diagnostic_severity_bench.cc
@@ -42,7 +42,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(DiagnosticSeverityParser);
 
diff --git a/src/tint/builtin/extension_bench.cc b/src/tint/builtin/extension_bench.cc
index b3e410e..7a50281 100644
--- a/src/tint/builtin/extension_bench.cc
+++ b/src/tint/builtin/extension_bench.cc
@@ -80,7 +80,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(ExtensionParser);
 
diff --git a/src/tint/builtin/interpolation.h b/src/tint/builtin/interpolation.h
new file mode 100644
index 0000000..ad73c30
--- /dev/null
+++ b/src/tint/builtin/interpolation.h
@@ -0,0 +1,33 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_BUILTIN_INTERPOLATION_H_
+#define SRC_TINT_BUILTIN_INTERPOLATION_H_
+
+#include "src/tint/builtin/interpolation_sampling.h"
+#include "src/tint/builtin/interpolation_type.h"
+
+namespace tint::builtin {
+
+/// The values of an `@interpolate` attribute
+struct Interpolation {
+    /// The first argument of a `@interpolate` attribute
+    builtin::InterpolationType type = builtin::InterpolationType::kUndefined;
+    /// The second argument of a `@interpolate` attribute
+    builtin::InterpolationSampling sampling = builtin::InterpolationSampling::kUndefined;
+};
+
+}  // namespace tint::builtin
+
+#endif  // SRC_TINT_BUILTIN_INTERPOLATION_H_
diff --git a/src/tint/builtin/interpolation_sampling_bench.cc b/src/tint/builtin/interpolation_sampling_bench.cc
index 4fb7e09..deb4de0 100644
--- a/src/tint/builtin/interpolation_sampling_bench.cc
+++ b/src/tint/builtin/interpolation_sampling_bench.cc
@@ -40,7 +40,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(InterpolationSamplingParser);
 
diff --git a/src/tint/builtin/interpolation_type_bench.cc b/src/tint/builtin/interpolation_type_bench.cc
index 5098f26..70b5961 100644
--- a/src/tint/builtin/interpolation_type_bench.cc
+++ b/src/tint/builtin/interpolation_type_bench.cc
@@ -43,7 +43,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(InterpolationTypeParser);
 
diff --git a/src/tint/builtin/texel_format_bench.cc b/src/tint/builtin/texel_format_bench.cc
index 5d202ea..0773b9a 100644
--- a/src/tint/builtin/texel_format_bench.cc
+++ b/src/tint/builtin/texel_format_bench.cc
@@ -62,7 +62,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+}  // NOLINT(readability/fn_size)
 
 BENCHMARK(TexelFormatParser);
 
diff --git a/src/tint/cmd/loopy.cc b/src/tint/cmd/loopy.cc
index 1714e3b..75102eb 100644
--- a/src/tint/cmd/loopy.cc
+++ b/src/tint/cmd/loopy.cc
@@ -19,6 +19,7 @@
 #include "tint/tint.h"
 
 #if TINT_BUILD_IR
+#include "src/tint/ir/converter.h"
 #include "src/tint/ir/module.h"
 #endif  // TINT_BUILD_IR
 
@@ -377,7 +378,7 @@
             loop_count = options.loop_count;
         }
         for (uint32_t i = 0; i < loop_count; ++i) {
-            auto result = tint::ir::Module::FromProgram(program.get());
+            auto result = tint::ir::Converter::FromProgram(program.get());
             if (!result) {
                 std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
             }
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index d61ae24..c8260f1 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -49,10 +49,11 @@
 #include "tint/tint.h"
 
 #if TINT_BUILD_IR
-#include "src/tint/ir/debug.h"
-#include "src/tint/ir/disassembler.h"
-#include "src/tint/ir/module.h"
-#endif  // TINT_BUILD_IR
+#include "src/tint/ir/converter.h"     // nogncheck
+#include "src/tint/ir/debug.h"         // nogncheck
+#include "src/tint/ir/disassembler.h"  // nogncheck
+#include "src/tint/ir/module.h"        // nogncheck
+#endif                                 // TINT_BUILD_IR
 
 namespace {
 
@@ -1078,7 +1079,7 @@
 
 #if TINT_BUILD_IR
     if (options.dump_ir || options.dump_ir_graph) {
-        auto result = tint::ir::Module::FromProgram(program.get());
+        auto result = tint::ir::Converter::FromProgram(program.get());
         if (!result) {
             std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
         } else {
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index 246943d..adafe3a 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -621,8 +621,8 @@
         // Recurse into members.
         for (auto* member : struct_ty->Members()) {
             AddEntryPointInOutVariables(name + "." + member->Name().Name(), member->Type(),
-                                        member->Declaration()->attributes, member->Location(),
-                                        variables);
+                                        member->Declaration()->attributes,
+                                        member->Attributes().location, variables);
         }
         return;
     }
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index ee4b517..0d667ab 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -218,6 +218,32 @@
 
   // Internal types.
   __packed_vec3
+  __atomic_compare_exchange_result_i32
+  __atomic_compare_exchange_result_u32
+  __frexp_result_abstract
+  __frexp_result_f16
+  __frexp_result_f32
+  __frexp_result_vec2_abstract
+  __frexp_result_vec2_f16
+  __frexp_result_vec2_f32
+  __frexp_result_vec3_abstract
+  __frexp_result_vec3_f16
+  __frexp_result_vec3_f32
+  __frexp_result_vec4_abstract
+  __frexp_result_vec4_f16
+  __frexp_result_vec4_f32
+  __modf_result_abstract
+  __modf_result_f16
+  __modf_result_f32
+  __modf_result_vec2_abstract
+  __modf_result_vec2_f16
+  __modf_result_vec2_f32
+  __modf_result_vec3_abstract
+  __modf_result_vec3_f16
+  __modf_result_vec3_f32
+  __modf_result_vec4_abstract
+  __modf_result_vec4_f16
+  __modf_result_vec4_f32
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#attributes
diff --git a/src/tint/ir/binary.cc b/src/tint/ir/binary.cc
index 6cbf742..d3e3548 100644
--- a/src/tint/ir/binary.cc
+++ b/src/tint/ir/binary.cc
@@ -19,8 +19,8 @@
 
 namespace tint::ir {
 
-Binary::Binary(Kind kind, Value* result, Value* lhs, Value* rhs)
-    : Base(result), kind_(kind), lhs_(lhs), rhs_(rhs) {
+Binary::Binary(uint32_t id, Kind kind, const type::Type* ty, Value* lhs, Value* rhs)
+    : Base(id, ty), kind_(kind), lhs_(lhs), rhs_(rhs) {
     TINT_ASSERT(IR, lhs_);
     TINT_ASSERT(IR, rhs_);
     lhs_->AddUsage(this);
@@ -29,69 +29,68 @@
 
 Binary::~Binary() = default;
 
-utils::StringStream& Binary::ToString(utils::StringStream& out) const {
-    Result()->ToString(out) << " = ";
-    lhs_->ToString(out) << " ";
+utils::StringStream& Binary::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = ";
 
     switch (GetKind()) {
         case Binary::Kind::kAdd:
-            out << "+";
+            out << "add";
             break;
         case Binary::Kind::kSubtract:
-            out << "-";
+            out << "sub";
             break;
         case Binary::Kind::kMultiply:
-            out << "*";
+            out << "mul";
             break;
         case Binary::Kind::kDivide:
-            out << "/";
+            out << "div";
             break;
         case Binary::Kind::kModulo:
-            out << "%";
+            out << "mod";
             break;
         case Binary::Kind::kAnd:
-            out << "&";
+            out << "bit_and";
             break;
         case Binary::Kind::kOr:
-            out << "|";
+            out << "bit_or";
             break;
         case Binary::Kind::kXor:
-            out << "^";
+            out << "bit_xor";
             break;
         case Binary::Kind::kLogicalAnd:
-            out << "&&";
+            out << "log_and";
             break;
         case Binary::Kind::kLogicalOr:
-            out << "||";
+            out << "log_or";
             break;
         case Binary::Kind::kEqual:
-            out << "==";
+            out << "eq";
             break;
         case Binary::Kind::kNotEqual:
-            out << "!=";
+            out << "neq";
             break;
         case Binary::Kind::kLessThan:
-            out << "<";
+            out << "lt";
             break;
         case Binary::Kind::kGreaterThan:
-            out << ">";
+            out << "gt";
             break;
         case Binary::Kind::kLessThanEqual:
-            out << "<=";
+            out << "lte";
             break;
         case Binary::Kind::kGreaterThanEqual:
-            out << ">=";
+            out << "gte";
             break;
         case Binary::Kind::kShiftLeft:
-            out << "<<";
+            out << "shiftl";
             break;
         case Binary::Kind::kShiftRight:
-            out << ">>";
+            out << "shiftr";
             break;
     }
     out << " ";
-    rhs_->ToString(out);
-
+    lhs_->ToValue(out) << ", ";
+    rhs_->ToValue(out);
     return out;
 }
 
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index 400e381..92a1998 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -51,17 +51,18 @@
     };
 
     /// Constructor
+    /// @param id the instruction id
     /// @param kind the kind of binary instruction
-    /// @param result the result value
+    /// @param type the result type
     /// @param lhs the lhs of the instruction
     /// @param rhs the rhs of the instruction
-    Binary(Kind kind, Value* result, Value* lhs, Value* rhs);
-    Binary(const Binary& instr) = delete;
-    Binary(Binary&& instr) = delete;
+    Binary(uint32_t id, Kind kind, const type::Type* type, Value* lhs, Value* rhs);
+    Binary(const Binary& inst) = delete;
+    Binary(Binary&& inst) = delete;
     ~Binary() override;
 
-    Binary& operator=(const Binary& instr) = delete;
-    Binary& operator=(Binary&& instr) = delete;
+    Binary& operator=(const Binary& inst) = delete;
+    Binary& operator=(Binary&& inst) = delete;
 
     /// @returns the kind of instruction
     Kind GetKind() const { return kind_; }
@@ -75,7 +76,7 @@
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
     Kind kind_;
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 6be82f9..e03a61d 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -20,537 +20,469 @@
 namespace {
 
 using namespace tint::number_suffixes;  // NOLINT
-                                        //
+
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, CreateAnd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                      b.builder.Constant(2_i));
+    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
+                                     b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kAnd);
+    ASSERT_NE(inst->Type(), nullptr);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    ASSERT_NE(instr->Result()->Type(), nullptr);
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 & 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = bit_and 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateOr) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Or(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                     b.builder.Constant(2_i));
+    const auto* inst = b.builder.Or(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
+                                    b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kOr);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kOr);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 | 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = bit_or 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateXor) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Xor(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                      b.builder.Constant(2_i));
+    const auto* inst = b.builder.Xor(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
+                                     b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kXor);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kXor);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 ^ 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = bit_xor 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateLogicalAnd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.LogicalAnd(b.builder.ir.types.Get<type::Bool>(),
-                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.LogicalAnd(b.builder.ir.types.Get<type::Bool>(),
+                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalAnd);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kLogicalAnd);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 && 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = log_and 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateLogicalOr) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.LogicalOr(b.builder.ir.types.Get<type::Bool>(),
-                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.LogicalOr(b.builder.ir.types.Get<type::Bool>(),
+                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalOr);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kLogicalOr);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 || 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = log_or 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Equal(b.builder.ir.types.Get<type::Bool>(),
-                                        b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.Equal(b.builder.ir.types.Get<type::Bool>(),
+                                       b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kEqual);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 == 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = eq 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateNotEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.NotEqual(b.builder.ir.types.Get<type::Bool>(),
-                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.NotEqual(b.builder.ir.types.Get<type::Bool>(),
+                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kNotEqual);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kNotEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 != 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = neq 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateLessThan) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.LessThan(b.builder.ir.types.Get<type::Bool>(),
-                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.LessThan(b.builder.ir.types.Get<type::Bool>(),
+                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThan);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kLessThan);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 < 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = lt 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateGreaterThan) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.GreaterThan(b.builder.ir.types.Get<type::Bool>(),
-                                              b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.GreaterThan(b.builder.ir.types.Get<type::Bool>(),
+                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThan);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kGreaterThan);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 > 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = gt 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateLessThanEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.LessThanEqual(b.builder.ir.types.Get<type::Bool>(),
-                                                b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.LessThanEqual(b.builder.ir.types.Get<type::Bool>(),
+                                               b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThanEqual);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kLessThanEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 <= 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = lte 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.GreaterThanEqual(
-        b.builder.ir.types.Get<type::Bool>(), b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.GreaterThanEqual(b.builder.ir.types.Get<type::Bool>(),
+                                                  b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThanEqual);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kGreaterThanEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = 4 >= 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = gte 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateShiftLeft) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.ShiftLeft(b.builder.ir.types.Get<type::I32>(),
-                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.ShiftLeft(b.builder.ir.types.Get<type::I32>(),
+                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftLeft);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kShiftLeft);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 << 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = shiftl 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateShiftRight) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.ShiftRight(b.builder.ir.types.Get<type::I32>(),
-                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.ShiftRight(b.builder.ir.types.Get<type::I32>(),
+                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftRight);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kShiftRight);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 >> 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = shiftr 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateAdd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Add(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                      b.builder.Constant(2_i));
+    const auto* inst = b.builder.Add(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
+                                     b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAdd);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kAdd);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 + 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = add 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateSubtract) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Subtract(b.builder.ir.types.Get<type::I32>(),
-                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.Subtract(b.builder.ir.types.Get<type::I32>(),
+                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kSubtract);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kSubtract);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 - 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = sub 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateMultiply) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Multiply(b.builder.ir.types.Get<type::I32>(),
-                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.Multiply(b.builder.ir.types.Get<type::I32>(),
+                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kMultiply);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kMultiply);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 * 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = mul 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateDivide) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Divide(b.builder.ir.types.Get<type::I32>(),
-                                         b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.Divide(b.builder.ir.types.Get<type::I32>(),
+                                        b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kDivide);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kDivide);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 / 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = div 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, CreateModulo) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Modulo(b.builder.ir.types.Get<type::I32>(),
-                                         b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.builder.Modulo(b.builder.ir.types.Get<type::I32>(),
+                                        b.builder.Constant(4_i), b.builder.Constant(2_i));
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kModulo);
+    ASSERT_TRUE(inst->Is<Binary>());
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kModulo);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->LHS()->Is<Constant>());
-    auto lhs = instr->LHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->LHS()->Is<Constant>());
+    auto lhs = inst->LHS()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    ASSERT_TRUE(instr->RHS()->Is<Constant>());
-    auto rhs = instr->RHS()->As<Constant>()->value;
+    ASSERT_TRUE(inst->RHS()->Is<Constant>());
+    auto rhs = inst->RHS()->As<Constant>()->value;
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4 % 2");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = mod 4i, 2i");
 }
 
 TEST_F(IR_InstructionTest, Binary_Usage) {
     auto& b = CreateEmptyBuilder();
+    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
+                                     b.builder.Constant(2_i));
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                      b.builder.Constant(2_i));
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kAnd);
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
+    ASSERT_NE(inst->LHS(), nullptr);
+    ASSERT_EQ(inst->LHS()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->LHS()->Usage()[0], inst);
 
-    ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Result()->Usage()[0], instr);
-
-    ASSERT_NE(instr->LHS(), nullptr);
-    ASSERT_EQ(instr->LHS()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->LHS()->Usage()[0], instr);
-
-    ASSERT_NE(instr->RHS(), nullptr);
-    ASSERT_EQ(instr->RHS()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->RHS()->Usage()[0], instr);
+    ASSERT_NE(inst->RHS(), nullptr);
+    ASSERT_EQ(inst->RHS()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->RHS()->Usage()[0], inst);
 }
 
 TEST_F(IR_InstructionTest, Binary_Usage_DuplicateValue) {
     auto& b = CreateEmptyBuilder();
-
     auto val = b.builder.Constant(4_i);
+    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), val, val);
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), val, val);
+    EXPECT_EQ(inst->GetKind(), Binary::Kind::kAnd);
+    ASSERT_EQ(inst->LHS(), inst->RHS());
 
-    EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
-
-    ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Result()->Usage()[0], instr);
-
-    ASSERT_EQ(instr->LHS(), instr->RHS());
-
-    ASSERT_NE(instr->LHS(), nullptr);
-    ASSERT_EQ(instr->LHS()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->LHS()->Usage()[0], instr);
+    ASSERT_NE(inst->LHS(), nullptr);
+    ASSERT_EQ(inst->LHS()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->LHS()->Usage()[0], inst);
 }
 
 }  // namespace
diff --git a/src/tint/ir/bitcast.cc b/src/tint/ir/bitcast.cc
index 749702e..70f412c 100644
--- a/src/tint/ir/bitcast.cc
+++ b/src/tint/ir/bitcast.cc
@@ -19,18 +19,14 @@
 
 namespace tint::ir {
 
-Bitcast::Bitcast(Value* result, Value* val) : Base(result), val_(val) {
-    TINT_ASSERT(IR, val_);
-    val_->AddUsage(this);
-}
+Bitcast::Bitcast(uint32_t id, const type::Type* type, Value* val)
+    : Base(id, type, utils::Vector{val}) {}
 
 Bitcast::~Bitcast() = default;
 
-utils::StringStream& Bitcast::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = bitcast(";
-    val_->ToString(out);
-    out << ")";
+utils::StringStream& Bitcast::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = bitcast ";
+    EmitArgs(out);
     return out;
 }
 
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index c7d9cb8..253ea0b 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -15,36 +15,31 @@
 #ifndef SRC_TINT_IR_BITCAST_H_
 #define SRC_TINT_IR_BITCAST_H_
 
-#include "src/tint/ir/instruction.h"
+#include "src/tint/ir/call.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A bitcast instruction in the IR.
-class Bitcast : public utils::Castable<Bitcast, Instruction> {
+class Bitcast : public utils::Castable<Bitcast, Call> {
   public:
     /// Constructor
-    /// @param result the result value
+    /// @param id the instruction id
+    /// @param type the result type
     /// @param val the value being bitcast
-    Bitcast(Value* result, Value* val);
-    Bitcast(const Bitcast& instr) = delete;
-    Bitcast(Bitcast&& instr) = delete;
+    Bitcast(uint32_t id, const type::Type* type, Value* val);
+    Bitcast(const Bitcast& inst) = delete;
+    Bitcast(Bitcast&& inst) = delete;
     ~Bitcast() override;
 
-    Bitcast& operator=(const Bitcast& instr) = delete;
-    Bitcast& operator=(Bitcast&& instr) = delete;
-
-    /// @returns the left-hand-side value for the instruction
-    const Value* Val() const { return val_; }
+    Bitcast& operator=(const Bitcast& inst) = delete;
+    Bitcast& operator=(Bitcast&& inst) = delete;
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
-
-  private:
-    Value* val_ = nullptr;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index 2e6ca9d..a70fb92 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -20,44 +20,37 @@
 namespace {
 
 using namespace tint::number_suffixes;  // NOLINT
-                                        //
+
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, Bitcast) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-    ASSERT_NE(instr->Result()->Type(), nullptr);
+    ASSERT_TRUE(inst->Is<ir::Bitcast>());
+    ASSERT_NE(inst->Type(), nullptr);
 
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto val = instr->Val()->As<Constant>()->value;
+    ASSERT_EQ(inst->Args().Length(), 1u);
+    ASSERT_TRUE(inst->Args()[0]->Is<Constant>());
+    auto val = inst->Args()[0]->As<Constant>()->value;
     ASSERT_TRUE(val->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, val->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = bitcast(4)");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = bitcast 4i");
 }
 
 TEST_F(IR_InstructionTest, Bitcast_Usage) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Result()->Usage()[0], instr);
-
-    ASSERT_NE(instr->Val(), nullptr);
-    ASSERT_EQ(instr->Val()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Val()->Usage()[0], instr);
+    ASSERT_EQ(inst->Args().Length(), 1u);
+    ASSERT_NE(inst->Args()[0], nullptr);
+    ASSERT_EQ(inst->Args()[0]->Usage().Length(), 1u);
+    EXPECT_EQ(inst->Args()[0]->Usage()[0], inst);
 }
 
 }  // namespace
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index e83d43a..3981355 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -33,7 +33,7 @@
 
     /// @returns true if this is a dead block. This can happen in the case like a loop merge block
     /// which is never reached.
-    bool IsDead() const { return branch.target == nullptr; }
+    bool IsDead() const override { return branch.target == nullptr; }
 
     /// The node this block branches too.
     Branch branch = {};
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 13ef345..2c97737 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -16,8 +16,6 @@
 
 #include <utility>
 
-#include "src/tint/ir/builder_impl.h"
-
 namespace tint::ir {
 
 Builder::Builder() {}
@@ -26,6 +24,17 @@
 
 Builder::~Builder() = default;
 
+ir::Block* Builder::CreateRootBlockIfNeeded() {
+    if (!ir.root_block) {
+        ir.root_block = CreateBlock();
+
+        // Everything in the module scope must have been const-eval's, so everything will go into a
+        // single block. So, we can create the terminator for the root-block now.
+        ir.root_block->branch.target = CreateTerminator();
+    }
+    return ir.root_block;
+}
+
 Block* Builder::CreateBlock() {
     return ir.flow_nodes.Create<Block>();
 }
@@ -93,12 +102,8 @@
     to->inbound_branches.Push(from);
 }
 
-Runtime::Id Builder::AllocateRuntimeId() {
-    return next_runtime_id++;
-}
-
 Binary* Builder::CreateBinary(Binary::Kind kind, const type::Type* type, Value* lhs, Value* rhs) {
-    return ir.instructions.Create<ir::Binary>(kind, Runtime(type), lhs, rhs);
+    return ir.instructions.Create<ir::Binary>(next_inst_id(), kind, type, lhs, rhs);
 }
 
 Binary* Builder::And(const type::Type* type, Value* lhs, Value* rhs) {
@@ -174,7 +179,7 @@
 }
 
 Unary* Builder::CreateUnary(Unary::Kind kind, const type::Type* type, Value* val) {
-    return ir.instructions.Create<ir::Unary>(kind, Runtime(type), val);
+    return ir.instructions.Create<ir::Unary>(next_inst_id(), kind, type, val);
 }
 
 Unary* Builder::AddressOf(const type::Type* type, Value* val) {
@@ -198,37 +203,43 @@
 }
 
 ir::Bitcast* Builder::Bitcast(const type::Type* type, Value* val) {
-    return ir.instructions.Create<ir::Bitcast>(Runtime(type), val);
+    return ir.instructions.Create<ir::Bitcast>(next_inst_id(), type, val);
 }
 
 ir::Discard* Builder::Discard() {
-    return ir.instructions.Create<ir::Discard>(Runtime(ir.types.Get<type::Void>()));
+    return ir.instructions.Create<ir::Discard>();
 }
 
 ir::UserCall* Builder::UserCall(const type::Type* type,
                                 Symbol name,
                                 utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::UserCall>(Runtime(type), name, std::move(args));
+    return ir.instructions.Create<ir::UserCall>(next_inst_id(), type, name, std::move(args));
 }
 
 ir::Convert* Builder::Convert(const type::Type* to,
                               const type::Type* from,
                               utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Convert>(Runtime(to), from, std::move(args));
+    return ir.instructions.Create<ir::Convert>(next_inst_id(), to, from, std::move(args));
 }
 
 ir::Construct* Builder::Construct(const type::Type* to, utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Construct>(Runtime(to), std::move(args));
+    return ir.instructions.Create<ir::Construct>(next_inst_id(), to, std::move(args));
 }
 
 ir::Builtin* Builder::Builtin(const type::Type* type,
                               builtin::Function func,
                               utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Builtin>(Runtime(type), func, args);
+    return ir.instructions.Create<ir::Builtin>(next_inst_id(), type, func, args);
 }
 
 ir::Store* Builder::Store(Value* to, Value* from) {
     return ir.instructions.Create<ir::Store>(to, from);
 }
 
+ir::Var* Builder::Declare(const type::Type* type,
+                          builtin::AddressSpace address_space,
+                          builtin::Access access) {
+    return ir.instructions.Create<ir::Var>(next_inst_id(), type, address_space, access);
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 3fc78d2..d509c06 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -29,13 +29,13 @@
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
-#include "src/tint/ir/runtime.h"
 #include "src/tint/ir/store.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
 #include "src/tint/ir/unary.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/value.h"
+#include "src/tint/ir/var.h"
 #include "src/tint/type/bool.h"
 #include "src/tint/type/f16.h"
 #include "src/tint/type/f32.h"
@@ -141,13 +141,6 @@
         return Constant(create<constant::Scalar<bool>>(ir.types.Get<type::Bool>(), v));
     }
 
-    /// Creates a new Runtime value
-    /// @param type the type of the temporary
-    /// @returns the new temporary
-    ir::Runtime* Runtime(const type::Type* type) {
-        return ir.values.Create<ir::Runtime>(type, AllocateRuntimeId());
-    }
-
     /// Creates an op for `lhs kind rhs`
     /// @param kind the kind of operation
     /// @param type the result type of the binary expression
@@ -366,14 +359,26 @@
     /// @returns the instruction
     ir::Store* Store(Value* to, Value* from);
 
-    /// @returns a unique runtime id
-    Runtime::Id AllocateRuntimeId();
+    /// Creates a new `var` declaration
+    /// @param type the var type
+    /// @param address_space the address space
+    /// @param access the access mode
+    /// @returns the instruction
+    ir::Var* Declare(const type::Type* type,
+                     builtin::AddressSpace address_space,
+                     builtin::Access access);
+
+    /// Retrieves the root block for the module, creating if necessary
+    /// @returns the root block
+    ir::Block* CreateRootBlockIfNeeded();
 
     /// The IR module.
     Module ir;
 
-    /// The next temporary number to allocate
-    Runtime::Id next_runtime_id = 1;
+  private:
+    uint32_t next_inst_id() { return next_instruction_id_++; }
+
+    uint32_t next_instruction_id_ = 1;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 1f0f79a..19caf6f 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -39,6 +39,7 @@
 #include "src/tint/ast/identifier_expression.h"
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/ast/let.h"
 #include "src/tint/ast/literal_expression.h"
 #include "src/tint/ast/loop_statement.h"
 #include "src/tint/ast/override.h"
@@ -50,6 +51,7 @@
 #include "src/tint/ast/switch_statement.h"
 #include "src/tint/ast/templated_identifier.h"
 #include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/ast/var.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ir/function.h"
@@ -69,8 +71,10 @@
 #include "src/tint/sem/value_constructor.h"
 #include "src/tint/sem/value_conversion.h"
 #include "src/tint/sem/value_expression.h"
+#include "src/tint/sem/variable.h"
 #include "src/tint/switch.h"
 #include "src/tint/type/void.h"
+#include "src/tint/utils/scoped_assignment.h"
 
 namespace tint::ir {
 namespace {
@@ -169,10 +173,13 @@
             [&](const ast::Alias*) {
                 // Folded away and doesn't appear in the IR.
             },
-            // [&](const ast::Variable* var) {
-            // TODO(dsinclair): Implement
-            // },
-            [&](const ast::Function* func) { return EmitFunction(func); },
+            [&](const ast::Variable* var) {
+                // Setup the current flow node to be the root block for the module. The builder will
+                // handle creating it if it doesn't exist already.
+                TINT_SCOPED_ASSIGNMENT(current_flow_block, builder.CreateRootBlockIfNeeded());
+                EmitVariable(var);
+            },
+            [&](const ast::Function* func) { EmitFunction(func); },
             // [&](const ast::Enable*) {
             // TODO(dsinclair): Implement? I think these need to be passed along so further stages
             // know what is enabled.
@@ -291,69 +298,69 @@
     }
 
     auto* ty = lhs.Get()->Type();
-    Binary* instr = nullptr;
+    Binary* inst = nullptr;
     switch (stmt->op) {
         case ast::BinaryOp::kAnd:
-            instr = builder.And(ty, lhs.Get(), rhs.Get());
+            inst = builder.And(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kOr:
-            instr = builder.Or(ty, lhs.Get(), rhs.Get());
+            inst = builder.Or(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kXor:
-            instr = builder.Xor(ty, lhs.Get(), rhs.Get());
+            inst = builder.Xor(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalAnd:
-            instr = builder.LogicalAnd(ty, lhs.Get(), rhs.Get());
+            inst = builder.LogicalAnd(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalOr:
-            instr = builder.LogicalOr(ty, lhs.Get(), rhs.Get());
+            inst = builder.LogicalOr(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kEqual:
-            instr = builder.Equal(ty, lhs.Get(), rhs.Get());
+            inst = builder.Equal(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kNotEqual:
-            instr = builder.NotEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.NotEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThan:
-            instr = builder.LessThan(ty, lhs.Get(), rhs.Get());
+            inst = builder.LessThan(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThan:
-            instr = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
+            inst = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThanEqual:
-            instr = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThanEqual:
-            instr = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftLeft:
-            instr = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
+            inst = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftRight:
-            instr = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
+            inst = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kAdd:
-            instr = builder.Add(ty, lhs.Get(), rhs.Get());
+            inst = builder.Add(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kSubtract:
-            instr = builder.Subtract(ty, lhs.Get(), rhs.Get());
+            inst = builder.Subtract(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kMultiply:
-            instr = builder.Multiply(ty, lhs.Get(), rhs.Get());
+            inst = builder.Multiply(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kDivide:
-            instr = builder.Divide(ty, lhs.Get(), rhs.Get());
+            inst = builder.Divide(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kModulo:
-            instr = builder.Modulo(ty, lhs.Get(), rhs.Get());
+            inst = builder.Modulo(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kNone:
             TINT_ICE(IR, diagnostics_) << "missing binary operand type";
             return;
     }
-    current_flow_block->instructions.Push(instr);
+    current_flow_block->instructions.Push(inst);
 
-    auto store = builder.Store(lhs.Get(), instr->Result());
+    auto store = builder.Store(lhs.Get(), inst);
     current_flow_block->instructions.Push(store);
 }
 
@@ -430,8 +437,8 @@
         BranchToIfNeeded(loop_node->start.target);
     }
 
-    // The loop merge can get disconnected if the loop returns directly, or the continuing target
-    // branches, eventually, to the merge, but nothing branched to the continuing target.
+    // The loop merge can get disconnected if the loop returns directly, or the continuing
+    // target branches, eventually, to the merge, but nothing branched to the continuing target.
     current_flow_block = loop_node->merge.target->As<Block>();
     if (!IsConnected(loop_node->merge.target)) {
         current_flow_block = nullptr;
@@ -613,12 +620,12 @@
 }
 
 // Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
-// code has to continue as before it just predicates writes. If WGSL grows some kind of terminating
-// discard that would probably make sense as a FlowNode but would then require figuring out the
-// multi-level exit that is triggered.
+// code has to continue as before it just predicates writes. If WGSL grows some kind of
+// terminating discard that would probably make sense as a FlowNode but would then require
+// figuring out the multi-level exit that is triggered.
 void BuilderImpl::EmitDiscard(const ast::DiscardStatement*) {
-    auto* instr = builder.Discard();
-    current_flow_block->instructions.Push(instr);
+    auto* inst = builder.Discard();
+    current_flow_block->instructions.Push(inst);
 }
 
 void BuilderImpl::EmitBreakIf(const ast::BreakIfStatement* stmt) {
@@ -655,6 +662,15 @@
 }
 
 utils::Result<Value*> BuilderImpl::EmitExpression(const ast::Expression* expr) {
+    // If this is a value that has been const-eval'd return the result.
+    if (auto* sem = program_->Sem().Get(expr)->As<sem::ValueExpression>()) {
+        if (auto* v = sem->ConstantValue()) {
+            if (auto* cv = v->Clone(clone_ctx_)) {
+                return builder.Constant(cv);
+            }
+        }
+    }
+
     return tint::Switch(
         expr,
         // [&](const ast::IndexAccessorExpression* a) {
@@ -682,14 +698,35 @@
 }
 
 void BuilderImpl::EmitVariable(const ast::Variable* var) {
+    auto* sem = program_->Sem().Get(var);
+
     return tint::Switch(  //
         var,
-        // [&](const ast::Var* var) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::Let*) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::Var* v) {
+            auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+            auto* val = builder.Declare(ty, sem->AddressSpace(), sem->Access());
+            current_flow_block->instructions.Push(val);
+
+            if (v->initializer) {
+                auto init = EmitExpression(v->initializer);
+                if (!init) {
+                    return;
+                }
+
+                auto* store = builder.Store(val, init.Get());
+                current_flow_block->instructions.Push(store);
+            }
+            // TODO(dsinclair): Store the mapping from the var name to the `Declare` value
+        },
+        [&](const ast::Let* l) {
+            // A `let` doesn't exist as a standalone item in the IR, it's just the result of the
+            // initializer.
+            auto init = EmitExpression(l->initializer);
+            if (!init) {
+                return;
+            }
+            // TODO(dsinclair): Store the mapping from the let name to the `init` value
+        },
         [&](const ast::Override*) {
             add_error(var->source,
                       "found an `Override` variable. The SubstituteOverrides "
@@ -701,8 +738,8 @@
             // should never be used.
             //
             // TODO(dsinclair): Probably want to store the const variable somewhere and then in
-            // identifier expression log an error if we ever see a const identifier. Add this when
-            // identifiers and variables are supported.
+            // identifier expression log an error if we ever see a const identifier. Add this
+            // when identifiers and variables are supported.
         },
         [&](Default) {
             add_error(var->source, "unknown variable: " + std::string(var->TypeInfo().name));
@@ -718,27 +755,27 @@
     auto* sem = program_->Sem().Get(expr);
     auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
 
-    Unary* instr = nullptr;
+    Unary* inst = nullptr;
     switch (expr->op) {
         case ast::UnaryOp::kAddressOf:
-            instr = builder.AddressOf(ty, val.Get());
+            inst = builder.AddressOf(ty, val.Get());
             break;
         case ast::UnaryOp::kComplement:
-            instr = builder.Complement(ty, val.Get());
+            inst = builder.Complement(ty, val.Get());
             break;
         case ast::UnaryOp::kIndirection:
-            instr = builder.Indirection(ty, val.Get());
+            inst = builder.Indirection(ty, val.Get());
             break;
         case ast::UnaryOp::kNegation:
-            instr = builder.Negation(ty, val.Get());
+            inst = builder.Negation(ty, val.Get());
             break;
         case ast::UnaryOp::kNot:
-            instr = builder.Not(ty, val.Get());
+            inst = builder.Not(ty, val.Get());
             break;
     }
 
-    current_flow_block->instructions.Push(instr);
-    return instr->Result();
+    current_flow_block->instructions.Push(inst);
+    return inst;
 }
 
 utils::Result<Value*> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
@@ -755,69 +792,69 @@
     auto* sem = program_->Sem().Get(expr);
     auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
 
-    Binary* instr = nullptr;
+    Binary* inst = nullptr;
     switch (expr->op) {
         case ast::BinaryOp::kAnd:
-            instr = builder.And(ty, lhs.Get(), rhs.Get());
+            inst = builder.And(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kOr:
-            instr = builder.Or(ty, lhs.Get(), rhs.Get());
+            inst = builder.Or(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kXor:
-            instr = builder.Xor(ty, lhs.Get(), rhs.Get());
+            inst = builder.Xor(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalAnd:
-            instr = builder.LogicalAnd(ty, lhs.Get(), rhs.Get());
+            inst = builder.LogicalAnd(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLogicalOr:
-            instr = builder.LogicalOr(ty, lhs.Get(), rhs.Get());
+            inst = builder.LogicalOr(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kEqual:
-            instr = builder.Equal(ty, lhs.Get(), rhs.Get());
+            inst = builder.Equal(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kNotEqual:
-            instr = builder.NotEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.NotEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThan:
-            instr = builder.LessThan(ty, lhs.Get(), rhs.Get());
+            inst = builder.LessThan(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThan:
-            instr = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
+            inst = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kLessThanEqual:
-            instr = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kGreaterThanEqual:
-            instr = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
+            inst = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftLeft:
-            instr = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
+            inst = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kShiftRight:
-            instr = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
+            inst = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kAdd:
-            instr = builder.Add(ty, lhs.Get(), rhs.Get());
+            inst = builder.Add(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kSubtract:
-            instr = builder.Subtract(ty, lhs.Get(), rhs.Get());
+            inst = builder.Subtract(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kMultiply:
-            instr = builder.Multiply(ty, lhs.Get(), rhs.Get());
+            inst = builder.Multiply(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kDivide:
-            instr = builder.Divide(ty, lhs.Get(), rhs.Get());
+            inst = builder.Divide(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kModulo:
-            instr = builder.Modulo(ty, lhs.Get(), rhs.Get());
+            inst = builder.Modulo(ty, lhs.Get(), rhs.Get());
             break;
         case ast::BinaryOp::kNone:
             TINT_ICE(IR, diagnostics_) << "missing binary operand type";
             return utils::Failure;
     }
 
-    current_flow_block->instructions.Push(instr);
-    return instr->Result();
+    current_flow_block->instructions.Push(inst);
+    return inst;
 }
 
 utils::Result<Value*> BuilderImpl::EmitBitcast(const ast::BitcastExpression* expr) {
@@ -828,10 +865,10 @@
 
     auto* sem = program_->Sem().Get(expr);
     auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
-    auto* instr = builder.Bitcast(ty, val.Get());
+    auto* inst = builder.Bitcast(ty, val.Get());
 
-    current_flow_block->instructions.Push(instr);
-    return instr->Result();
+    current_flow_block->instructions.Push(inst);
+    return inst;
 }
 
 void BuilderImpl::EmitCall(const ast::CallStatement* stmt) {
@@ -874,29 +911,29 @@
 
     auto* ty = sem->Target()->ReturnType()->Clone(clone_ctx_.type_ctx);
 
-    Instruction* instr = nullptr;
+    Instruction* inst = nullptr;
 
     // If this is a builtin function, emit the specific builtin value
     if (auto* b = sem->Target()->As<sem::Builtin>()) {
-        instr = builder.Builtin(ty, b->Type(), args);
+        inst = builder.Builtin(ty, b->Type(), args);
     } else if (sem->Target()->As<sem::ValueConstructor>()) {
-        instr = builder.Construct(ty, std::move(args));
+        inst = builder.Construct(ty, std::move(args));
     } else if (auto* conv = sem->Target()->As<sem::ValueConversion>()) {
         auto* from = conv->Source()->Clone(clone_ctx_.type_ctx);
-        instr = builder.Convert(ty, from, std::move(args));
+        inst = builder.Convert(ty, from, std::move(args));
     } else if (expr->target->identifier->Is<ast::TemplatedIdentifier>()) {
         TINT_UNIMPLEMENTED(IR, diagnostics_) << "missing templated ident support";
         return utils::Failure;
     } else {
         // Not a builtin and not a templated call, so this is a user function.
         auto name = CloneSymbol(expr->target->identifier->symbol);
-        instr = builder.UserCall(ty, name, std::move(args));
+        inst = builder.UserCall(ty, name, std::move(args));
     }
-    if (instr == nullptr) {
+    if (inst == nullptr) {
         return utils::Failure;
     }
-    current_flow_block->instructions.Push(instr);
-    return instr->Result();
+    current_flow_block->instructions.Push(inst);
+    return inst;
 }
 
 utils::Result<Value*> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index cd6bf0b..1e6a26d 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -42,10 +42,10 @@
     EXPECT_EQ(1u, f->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, f->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function f
-  %bb1 = Block
-  Return ()
-FunctionEnd
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func f
+  %fn1 = block
+  ret
+func_end
 
 )");
 }
@@ -88,23 +88,23 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = if (true)
+  %fn2 = if true [t: %fn3, f: %fn4, m: %fn5]
     # true branch
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn5
 
     # false branch
-    %bb5 = Block
-    BranchTo %bb4 ()
+    %fn4 = block
+    branch %fn5
 
   # if merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -136,22 +136,22 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = if (true)
+  %fn2 = if true [t: %fn3, f: %fn4, m: %fn5]
     # true branch
-    %bb3 = Block
-    Return ()
+    %fn3 = block
+    ret
     # false branch
-    %bb4 = Block
-    BranchTo %bb5 ()
+    %fn4 = block
+    branch %fn5
 
   # if merge
-  %bb5 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -183,22 +183,22 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = if (true)
+  %fn2 = if true [t: %fn3, f: %fn4, m: %fn5]
     # true branch
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn5
 
     # false branch
-    %bb5 = Block
-    Return ()
+    %fn4 = block
+    ret
   # if merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -230,18 +230,18 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = if (true)
+  %fn2 = if true [t: %fn3, f: %fn4]
     # true branch
-    %bb3 = Block
-    Return ()
+    %fn3 = block
+    ret
     # false branch
-    %bb4 = Block
-    Return ()
-FunctionEnd
+    %fn4 = block
+    ret
+func_end
 
 )");
 }
@@ -273,36 +273,32 @@
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = if (true)
+  %fn2 = if true [t: %fn3, f: %fn4, m: %fn5]
     # true branch
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn6
 
-    %bb4 = loop
+    %fn6 = loop [s: %fn7, m: %fn8]
       # loop start
-      %bb5 = Block
-      BranchTo %bb6 ()
-
-      # loop continuing
-      %bb7 = Block
-      BranchTo %bb5 ()
+      %fn7 = block
+      branch %fn8
 
     # loop merge
-    %bb6 = Block
-    BranchTo %bb8 ()
+    %fn8 = block
+    branch %fn5
 
     # false branch
-    %bb9 = Block
-    BranchTo %bb8 ()
+    %fn4 = block
+    branch %fn5
 
   # if merge
-  %bb8 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -334,23 +330,19 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, m: %fn4]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
-
-    # loop continuing
-    %bb5 = Block
-    BranchTo %bb3 ()
+    %fn3 = block
+    branch %fn4
 
   # loop merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -396,36 +388,36 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, c: %fn4, m: %fn5]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn6
 
-    %bb4 = if (true)
+    %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
       # true branch
-      %bb5 = Block
-      BranchTo %bb6 ()
+      %fn7 = block
+      branch %fn5
 
       # false branch
-      %bb7 = Block
-      BranchTo %bb8 ()
+      %fn8 = block
+      branch %fn9
 
     # if merge
-    %bb8 = Block
-    BranchTo %bb9 ()
+    %fn9 = block
+    branch %fn4
 
     # loop continuing
-    %bb9 = Block
-    BranchTo %bb3 ()
+    %fn4 = block
+    branch %fn3
 
   # loop merge
-  %bb6 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -471,36 +463,36 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, c: %fn4, m: %fn5]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn4
 
     # loop continuing
-    %bb4 = Block
-    BranchTo %bb5 ()
+    %fn4 = block
+    branch %fn6
 
-    %bb5 = if (true)
+    %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
       # true branch
-      %bb6 = Block
-      BranchTo %bb7 ()
+      %fn7 = block
+      branch %fn5
 
       # false branch
-      %bb8 = Block
-      BranchTo %bb9 ()
+      %fn8 = block
+      branch %fn9
 
     # if merge
-    %bb9 = Block
-    BranchTo %bb3 ()
+    %fn9 = block
+    branch %fn3
 
   # loop merge
-  %bb7 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -546,34 +538,32 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, c: %fn4]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn5
 
-    %bb4 = if (true)
+    %fn5 = if true [t: %fn6, f: %fn7, m: %fn8]
       # true branch
-      %bb5 = Block
-      Return ()
+      %fn6 = block
+      ret
       # false branch
-      %bb6 = Block
-      BranchTo %bb7 ()
+      %fn7 = block
+      branch %fn8
 
     # if merge
-    %bb7 = Block
-    BranchTo %bb8 ()
+    %fn8 = block
+    branch %fn4
 
     # loop continuing
-    %bb8 = Block
-    BranchTo %bb3 ()
+    %fn4 = block
+    branch %fn3
 
-  # loop merge
-  # Dead
-FunctionEnd
+func_end
 
 )");
 }
@@ -605,21 +595,15 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3]
     # loop start
-    %bb3 = Block
-    Return ()
-    # loop continuing
-    %bb4 = Block
-    BranchTo %bb3 ()
-
-  # loop merge
-  # Dead
-FunctionEnd
+    %fn3 = block
+    ret
+func_end
 
 )");
 }
@@ -629,6 +613,9 @@
     // `ast_if` below), it doesn't get emitted as there is no way to reach the
     // loop merge due to the loop itself doing a `return`. This is why the
     // loop merge gets marked as Dead and the `ast_if` doesn't appear.
+    //
+    // Similar, the continuing block goes away as there is no way to get there, so it's treated
+    // as dead code and dropped.
     auto* ast_break_if = BreakIf(true);
     auto* ast_loop = Loop(Block(Return()), Block(ast_break_if));
     auto* ast_if = If(true, Block(Return()));
@@ -670,34 +657,15 @@
     // This is 1 because only the loop branch happens. The subsequent if return is dead code.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3]
     # loop start
-    %bb3 = Block
-    Return ()
-    # loop continuing
-    %bb4 = Block
-    BranchTo %bb5 ()
-
-    %bb5 = if (true)
-      # true branch
-      %bb6 = Block
-      BranchTo %bb7 ()
-
-      # false branch
-      %bb8 = Block
-      BranchTo %bb9 ()
-
-    # if merge
-    %bb9 = Block
-    BranchTo %bb3 ()
-
-  # loop merge
-  # Dead
-FunctionEnd
+    %fn3 = block
+    ret
+func_end
 
 )");
 }
@@ -743,32 +711,28 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, m: %fn4]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn5
 
-    %bb4 = if (true)
+    %fn5 = if true [t: %fn6, f: %fn7]
       # true branch
-      %bb5 = Block
-      BranchTo %bb6 ()
+      %fn6 = block
+      branch %fn4
 
       # false branch
-      %bb7 = Block
-      BranchTo %bb6 ()
-
-    # loop continuing
-    %bb8 = Block
-    BranchTo %bb3 ()
+      %fn7 = block
+      branch %fn4
 
   # loop merge
-  %bb6 = Block
-  Return ()
-FunctionEnd
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -893,114 +857,110 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, c: %fn4, m: %fn5]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn6
 
-    %bb4 = loop
+    %fn6 = loop [s: %fn7, c: %fn8, m: %fn9]
       # loop start
-      %bb5 = Block
-      BranchTo %bb6 ()
+      %fn7 = block
+      branch %fn10
 
-      %bb6 = if (true)
+      %fn10 = if true [t: %fn11, f: %fn12, m: %fn13]
         # true branch
-        %bb7 = Block
-        BranchTo %bb8 ()
+        %fn11 = block
+        branch %fn9
 
         # false branch
-        %bb9 = Block
-        BranchTo %bb10 ()
+        %fn12 = block
+        branch %fn13
 
       # if merge
-      %bb10 = Block
-      BranchTo %bb11 ()
+      %fn13 = block
+      branch %fn14
 
-      %bb11 = if (true)
+      %fn14 = if true [t: %fn15, f: %fn16, m: %fn17]
         # true branch
-        %bb12 = Block
-        BranchTo %bb13 ()
+        %fn15 = block
+        branch %fn8
 
         # false branch
-        %bb14 = Block
-        BranchTo %bb15 ()
+        %fn16 = block
+        branch %fn17
 
       # if merge
-      %bb15 = Block
-      BranchTo %bb13 ()
+      %fn17 = block
+      branch %fn8
 
       # loop continuing
-      %bb13 = Block
-      BranchTo %bb16 ()
+      %fn8 = block
+      branch %fn18
 
-      %bb16 = loop
+      %fn18 = loop [s: %fn19, m: %fn20]
         # loop start
-        %bb17 = Block
-        BranchTo %bb18 ()
-
-        # loop continuing
-        %bb19 = Block
-        BranchTo %bb17 ()
+        %fn19 = block
+        branch %fn20
 
       # loop merge
-      %bb18 = Block
-      BranchTo %bb20 ()
+      %fn20 = block
+      branch %fn21
 
-      %bb20 = loop
+      %fn21 = loop [s: %fn22, c: %fn23, m: %fn24]
         # loop start
-        %bb21 = Block
-        BranchTo %bb22 ()
+        %fn22 = block
+        branch %fn23
 
         # loop continuing
-        %bb22 = Block
-        BranchTo %bb23 ()
+        %fn23 = block
+        branch %fn25
 
-        %bb23 = if (true)
+        %fn25 = if true [t: %fn26, f: %fn27, m: %fn28]
           # true branch
-          %bb24 = Block
-          BranchTo %bb25 ()
+          %fn26 = block
+          branch %fn24
 
           # false branch
-          %bb26 = Block
-          BranchTo %bb27 ()
+          %fn27 = block
+          branch %fn28
 
         # if merge
-        %bb27 = Block
-        BranchTo %bb21 ()
+        %fn28 = block
+        branch %fn22
 
       # loop merge
-      %bb25 = Block
-      BranchTo %bb5 ()
+      %fn24 = block
+      branch %fn7
 
     # loop merge
-    %bb8 = Block
-    BranchTo %bb28 ()
+    %fn9 = block
+    branch %fn29
 
-    %bb28 = if (true)
+    %fn29 = if true [t: %fn30, f: %fn31, m: %fn32]
       # true branch
-      %bb29 = Block
-      BranchTo %bb30 ()
+      %fn30 = block
+      branch %fn5
 
       # false branch
-      %bb31 = Block
-      BranchTo %bb32 ()
+      %fn31 = block
+      branch %fn32
 
     # if merge
-    %bb32 = Block
-    BranchTo %bb33 ()
+    %fn32 = block
+    branch %fn4
 
     # loop continuing
-    %bb33 = Block
-    BranchTo %bb3 ()
+    %fn4 = block
+    branch %fn3
 
   # loop merge
-  %bb30 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -1041,36 +1001,36 @@
     EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, c: %fn4, m: %fn5]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn6
 
-    %bb4 = if (false)
+    %fn6 = if false [t: %fn7, f: %fn8, m: %fn9]
       # true branch
-      %bb5 = Block
-      BranchTo %bb6 ()
+      %fn7 = block
+      branch %fn9
 
       # false branch
-      %bb7 = Block
-      BranchTo %bb8 ()
+      %fn8 = block
+      branch %fn5
 
     # if merge
-    %bb6 = Block
-    BranchTo %bb9 ()
+    %fn9 = block
+    branch %fn4
 
     # loop continuing
-    %bb9 = Block
-    BranchTo %bb3 ()
+    %fn4 = block
+    branch %fn3
 
   # loop merge
-  %bb8 = Block
-  Return ()
-FunctionEnd
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -1111,35 +1071,31 @@
     EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, m: %fn4]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
+    %fn3 = block
+    branch %fn5
 
-    %bb4 = if (true)
+    %fn5 = if true [t: %fn6, f: %fn7, m: %fn8]
       # true branch
-      %bb5 = Block
-      BranchTo %bb6 ()
+      %fn6 = block
+      branch %fn8
 
       # false branch
-      %bb7 = Block
-      BranchTo %bb8 ()
+      %fn7 = block
+      branch %fn4
 
     # if merge
-    %bb6 = Block
-    Return ()
-    # loop continuing
-    %bb9 = Block
-    BranchTo %bb3 ()
-
+    %fn8 = block
+    ret
   # loop merge
-  %bb8 = Block
-  Return ()
-FunctionEnd
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -1222,23 +1178,19 @@
     EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = loop
+  %fn2 = loop [s: %fn3, m: %fn4]
     # loop start
-    %bb3 = Block
-    BranchTo %bb4 ()
-
-    # loop continuing
-    %bb5 = Block
-    BranchTo %bb3 ()
+    %fn3 = block
+    branch %fn4
 
   # loop merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -1285,27 +1237,83 @@
     EXPECT_EQ(3u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = Switch (1)
-    # Case 0
-    %bb3 = Block
-    BranchTo %bb4 ()
+  %fn2 = switch 1i [c: (0i, %fn3), c: (1i, %fn4), c: (default, %fn5), m: %fn6]
+    # case 0i
+    %fn3 = block
+    branch %fn6
 
-    # Case 1
-    %bb5 = Block
-    BranchTo %bb4 ()
+    # case 1i
+    %fn4 = block
+    branch %fn6
 
-    # Case default
-    %bb6 = Block
-    BranchTo %bb4 ()
+    # case default
+    %fn5 = block
+    branch %fn6
 
-  # Switch Merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  # switch merge
+  %fn6 = block
+  ret
+func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Switch_MultiSelector) {
+    auto* ast_switch = Switch(
+        1_i,
+        utils::Vector{Case(
+            utils::Vector{CaseSelector(0_i), CaseSelector(1_i), DefaultCaseSelector()}, Block())});
+
+    WrapInFunction(ast_switch);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    auto* ir_switch = FlowNodeForAstNode(ast_switch);
+    ASSERT_NE(ir_switch, nullptr);
+    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
+
+    auto* flow = ir_switch->As<ir::Switch>();
+    ASSERT_NE(flow->merge.target, nullptr);
+    ASSERT_EQ(1u, flow->cases.Length());
+
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
+
+    ASSERT_EQ(3u, flow->cases[0].selectors.Length());
+    ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(0_i,
+              flow->cases[0].selectors[0].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
+
+    ASSERT_TRUE(flow->cases[0].selectors[1].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(1_i,
+              flow->cases[0].selectors[1].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
+
+    EXPECT_TRUE(flow->cases[0].selectors[2].IsDefault());
+
+    EXPECT_EQ(1u, flow->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[0].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
+    EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
+
+  %fn2 = switch 1i [c: (0i 1i default, %fn3), m: %fn4]
+    # case 0i 1i default
+    %fn3 = block
+    branch %fn4
+
+  # switch merge
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -1337,19 +1345,19 @@
     EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = Switch (1)
-    # Case default
-    %bb3 = Block
-    BranchTo %bb4 ()
+  %fn2 = switch 1i [c: (default, %fn3), m: %fn4]
+    # case default
+    %fn3 = block
+    branch %fn4
 
-  # Switch Merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  # switch merge
+  %fn4 = block
+  ret
+func_end
 
 )");
 }
@@ -1390,23 +1398,23 @@
     // This is 1 because the if is dead-code eliminated and the return doesn't happen.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = Switch (1)
-    # Case 0
-    %bb3 = Block
-    BranchTo %bb4 ()
+  %fn2 = switch 1i [c: (0i, %fn3), c: (default, %fn4), m: %fn5]
+    # case 0i
+    %fn3 = block
+    branch %fn5
 
-    # Case default
-    %bb5 = Block
-    BranchTo %bb4 ()
+    # case default
+    %fn4 = block
+    branch %fn5
 
-  # Switch Merge
-  %bb4 = Block
-  Return ()
-FunctionEnd
+  # switch merge
+  %fn5 = block
+  ret
+func_end
 
 )");
 }
@@ -1449,20 +1457,18 @@
     EXPECT_EQ(0u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  BranchTo %bb2 ()
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  branch %fn2
 
-  %bb2 = Switch (1)
-    # Case 0
-    %bb3 = Block
-    Return ()
-    # Case default
-    %bb4 = Block
-    Return ()
-  # Switch Merge
-  # Dead
-FunctionEnd
+  %fn2 = switch 1i [c: (0i, %fn3), c: (default, %fn4)]
+    # case 0i
+    %fn3 = block
+    ret
+    # case default
+    %fn4 = block
+    ret
+func_end
 
 )");
 }
@@ -1552,8 +1558,75 @@
     EXPECT_EQ(2_u, val->As<constant::Scalar<u32>>()->ValueAs<f32>());
 }
 
+TEST_F(IR_BuilderImplTest, Emit_GlobalVar_NoInit) {
+    GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = block
+%1(ref<private, u32, read_write>) = var private read_write
+ret
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_GlobalVar_Init) {
+    auto* expr = Expr(2_u);
+    GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = block
+%1(ref<private, u32, read_write>) = var private read_write
+store %1(ref<private, u32, read_write>), 2u
+ret
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_Var_NoInit) {
+    auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction);
+    WrapInFunction(a);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  %1(ref<function, u32, read_write>) = var function read_write
+  ret
+func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_Var_Init) {
+    auto* expr = Expr(2_u);
+    auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction, expr);
+    WrapInFunction(a);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  %1(ref<function, u32, read_write>) = var function read_write
+  store %1(ref<function, u32, read_write>), 2u
+  ret
+func_end
+
+)");
+}
+
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
-    auto* expr = Add(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Add(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1564,12 +1637,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 + 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = add %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Subtract) {
-    auto* expr = Sub(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Sub(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1580,12 +1655,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 - 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = sub %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Multiply) {
-    auto* expr = Mul(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Mul(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1596,12 +1673,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 * 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = mul %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Div) {
-    auto* expr = Div(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Div(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1612,12 +1691,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 / 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = div %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Modulo) {
-    auto* expr = Mod(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Mod(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1628,12 +1709,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 % 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = mod %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_And) {
-    auto* expr = And(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = And(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1644,12 +1727,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 & 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = bit_and %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Or) {
-    auto* expr = Or(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Or(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1660,12 +1745,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 | 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = bit_or %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Xor) {
-    auto* expr = Xor(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Xor(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1676,12 +1763,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 ^ 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = bit_xor %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalAnd) {
-    auto* expr = LogicalAnd(true, false);
+    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = LogicalAnd(Call("my_func"), false);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1692,12 +1781,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = true && false
+    EXPECT_EQ(d.AsString(), R"(%1(bool) = call my_func
+%2(bool) = log_and %1(bool), false
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalOr) {
-    auto* expr = LogicalOr(false, true);
+    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = LogicalOr(Call("my_func"), true);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1708,12 +1799,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = false || true
+    EXPECT_EQ(d.AsString(), R"(%1(bool) = call my_func
+%2(bool) = log_or %1(bool), true
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Equal) {
-    auto* expr = Equal(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Equal(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1724,12 +1817,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 == 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = eq %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_NotEqual) {
-    auto* expr = NotEqual(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = NotEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1740,12 +1835,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 != 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = neq %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThan) {
-    auto* expr = LessThan(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = LessThan(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1756,12 +1853,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 < 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = lt %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThan) {
-    auto* expr = GreaterThan(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = GreaterThan(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1772,12 +1871,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 > 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = gt %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThanEqual) {
-    auto* expr = LessThanEqual(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = LessThanEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1788,12 +1889,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 <= 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = lte %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThanEqual) {
-    auto* expr = GreaterThanEqual(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = GreaterThanEqual(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1804,12 +1907,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 >= 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(bool) = gte %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftLeft) {
-    auto* expr = Shl(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Shl(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1820,12 +1925,14 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 << 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = shiftl %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftRight) {
-    auto* expr = Shr(3_u, 4_u);
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Shr(Call("my_func"), 4_u);
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1836,13 +1943,16 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 >> 4
+    EXPECT_EQ(d.AsString(), R"(%1(u32) = call my_func
+%2(u32) = shiftr %1(u32), 4u
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound) {
-    auto* expr = LogicalAnd(LessThan(1_u, Add(Shr(3_u, 4_u), 9_u)),
-                            GreaterThan(2.5_f, Div(6.7_f, Mul(2.3_f, 5.5_f))));
+    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
+
+    auto* expr = LogicalAnd(LessThan(Call("my_func"), 2_f),
+                            GreaterThan(2.5_f, Div(Call("my_func"), Mul(2.3_f, Call("my_func")))));
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1853,18 +1963,39 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 >> 4
-%2 (u32) = %1 (u32) + 9
-%3 (bool) = 1 < %2 (u32)
-%4 (f32) = 2.29999995231628417969 * 5.5
-%5 (f32) = 6.69999980926513671875 / %4 (f32)
-%6 (bool) = 2.5 > %5 (f32)
-%7 (bool) = %3 (bool) && %6 (bool)
+    EXPECT_EQ(d.AsString(), R"(%1(f32) = call my_func
+%2(bool) = lt %1(f32), 2.0f
+%3(f32) = call my_func
+%4(f32) = call my_func
+%5(f32) = mul 2.29999995231628417969f, %4(f32)
+%6(f32) = div %3(f32), %5(f32)
+%7(bool) = gt 2.5f, %6(f32)
+%8(bool) = log_and %2(bool), %7(bool)
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound_WithConstEval) {
+    Func("my_func", utils::Vector{Param("p", ty.bool_())}, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = Call("my_func", LogicalAnd(LessThan(2.4_f, 2_f),
+                                            GreaterThan(2.5_f, Div(10_f, Mul(2.3_f, 9.4_f)))));
+    WrapInFunction(expr);
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    auto r = b.EmitExpression(expr);
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%1(bool) = call my_func, false
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitExpression_Bitcast) {
-    auto* expr = Bitcast<f32>(3_u);
+    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
+
+    auto* expr = Bitcast<f32>(Call("my_func"));
     WrapInFunction(expr);
 
     auto& b = CreateBuilder();
@@ -1875,7 +2006,8 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (f32) = bitcast(3)
+    EXPECT_EQ(d.AsString(), R"(%1(f32) = call my_func
+%2(f32) = bitcast %1(f32)
 )");
 }
 
@@ -1893,25 +2025,25 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (void) = discard
+    EXPECT_EQ(d.AsString(), R"(discard
 )");
 }
 
 TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
     Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
 
-    auto* stmt = CallStmt(Call("my_func", Mul(2_f, 3_f)));
+    auto* stmt = CallStmt(Call("my_func", Mul(2_a, 3_a)));
     WrapInFunction(stmt);
 
     auto& b = CreateBuilder();
+
     InjectFlowBlock();
     b.EmitStatement(stmt);
     ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (f32) = 2.0 * 3.0
-%2 (void) = call(my_func, %1 (f32))
+    EXPECT_EQ(d.AsString(), R"(%1(void) = call my_func, 6.0f
 )");
 }
 
@@ -1930,7 +2062,7 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1 (vec3<f32>) = construct(vec3<f32>)
+    EXPECT_EQ(d.AsString(), R"(%1(vec3<f32>) = construct
 )");
 }
 
@@ -1948,7 +2080,7 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%2 (vec3<f32>) = construct(vec3<f32>, 2.0, 3.0, %1 (void))
+    EXPECT_EQ(d.AsString(), R"(%2(vec3<f32>) = construct 2.0f, 3.0f, %1(void)
 )");
 }
 
@@ -1966,7 +2098,7 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%2 (f32) = convert(f32, i32, %1 (void))
+    EXPECT_EQ(d.AsString(), R"(%2(f32) = convert i32, %1(void)
 )");
 }
 
@@ -1979,10 +2111,10 @@
     ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
-  %bb1 = Block
-  Return (2.0)
-FunctionEnd
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  ret 2.0f
+func_end
 
 )");
 }
@@ -2001,7 +2133,7 @@
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%2 (f32) = asin(%1 (void))
+    EXPECT_EQ(d.AsString(), R"(%2(f32) = asin %1(void)
 )");
 }
 
diff --git a/src/tint/ir/builtin.cc b/src/tint/ir/builtin.cc
index 4237797..54cf882 100644
--- a/src/tint/ir/builtin.cc
+++ b/src/tint/ir/builtin.cc
@@ -20,16 +20,17 @@
 // \cond DO_NOT_DOCUMENT
 namespace tint::ir {
 
-Builtin::Builtin(Value* result, builtin::Function func, utils::VectorRef<Value*> args)
-    : Base(result, args), func_(func) {}
+Builtin::Builtin(uint32_t id,
+                 const type::Type* type,
+                 builtin::Function func,
+                 utils::VectorRef<Value*> args)
+    : Base(id, type, args), func_(func) {}
 
 Builtin::~Builtin() = default;
 
-utils::StringStream& Builtin::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = " << builtin::str(func_) << "(";
+utils::StringStream& Builtin::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = " << builtin::str(func_) << " ";
     EmitArgs(out);
-    out << ")";
     return out;
 }
 
diff --git a/src/tint/ir/builtin.h b/src/tint/ir/builtin.h
index b1fd61d..9f6f666 100644
--- a/src/tint/ir/builtin.h
+++ b/src/tint/ir/builtin.h
@@ -26,16 +26,20 @@
 class Builtin : public utils::Castable<Builtin, Call> {
   public:
     /// Constructor
-    /// @param result the result value
+    /// @param id the instruction id
+    /// @param type the result type
     /// @param func the builtin function
     /// @param args the conversion arguments
-    Builtin(Value* result, builtin::Function func, utils::VectorRef<Value*> args);
-    Builtin(const Builtin& instr) = delete;
-    Builtin(Builtin&& instr) = delete;
+    Builtin(uint32_t id,
+            const type::Type* type,
+            builtin::Function func,
+            utils::VectorRef<Value*> args);
+    Builtin(const Builtin& inst) = delete;
+    Builtin(Builtin&& inst) = delete;
     ~Builtin() override;
 
-    Builtin& operator=(const Builtin& instr) = delete;
-    Builtin& operator=(Builtin&& instr) = delete;
+    Builtin& operator=(const Builtin& inst) = delete;
+    Builtin& operator=(Builtin&& inst) = delete;
 
     /// @returns the builtin function
     builtin::Function Func() const { return func_; }
@@ -43,7 +47,7 @@
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
     const builtin::Function func_;
diff --git a/src/tint/ir/call.cc b/src/tint/ir/call.cc
index b801260..322f83d 100644
--- a/src/tint/ir/call.cc
+++ b/src/tint/ir/call.cc
@@ -18,7 +18,10 @@
 
 namespace tint::ir {
 
-Call::Call(Value* result, utils::VectorRef<Value*> args) : Base(result), args_(args) {
+Call::Call() : Base() {}
+
+Call::Call(uint32_t id, const type::Type* type, utils::VectorRef<Value*> args)
+    : Base(id, type), args_(args) {
     for (auto* arg : args) {
         arg->AddUsage(this);
     }
@@ -33,7 +36,7 @@
             out << ", ";
         }
         first = false;
-        arg->ToString(out);
+        arg->ToValue(out);
     }
 }
 
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
index e7bf684..a49e9fa 100644
--- a/src/tint/ir/call.h
+++ b/src/tint/ir/call.h
@@ -24,16 +24,12 @@
 /// A Call instruction in the IR.
 class Call : public utils::Castable<Call, Instruction> {
   public:
-    /// Constructor
-    /// @param result the result value
-    /// @param args the constructor arguments
-    Call(Value* result, utils::VectorRef<Value*> args);
-    Call(const Call& instr) = delete;
-    Call(Call&& instr) = delete;
+    Call(const Call& inst) = delete;
+    Call(Call&& inst) = delete;
     ~Call() override;
 
-    Call& operator=(const Call& instr) = delete;
-    Call& operator=(Call&& instr) = delete;
+    Call& operator=(const Call& inst) = delete;
+    Call& operator=(Call&& inst) = delete;
 
     /// @returns the constructor arguments
     utils::VectorRef<Value*> Args() const { return args_; }
@@ -42,6 +38,15 @@
     /// @param out the output stream
     void EmitArgs(utils::StringStream& out) const;
 
+  protected:
+    /// Constructor
+    Call();
+    /// Constructor
+    /// @param id the instruction id
+    /// @param type the result type
+    /// @param args the constructor arguments
+    Call(uint32_t id, const type::Type* type, utils::VectorRef<Value*> args);
+
   private:
     utils::Vector<Value*, 1> args_;
 };
diff --git a/src/tint/ir/constant.cc b/src/tint/ir/constant.cc
index 4036616..0666e91 100644
--- a/src/tint/ir/constant.cc
+++ b/src/tint/ir/constant.cc
@@ -29,33 +29,39 @@
 
 Constant::~Constant() = default;
 
-utils::StringStream& Constant::ToString(utils::StringStream& out) const {
+utils::StringStream& Constant::ToValue(utils::StringStream& out) const {
     std::function<void(const constant::Value*)> emit = [&](const constant::Value* c) {
         Switch(
             c,
             [&](const constant::Scalar<AFloat>* scalar) { out << scalar->ValueAs<AFloat>().value; },
             [&](const constant::Scalar<AInt>* scalar) { out << scalar->ValueAs<AInt>().value; },
-            [&](const constant::Scalar<i32>* scalar) { out << scalar->ValueAs<i32>().value; },
-            [&](const constant::Scalar<u32>* scalar) { out << scalar->ValueAs<u32>().value; },
-            [&](const constant::Scalar<f32>* scalar) { out << scalar->ValueAs<f32>().value; },
-            [&](const constant::Scalar<f16>* scalar) { out << scalar->ValueAs<f16>().value; },
+            [&](const constant::Scalar<i32>* scalar) {
+                out << scalar->ValueAs<i32>().value << "i";
+            },
+            [&](const constant::Scalar<u32>* scalar) {
+                out << scalar->ValueAs<u32>().value << "u";
+            },
+            [&](const constant::Scalar<f32>* scalar) {
+                out << scalar->ValueAs<f32>().value << "f";
+            },
+            [&](const constant::Scalar<f16>* scalar) {
+                out << scalar->ValueAs<f16>().value << "h";
+            },
             [&](const constant::Scalar<bool>* scalar) {
                 out << (scalar->ValueAs<bool>() ? "true" : "false");
             },
             [&](const constant::Splat* splat) {
-                out << splat->Type()->FriendlyName() << "(";
+                out << splat->Type()->FriendlyName() << " ";
                 emit(splat->Index(0));
-                out << ")";
             },
             [&](const constant::Composite* composite) {
-                out << composite->Type()->FriendlyName() << "(";
+                out << composite->Type()->FriendlyName() << " ";
                 for (const auto* elem : composite->elements) {
                     if (elem != composite->elements[0]) {
                         out << ", ";
                     }
                     emit(elem);
                 }
-                out << ")";
             });
     };
     emit(value);
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index 207af5d..2413721 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -35,7 +35,7 @@
     /// Write the constant to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToValue(utils::StringStream& out) const override;
 
     /// The constants value
     const constant::Value* const value;
diff --git a/src/tint/ir/constant_test.cc b/src/tint/ir/constant_test.cc
index f407cdb..43d92b4 100644
--- a/src/tint/ir/constant_test.cc
+++ b/src/tint/ir/constant_test.cc
@@ -31,8 +31,8 @@
     auto* c = b.builder.Constant(1.2_f);
     EXPECT_EQ(1.2_f, c->value->As<constant::Scalar<f32>>()->ValueAs<f32>());
 
-    c->ToString(str);
-    EXPECT_EQ("1.20000004768371582031", str.str());
+    c->ToValue(str);
+    EXPECT_EQ("1.20000004768371582031f", str.str());
 
     EXPECT_TRUE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
@@ -49,8 +49,8 @@
     auto* c = b.builder.Constant(1.1_h);
     EXPECT_EQ(1.1_h, c->value->As<constant::Scalar<f16>>()->ValueAs<f16>());
 
-    c->ToString(str);
-    EXPECT_EQ("1.099609375", str.str());
+    c->ToValue(str);
+    EXPECT_EQ("1.099609375h", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_TRUE(c->value->Is<constant::Scalar<f16>>());
@@ -67,8 +67,8 @@
     auto* c = b.builder.Constant(1_i);
     EXPECT_EQ(1_i, c->value->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    c->ToString(str);
-    EXPECT_EQ("1", str.str());
+    c->ToValue(str);
+    EXPECT_EQ("1i", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
@@ -85,8 +85,8 @@
     auto* c = b.builder.Constant(2_u);
     EXPECT_EQ(2_u, c->value->As<constant::Scalar<u32>>()->ValueAs<u32>());
 
-    c->ToString(str);
-    EXPECT_EQ("2", str.str());
+    c->ToValue(str);
+    EXPECT_EQ("2u", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
@@ -104,7 +104,7 @@
         auto* c = b.builder.Constant(false);
         EXPECT_FALSE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-        c->ToString(str);
+        c->ToValue(str);
         EXPECT_EQ("false", str.str());
     }
 
@@ -113,7 +113,7 @@
         auto c = b.builder.Constant(true);
         EXPECT_TRUE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-        c->ToString(str);
+        c->ToValue(str);
         EXPECT_EQ("true", str.str());
 
         EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
diff --git a/src/tint/ir/construct.cc b/src/tint/ir/construct.cc
index 595075b..1507e8f 100644
--- a/src/tint/ir/construct.cc
+++ b/src/tint/ir/construct.cc
@@ -19,18 +19,14 @@
 
 namespace tint::ir {
 
-Construct::Construct(Value* result, utils::VectorRef<Value*> args) : Base(result, args) {}
+Construct::Construct(uint32_t id, const type::Type* type, utils::VectorRef<Value*> args)
+    : Base(id, type, args) {}
 
 Construct::~Construct() = default;
 
-utils::StringStream& Construct::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = construct(" << Result()->Type()->FriendlyName();
-    if (!Args().IsEmpty()) {
-        out << ", ";
-        EmitArgs(out);
-    }
-    out << ")";
+utils::StringStream& Construct::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = construct ";
+    EmitArgs(out);
     return out;
 }
 
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
index c9d1819..1cc0102 100644
--- a/src/tint/ir/construct.h
+++ b/src/tint/ir/construct.h
@@ -25,20 +25,21 @@
 class Construct : public utils::Castable<Construct, Call> {
   public:
     /// Constructor
-    /// @param result the result value
+    /// @param id the instruction id
+    /// @param type the result type
     /// @param args the constructor arguments
-    Construct(Value* result, utils::VectorRef<Value*> args);
-    Construct(const Construct& instr) = delete;
-    Construct(Construct&& instr) = delete;
+    Construct(uint32_t id, const type::Type* type, utils::VectorRef<Value*> args);
+    Construct(const Construct& inst) = delete;
+    Construct(Construct&& inst) = delete;
     ~Construct() override;
 
-    Construct& operator=(const Construct& instr) = delete;
-    Construct& operator=(Construct&& instr) = delete;
+    Construct& operator=(const Construct& inst) = delete;
+    Construct& operator=(Construct&& inst) = delete;
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/convert.cc b/src/tint/ir/convert.cc
index ed31250..7ffd6f5 100644
--- a/src/tint/ir/convert.cc
+++ b/src/tint/ir/convert.cc
@@ -19,17 +19,17 @@
 
 namespace tint::ir {
 
-Convert::Convert(Value* result, const type::Type* from, utils::VectorRef<Value*> args)
-    : Base(result, args), from_(from) {}
+Convert::Convert(uint32_t id,
+                 const type::Type* to_type,
+                 const type::Type* from_type,
+                 utils::VectorRef<Value*> args)
+    : Base(id, to_type, args), from_type_(from_type) {}
 
 Convert::~Convert() = default;
 
-utils::StringStream& Convert::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = convert(" << Result()->Type()->FriendlyName() << ", " << from_->FriendlyName()
-        << ", ";
+utils::StringStream& Convert::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = convert " << from_type_->FriendlyName() << ", ";
     EmitArgs(out);
-    out << ")";
     return out;
 }
 
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
index d157e03..c164a45 100644
--- a/src/tint/ir/convert.h
+++ b/src/tint/ir/convert.h
@@ -26,29 +26,33 @@
 class Convert : public utils::Castable<Convert, Call> {
   public:
     /// Constructor
-    /// @param result the result value
-    /// @param from the type being converted from
+    /// @param id the instruction id
+    /// @param result_type the result type
+    /// @param from_type the type being converted from
     /// @param args the conversion arguments
-    Convert(Value* result, const type::Type* from, utils::VectorRef<Value*> args);
-    Convert(const Convert& instr) = delete;
-    Convert(Convert&& instr) = delete;
+    Convert(uint32_t id,
+            const type::Type* result_type,
+            const type::Type* from_type,
+            utils::VectorRef<Value*> args);
+    Convert(const Convert& inst) = delete;
+    Convert(Convert&& inst) = delete;
     ~Convert() override;
 
-    Convert& operator=(const Convert& instr) = delete;
-    Convert& operator=(Convert&& instr) = delete;
+    Convert& operator=(const Convert& inst) = delete;
+    Convert& operator=(Convert&& inst) = delete;
 
     /// @returns the from type
-    const type::Type* From() const { return from_; }
+    const type::Type* FromType() const { return from_type_; }
     /// @returns the to type
-    const type::Type* To() const { return Result()->Type(); }
+    const type::Type* ToType() const { return Type(); }
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
-    const type::Type* from_ = nullptr;
+    const type::Type* from_type_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/converter.cc b/src/tint/ir/converter.cc
new file mode 100644
index 0000000..e89be3a
--- /dev/null
+++ b/src/tint/ir/converter.cc
@@ -0,0 +1,42 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/converter.h"
+
+#include "src/tint/ir/builder_impl.h"
+#include "src/tint/program.h"
+
+namespace tint::ir {
+
+// static
+Converter::Result Converter::FromProgram(const Program* program) {
+    if (!program->IsValid()) {
+        return Result{std::string("input program is not valid")};
+    }
+
+    BuilderImpl b(program);
+    auto r = b.Build();
+    if (!r) {
+        return b.Diagnostics().str();
+    }
+
+    return Result{r.Move()};
+}
+
+// static
+const Program* Converter::ToProgram() {
+    return nullptr;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/converter.h b/src/tint/ir/converter.h
new file mode 100644
index 0000000..8defd83
--- /dev/null
+++ b/src/tint/ir/converter.h
@@ -0,0 +1,54 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_IR_CONVERTER_H_
+#define SRC_TINT_IR_CONVERTER_H_
+
+#include <string>
+
+#include "src/tint/ir/module.h"
+#include "src/tint/utils/result.h"
+
+// Forward Declarations
+namespace tint {
+class Program;
+}  // namespace tint
+
+namespace tint::ir {
+
+/// Class for converting into and out of IR.
+class Converter {
+  public:
+    /// The result type for the FromProgram method.
+    using Result = utils::Result<Module, std::string>;
+
+    /// Builds an ir::Module from the given Program
+    /// @param program the Program to use.
+    /// @returns the `utiils::Result` of generating the IR. The result will contain the `ir::Module`
+    /// on success, otherwise the `std::string` error.
+    ///
+    /// @note this assumes the program |IsValid|, and has had const-eval done so
+    /// any abstract values have been calculated and converted into the relevant
+    /// concrete types.
+    static Result FromProgram(const Program* program);
+
+    /// Converts the module back to a Program
+    /// @returns the resulting program, or nullptr on error
+    ///  (Note, this will probably turn into a utils::Result, just stubbing for now)
+    static const Program* ToProgram();
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_CONVERTER_H_
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index c3bb006..2b8de0b 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -20,6 +20,7 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
 #include "src/tint/switch.h"
+#include "src/tint/utils/scoped_assignment.h"
 
 namespace tint::ir {
 namespace {
@@ -62,9 +63,9 @@
 }
 
 void Disassembler::EmitBlockInstructions(const Block* b) {
-    for (const auto* instr : b->instructions) {
+    for (const auto* inst : b->instructions) {
         Indent();
-        instr->ToString(out_) << std::endl;
+        inst->ToInstruction(out_) << std::endl;
     }
 }
 
@@ -89,7 +90,9 @@
     tint::Switch(
         node,
         [&](const ir::Function* f) {
-            Indent() << "%bb" << GetIdForNode(f) << " = Function " << f->name.Name() << std::endl;
+            TINT_SCOPED_ASSIGNMENT(in_function_, true);
+
+            Indent() << "%fn" << GetIdForNode(f) << " = func " << f->name.Name() << std::endl;
 
             {
                 ScopedIndent func_indent(&indent_size_);
@@ -101,27 +104,28 @@
         [&](const ir::Block* b) {
             // If this block is dead, nothing to do
             if (b->IsDead()) {
-                Indent() << "# Dead" << std::endl;
                 return;
             }
 
-            Indent() << "%bb" << GetIdForNode(b) << " = Block" << std::endl;
+            Indent() << "%fn" << GetIdForNode(b) << " = block" << std::endl;
             EmitBlockInstructions(b);
 
             if (b->branch.target->Is<Terminator>()) {
-                Indent() << "Return";
+                Indent() << "ret";
             } else {
-                Indent() << "BranchTo "
-                         << "%bb" << GetIdForNode(b->branch.target);
+                Indent() << "branch "
+                         << "%fn" << GetIdForNode(b->branch.target);
             }
-            out_ << " (";
-            for (const auto* v : b->branch.args) {
-                if (v != b->branch.args.Front()) {
-                    out_ << ", ";
+            if (!b->branch.args.IsEmpty()) {
+                out_ << " ";
+                for (const auto* v : b->branch.args) {
+                    if (v != b->branch.args.Front()) {
+                        out_ << ", ";
+                    }
+                    v->ToValue(out_);
                 }
-                v->ToString(out_);
             }
-            out_ << ")" << std::endl;
+            out_ << std::endl;
 
             if (!b->branch.target->Is<Terminator>()) {
                 out_ << std::endl;
@@ -130,15 +134,37 @@
             Walk(b->branch.target);
         },
         [&](const ir::Switch* s) {
-            Indent() << "%bb" << GetIdForNode(s) << " = Switch (";
-            s->condition->ToString(out_);
-            out_ << ")" << std::endl;
+            Indent() << "%fn" << GetIdForNode(s) << " = switch ";
+            s->condition->ToValue(out_);
+            out_ << " [";
+            for (const auto& c : s->cases) {
+                if (&c != &s->cases.Front()) {
+                    out_ << ", ";
+                }
+                out_ << "c: (";
+                for (const auto& selector : c.selectors) {
+                    if (&selector != &c.selectors.Front()) {
+                        out_ << " ";
+                    }
+
+                    if (selector.IsDefault()) {
+                        out_ << "default";
+                    } else {
+                        selector.val->ToValue(out_);
+                    }
+                }
+                out_ << ", %fn" << GetIdForNode(c.start.target) << ")";
+            }
+            if (s->merge.target->IsConnected()) {
+                out_ << ", m: %fn" << GetIdForNode(s->merge.target);
+            }
+            out_ << "]" << std::endl;
 
             {
                 ScopedIndent switch_indent(&indent_size_);
                 ScopedStopNode scope(&stop_nodes_, s->merge.target);
                 for (const auto& c : s->cases) {
-                    Indent() << "# Case ";
+                    Indent() << "# case ";
                     for (const auto& selector : c.selectors) {
                         if (&selector != &c.selectors.Front()) {
                             out_ << " ";
@@ -147,7 +173,7 @@
                         if (selector.IsDefault()) {
                             out_ << "default";
                         } else {
-                            selector.val->ToString(out_);
+                            selector.val->ToValue(out_);
                         }
                     }
                     out_ << std::endl;
@@ -155,13 +181,20 @@
                 }
             }
 
-            Indent() << "# Switch Merge" << std::endl;
-            Walk(s->merge.target);
+            if (s->merge.target->IsConnected()) {
+                Indent() << "# switch merge" << std::endl;
+                Walk(s->merge.target);
+            }
         },
         [&](const ir::If* i) {
-            Indent() << "%bb" << GetIdForNode(i) << " = if (";
-            i->condition->ToString(out_);
-            out_ << ")" << std::endl;
+            Indent() << "%fn" << GetIdForNode(i) << " = if ";
+            i->condition->ToValue(out_);
+            out_ << " [t: %fn" << GetIdForNode(i->true_.target) << ", f: %fn"
+                 << GetIdForNode(i->false_.target);
+            if (i->merge.target->IsConnected()) {
+                out_ << ", m: %fn" << GetIdForNode(i->merge.target);
+            }
+            out_ << "]" << std::endl;
 
             {
                 ScopedIndent if_indent(&indent_size_);
@@ -174,13 +207,23 @@
                 Walk(i->false_.target);
             }
 
-            if (!i->merge.target->IsDisconnected()) {
+            if (i->merge.target->IsConnected()) {
                 Indent() << "# if merge" << std::endl;
                 Walk(i->merge.target);
             }
         },
         [&](const ir::Loop* l) {
-            Indent() << "%bb" << GetIdForNode(l) << " = loop" << std::endl;
+            Indent() << "%fn" << GetIdForNode(l) << " = loop [s: %fn"
+                     << GetIdForNode(l->start.target);
+
+            if (l->continuing.target->IsConnected()) {
+                out_ << ", c: %fn" << GetIdForNode(l->continuing.target);
+            }
+            if (l->merge.target->IsConnected()) {
+                out_ << ", m: %fn" << GetIdForNode(l->merge.target);
+            }
+            out_ << "]" << std::endl;
+
             {
                 ScopedStopNode loop_scope(&stop_nodes_, l->merge.target);
                 ScopedIndent loop_indent(&indent_size_);
@@ -190,18 +233,30 @@
                     Walk(l->start.target);
                 }
 
-                Indent() << "# loop continuing" << std::endl;
-                Walk(l->continuing.target);
+                if (l->continuing.target->IsConnected()) {
+                    Indent() << "# loop continuing" << std::endl;
+                    Walk(l->continuing.target);
+                }
             }
 
-            Indent() << "# loop merge" << std::endl;
-            Walk(l->merge.target);
+            if (l->merge.target->IsConnected()) {
+                Indent() << "# loop merge" << std::endl;
+                Walk(l->merge.target);
+            }
         },
-        [&](const ir::Terminator*) { Indent() << "FunctionEnd" << std::endl
-                                              << std::endl; });
+        [&](const ir::Terminator*) {
+            if (in_function_) {
+                Indent() << "func_end" << std::endl;
+            }
+            out_ << std::endl;
+        });
 }
 
 std::string Disassembler::Disassemble() {
+    if (mod_.root_block) {
+        Walk(mod_.root_block);
+    }
+
     for (const auto* func : mod_.functions) {
         Walk(func);
     }
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index eee6b76..04b3997 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -56,6 +56,7 @@
     std::unordered_map<const FlowNode*, size_t> flow_node_to_id_;
     size_t next_node_id_ = 0;
     uint32_t indent_size_ = 0;
+    bool in_function_ = false;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/discard.cc b/src/tint/ir/discard.cc
index 1d179b5..bc7bdc3 100644
--- a/src/tint/ir/discard.cc
+++ b/src/tint/ir/discard.cc
@@ -19,13 +19,12 @@
 
 namespace tint::ir {
 
-Discard::Discard(Value* result) : Base(result) {}
+Discard::Discard() : Base() {}
 
 Discard::~Discard() = default;
 
-utils::StringStream& Discard::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = discard";
+utils::StringStream& Discard::ToInstruction(utils::StringStream& out) const {
+    out << "discard";
     return out;
 }
 
diff --git a/src/tint/ir/discard.h b/src/tint/ir/discard.h
index b27dc9d..456c8d5 100644
--- a/src/tint/ir/discard.h
+++ b/src/tint/ir/discard.h
@@ -15,29 +15,29 @@
 #ifndef SRC_TINT_IR_DISCARD_H_
 #define SRC_TINT_IR_DISCARD_H_
 
-#include "src/tint/ir/instruction.h"
+#include "src/tint/debug.h"
+#include "src/tint/ir/call.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A discard instruction in the IR.
-class Discard : public utils::Castable<Discard, Instruction> {
+class Discard : public utils::Castable<Discard, Call> {
   public:
     /// Constructor
-    /// @param result the result id
-    explicit Discard(Value* result);
-    Discard(const Discard& instr) = delete;
-    Discard(Discard&& instr) = delete;
+    Discard();
+    Discard(const Discard& inst) = delete;
+    Discard(Discard&& inst) = delete;
     ~Discard() override;
 
-    Discard& operator=(const Discard& instr) = delete;
-    Discard& operator=(Discard&& instr) = delete;
+    Discard& operator=(const Discard& inst) = delete;
+    Discard& operator=(Discard&& inst) = delete;
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/discard_test.cc b/src/tint/ir/discard_test.cc
index 0f2fb5d..32b5c1e 100644
--- a/src/tint/ir/discard_test.cc
+++ b/src/tint/ir/discard_test.cc
@@ -24,17 +24,12 @@
 TEST_F(IR_InstructionTest, Discard) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr = b.builder.Discard();
-
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-    ASSERT_NE(instr->Result()->Type(), nullptr);
-    ASSERT_NE(instr->Result()->Type()->As<type::Void>(), nullptr);
+    const auto* inst = b.builder.Discard();
+    ASSERT_TRUE(inst->Is<ir::Discard>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (void) = discard");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "discard");
 }
 
 }  // namespace
diff --git a/src/tint/ir/flow_node.h b/src/tint/ir/flow_node.h
index 2a91674..905f077 100644
--- a/src/tint/ir/flow_node.h
+++ b/src/tint/ir/flow_node.h
@@ -32,8 +32,11 @@
     ///   - Node is a continue target outside control flow (loop that returns)
     utils::Vector<FlowNode*, 2> inbound_branches;
 
-    /// @returns true if this node has no inbound branches
-    bool IsDisconnected() const { return inbound_branches.IsEmpty(); }
+    /// @returns true if this node has inbound branches and branches out
+    bool IsConnected() const { return !IsDead() && !inbound_branches.IsEmpty(); }
+
+    /// @returns true if the node does not branch out
+    virtual bool IsDead() const { return false; }
 
   protected:
     /// Constructor
diff --git a/src/tint/ir/instruction.cc b/src/tint/ir/instruction.cc
index 46644dd..3e3192f 100644
--- a/src/tint/ir/instruction.cc
+++ b/src/tint/ir/instruction.cc
@@ -18,10 +18,9 @@
 
 namespace tint::ir {
 
-Instruction::Instruction(Value* result) : result_(result) {
-    TINT_ASSERT(IR, result_);
-    result_->AddUsage(this);
-}
+Instruction::Instruction() = default;
+
+Instruction::Instruction(uint32_t id, const type::Type* ty) : id_(id), type_(ty) {}
 
 Instruction::~Instruction() = default;
 
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index df965e4..f2dac09 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -22,31 +22,46 @@
 namespace tint::ir {
 
 /// An instruction in the IR.
-class Instruction : public utils::Castable<Instruction> {
+class Instruction : public utils::Castable<Instruction, Value> {
   public:
-    Instruction(const Instruction& instr) = delete;
-    Instruction(Instruction&& instr) = delete;
+    Instruction(const Instruction& inst) = delete;
+    Instruction(Instruction&& inst) = delete;
     /// Destructor
     ~Instruction() override;
 
-    Instruction& operator=(const Instruction& instr) = delete;
-    Instruction& operator=(Instruction&& instr) = delete;
+    Instruction& operator=(const Instruction& inst) = delete;
+    Instruction& operator=(Instruction&& inst) = delete;
 
-    /// @returns the result value for the instruction
-    Value* Result() const { return result_; }
+    /// @returns the type of the value
+    const type::Type* Type() const override { return type_; }
+
+    /// Write the value to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToValue(utils::StringStream& out) const override {
+        out << "%" << std::to_string(id_);
+        if (type_ != nullptr) {
+            out << "(" << Type()->FriendlyName() << ")";
+        }
+        return out;
+    }
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    virtual utils::StringStream& ToString(utils::StringStream& out) const = 0;
+    virtual utils::StringStream& ToInstruction(utils::StringStream& out) const = 0;
 
   protected:
     /// Constructor
-    /// @param result the result value
-    explicit Instruction(Value* result);
+    Instruction();
+    /// Constructor
+    /// @param id the instruction id
+    /// @param type the result type
+    Instruction(uint32_t id, const type::Type* type);
 
   private:
-    Value* result_ = nullptr;
+    uint32_t id_ = 0;
+    const type::Type* type_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/module.cc b/src/tint/ir/module.cc
index e434888..133d364 100644
--- a/src/tint/ir/module.cc
+++ b/src/tint/ir/module.cc
@@ -14,26 +14,8 @@
 
 #include "src/tint/ir/module.h"
 
-#include "src/tint/ir/builder_impl.h"
-#include "src/tint/program.h"
-
 namespace tint::ir {
 
-// static
-Module::Result Module::FromProgram(const Program* program) {
-    if (!program->IsValid()) {
-        return Result{std::string("input program is not valid")};
-    }
-
-    BuilderImpl b(program);
-    auto r = b.Build();
-    if (!r) {
-        return b.Diagnostics().str();
-    }
-
-    return Result{r.Move()};
-}
-
 Module::Module() = default;
 
 Module::Module(Module&&) = default;
@@ -42,8 +24,4 @@
 
 Module& Module::operator=(Module&&) = default;
 
-const Program* Module::ToProgram() const {
-    return nullptr;
-}
-
 }  // namespace tint::ir
diff --git a/src/tint/ir/module.h b/src/tint/ir/module.h
index 5cfc883..ebf9209 100644
--- a/src/tint/ir/module.h
+++ b/src/tint/ir/module.h
@@ -28,29 +28,11 @@
 #include "src/tint/utils/result.h"
 #include "src/tint/utils/vector.h"
 
-// Forward Declarations
-namespace tint {
-class Program;
-}  // namespace tint
-
 namespace tint::ir {
 
 /// Main module class for the IR.
 class Module {
   public:
-    /// The result type for the FromProgram method.
-    using Result = utils::Result<Module, std::string>;
-
-    /// Builds an ir::Module from the given Program
-    /// @param program the Program to use.
-    /// @returns the `utiils::Result` of generating the IR. The result will contain the `ir::Module`
-    /// on success, otherwise the `std::string` error.
-    ///
-    /// @note this assumes the program |IsValid|, and has had const-eval done so
-    /// any abstract values have been calculated and converted into the relevant
-    /// concrete types.
-    static Result FromProgram(const Program* program);
-
     /// Constructor
     Module();
     /// Move constructor
@@ -64,11 +46,6 @@
     /// @returns a reference to this module
     Module& operator=(Module&& o);
 
-    /// Converts the module back to a Program
-    /// @returns the resulting program, or nullptr on error
-    ///  (Note, this will probably turn into a utils::Result, just stubbing for now)
-    const Program* ToProgram() const;
-
   private:
     /// Program Id required to create other components
     ProgramID prog_id_;
@@ -88,6 +65,9 @@
     /// List of indexes into the functions list for the entry points
     utils::Vector<Function*, 8> entry_points;
 
+    /// The block containing module level declarations, if any exist.
+    Block* root_block = nullptr;
+
     /// The type manager for the module
     type::Manager types;
 
diff --git a/src/tint/ir/runtime.cc b/src/tint/ir/runtime.cc
deleted file mode 100644
index a1f485d..0000000
--- a/src/tint/ir/runtime.cc
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/runtime.h"
-
-#include <string>
-
-TINT_INSTANTIATE_TYPEINFO(tint::ir::Runtime);
-
-namespace tint::ir {
-
-Runtime::Runtime(const type::Type* type, Id id) : type_(type), id_(id) {}
-
-Runtime::~Runtime() = default;
-
-utils::StringStream& Runtime::ToString(utils::StringStream& out) const {
-    out << "%" << std::to_string(AsId()) << " (" << type_->FriendlyName() << ")";
-    return out;
-}
-
-}  // namespace tint::ir
diff --git a/src/tint/ir/runtime.h b/src/tint/ir/runtime.h
deleted file mode 100644
index 6e2558c..0000000
--- a/src/tint/ir/runtime.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_IR_RUNTIME_H_
-#define SRC_TINT_IR_RUNTIME_H_
-
-#include "src/tint/ir/value.h"
-#include "src/tint/utils/string_stream.h"
-
-namespace tint::ir {
-
-/// Runtime value in the IR.
-class Runtime : public utils::Castable<Runtime, Value> {
-  public:
-    /// A value id.
-    using Id = uint32_t;
-
-    /// Constructor
-    /// @param type the type of the value
-    /// @param id the id for the value
-    Runtime(const type::Type* type, Id id);
-
-    /// Destructor
-    ~Runtime() override;
-
-    Runtime(const Runtime&) = delete;
-    Runtime(Runtime&&) = delete;
-
-    Runtime& operator=(const Runtime&) = delete;
-    Runtime& operator=(Runtime&&) = delete;
-
-    /// @returns the value data as an `Id`.
-    Id AsId() const { return id_; }
-
-    /// @returns the type of the value
-    const type::Type* Type() const override { return type_; }
-
-    /// Write the id to the given stream
-    /// @param out the stream to write to
-    /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
-
-  private:
-    const type::Type* type_ = nullptr;
-    Id id_ = 0;
-};
-
-}  // namespace tint::ir
-
-#endif  // SRC_TINT_IR_RUNTIME_H_
diff --git a/src/tint/ir/runtime_test.cc b/src/tint/ir/runtime_test.cc
deleted file mode 100644
index 7959909..0000000
--- a/src/tint/ir/runtime_test.cc
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/runtime.h"
-#include "src/tint/ir/test_helper.h"
-#include "src/tint/utils/string_stream.h"
-
-namespace tint::ir {
-namespace {
-
-using namespace tint::number_suffixes;  // NOLINT
-
-using IR_RuntimeTest = TestHelper;
-
-TEST_F(IR_RuntimeTest, id) {
-    auto& b = CreateEmptyBuilder();
-
-    utils::StringStream str;
-
-    b.builder.next_runtime_id = Runtime::Id(4);
-    auto* val = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
-    EXPECT_EQ(4u, val->AsId());
-
-    val->ToString(str);
-    EXPECT_EQ("%4 (i32)", str.str());
-}
-
-}  // namespace
-}  // namespace tint::ir
diff --git a/src/tint/ir/store.cc b/src/tint/ir/store.cc
index 32162c3..8d35214 100644
--- a/src/tint/ir/store.cc
+++ b/src/tint/ir/store.cc
@@ -19,17 +19,19 @@
 
 namespace tint::ir {
 
-Store::Store(Value* to, Value* from) : Base(to), from_(from) {
+Store::Store(Value* to, Value* from) : Base(), to_(to), from_(from) {
+    TINT_ASSERT(IR, to_);
     TINT_ASSERT(IR, from_);
+    to_->AddUsage(this);
     from_->AddUsage(this);
 }
 
 Store::~Store() = default;
 
-utils::StringStream& Store::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = ";
-    from_->ToString(out);
+utils::StringStream& Store::ToInstruction(utils::StringStream& out) const {
+    out << "store ";
+    to_->ToValue(out) << ", ";
+    from_->ToValue(out);
     return out;
 }
 
diff --git a/src/tint/ir/store.h b/src/tint/ir/store.h
index 57544fc..ea5550d 100644
--- a/src/tint/ir/store.h
+++ b/src/tint/ir/store.h
@@ -28,22 +28,25 @@
     /// @param to the value to store too
     /// @param from the value being stored from
     Store(Value* to, Value* from);
-    Store(const Store& instr) = delete;
-    Store(Store&& instr) = delete;
+    Store(const Store& inst) = delete;
+    Store(Store&& inst) = delete;
     ~Store() override;
 
-    Store& operator=(const Store& instr) = delete;
-    Store& operator=(Store&& instr) = delete;
+    Store& operator=(const Store& inst) = delete;
+    Store& operator=(Store&& inst) = delete;
 
+    /// @returns the value being stored too
+    const Value* to() const { return to_; }
     /// @returns the value being stored
     const Value* from() const { return from_; }
 
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
+    Value* to_ = nullptr;
     Value* from_ = nullptr;
 };
 
diff --git a/src/tint/ir/store_test.cc b/src/tint/ir/store_test.cc
index e0632dd..82347dc 100644
--- a/src/tint/ir/store_test.cc
+++ b/src/tint/ir/store_test.cc
@@ -26,39 +26,37 @@
 TEST_F(IR_InstructionTest, CreateStore) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
+    // TODO(dsinclair): This is wrong, but we don't have anything correct to store too at the
+    // moment.
+    auto* to = b.builder.Discard();
+    const auto* inst = b.builder.Store(to, b.builder.Constant(4_i));
 
-    auto* rt = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
-    const auto* instr = b.builder.Store(rt, b.builder.Constant(4_i));
+    ASSERT_TRUE(inst->Is<Store>());
+    ASSERT_EQ(inst->to(), to);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    ASSERT_NE(instr->Result()->Type(), nullptr);
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->from()->Is<Constant>());
-    auto lhs = instr->from()->As<Constant>()->value;
+    ASSERT_TRUE(inst->from()->Is<Constant>());
+    auto lhs = inst->from()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = 4");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "store %0, 4i");
 }
 
 TEST_F(IR_InstructionTest, Store_Usage) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
-    auto* rt = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
-    const auto* instr = b.builder.Store(rt, b.builder.Constant(4_i));
+    auto* to = b.builder.Discard();
+    const auto* inst = b.builder.Store(to, b.builder.Constant(4_i));
 
-    ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Result()->Usage()[0], instr);
+    ASSERT_NE(inst->to(), nullptr);
+    ASSERT_EQ(inst->to()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->to()->Usage()[0], inst);
 
-    ASSERT_NE(instr->from(), nullptr);
-    ASSERT_EQ(instr->from()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->from()->Usage()[0], instr);
+    ASSERT_NE(inst->from(), nullptr);
+    ASSERT_EQ(inst->from()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->from()->Usage()[0], inst);
 }
 
 }  // namespace
diff --git a/src/tint/ir/unary.cc b/src/tint/ir/unary.cc
index 532efcc..f3c4f24 100644
--- a/src/tint/ir/unary.cc
+++ b/src/tint/ir/unary.cc
@@ -19,34 +19,35 @@
 
 namespace tint::ir {
 
-Unary::Unary(Kind kind, Value* result, Value* val) : Base(result), kind_(kind), val_(val) {
+Unary::Unary(uint32_t id, Kind kind, const type::Type* type, Value* val)
+    : Base(id, type), kind_(kind), val_(val) {
     TINT_ASSERT(IR, val_);
     val_->AddUsage(this);
 }
 
 Unary::~Unary() = default;
 
-utils::StringStream& Unary::ToString(utils::StringStream& out) const {
-    Result()->ToString(out) << " = ";
+utils::StringStream& Unary::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = ";
     switch (GetKind()) {
         case Unary::Kind::kAddressOf:
-            out << "&";
+            out << "addr_of";
             break;
         case Unary::Kind::kComplement:
-            out << "~";
+            out << "bit_complement";
             break;
         case Unary::Kind::kIndirection:
-            out << "*";
+            out << "indirection";
             break;
         case Unary::Kind::kNegation:
-            out << "-";
+            out << "negation";
             break;
         case Unary::Kind::kNot:
-            out << "!";
+            out << "log_not";
             break;
     }
-    val_->ToString(out);
-
+    out << " ";
+    val_->ToValue(out);
     return out;
 }
 
diff --git a/src/tint/ir/unary.h b/src/tint/ir/unary.h
index 0337b4f..ce3e5d7 100644
--- a/src/tint/ir/unary.h
+++ b/src/tint/ir/unary.h
@@ -34,16 +34,17 @@
     };
 
     /// Constructor
+    /// @param id the instruction id
     /// @param kind the kind of unary instruction
-    /// @param result the result value
+    /// @param type the result type
     /// @param val the lhs of the instruction
-    Unary(Kind kind, Value* result, Value* val);
-    Unary(const Unary& instr) = delete;
-    Unary(Unary&& instr) = delete;
+    Unary(uint32_t id, Kind kind, const type::Type* type, Value* val);
+    Unary(const Unary& inst) = delete;
+    Unary(Unary&& inst) = delete;
     ~Unary() override;
 
-    Unary& operator=(const Unary& instr) = delete;
-    Unary& operator=(Unary&& instr) = delete;
+    Unary& operator=(const Unary& inst) = delete;
+    Unary& operator=(Unary&& inst) = delete;
 
     /// @returns the kind of instruction
     Kind GetKind() const { return kind_; }
@@ -54,7 +55,7 @@
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
     Kind kind_;
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
index cfc5579..ff247a2 100644
--- a/src/tint/ir/unary_test.cc
+++ b/src/tint/ir/unary_test.cc
@@ -26,135 +26,112 @@
 TEST_F(IR_InstructionTest, CreateAddressOf) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
     // TODO(dsinclair): This would be better as an identifier, but works for now.
-    const auto* instr =
+    const auto* inst =
         b.builder.AddressOf(b.builder.ir.types.Get<type::Pointer>(
                                 b.builder.ir.types.Get<type::I32>(),
                                 builtin::AddressSpace::kPrivate, builtin::Access::kReadWrite),
                             b.builder.Constant(4_i));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kAddressOf);
+    ASSERT_TRUE(inst->Is<Unary>());
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kAddressOf);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    ASSERT_NE(instr->Result()->Type(), nullptr);
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+    ASSERT_NE(inst->Type(), nullptr);
 
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(inst->Val()->Is<Constant>());
+    auto lhs = inst->Val()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (ptr<private, i32, read_write>) = &4");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(ptr<private, i32, read_write>) = addr_of 4i");
 }
 
 TEST_F(IR_InstructionTest, CreateComplement) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Complement(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kComplement);
+    ASSERT_TRUE(inst->Is<Unary>());
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kComplement);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(inst->Val()->Is<Constant>());
+    auto lhs = inst->Val()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = ~4");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = bit_complement 4i");
 }
 
 TEST_F(IR_InstructionTest, CreateIndirection) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_runtime_id = Runtime::Id(42);
     // TODO(dsinclair): This would be better as an identifier, but works for now.
-    const auto* instr =
+    const auto* inst =
         b.builder.Indirection(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kIndirection);
+    ASSERT_TRUE(inst->Is<Unary>());
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kIndirection);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(inst->Val()->Is<Constant>());
+    auto lhs = inst->Val()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = *4");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = indirection 4i");
 }
 
 TEST_F(IR_InstructionTest, CreateNegation) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNegation);
+    ASSERT_TRUE(inst->Is<Unary>());
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kNegation);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(inst->Val()->Is<Constant>());
+    auto lhs = inst->Val()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (i32) = -4");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(i32) = negation 4i");
 }
 
 TEST_F(IR_InstructionTest, CreateNot) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Not(b.builder.ir.types.Get<type::Bool>(), b.builder.Constant(true));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNot);
+    ASSERT_TRUE(inst->Is<Unary>());
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kNot);
 
-    ASSERT_TRUE(instr->Result()->Is<Runtime>());
-    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
-
-    ASSERT_TRUE(instr->Val()->Is<Constant>());
-    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(inst->Val()->Is<Constant>());
+    auto lhs = inst->Val()->As<Constant>()->value;
     ASSERT_TRUE(lhs->Is<constant::Scalar<bool>>());
     EXPECT_TRUE(lhs->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
     utils::StringStream str;
-    instr->ToString(str);
-    EXPECT_EQ(str.str(), "%42 (bool) = !true");
+    inst->ToInstruction(str);
+    EXPECT_EQ(str.str(), "%1(bool) = log_not true");
 }
 
 TEST_F(IR_InstructionTest, Unary_Usage) {
     auto& b = CreateEmptyBuilder();
-
-    b.builder.next_runtime_id = Runtime::Id(42);
-    const auto* instr =
+    const auto* inst =
         b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNegation);
+    EXPECT_EQ(inst->GetKind(), Unary::Kind::kNegation);
 
-    ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Result()->Usage()[0], instr);
-
-    ASSERT_NE(instr->Val(), nullptr);
-    ASSERT_EQ(instr->Val()->Usage().Length(), 1u);
-    EXPECT_EQ(instr->Val()->Usage()[0], instr);
+    ASSERT_NE(inst->Val(), nullptr);
+    ASSERT_EQ(inst->Val()->Usage().Length(), 1u);
+    EXPECT_EQ(inst->Val()->Usage()[0], inst);
 }
 
 }  // namespace
diff --git a/src/tint/ir/user_call.cc b/src/tint/ir/user_call.cc
index 9a1f7eb..f23a2ca 100644
--- a/src/tint/ir/user_call.cc
+++ b/src/tint/ir/user_call.cc
@@ -19,17 +19,17 @@
 
 namespace tint::ir {
 
-UserCall::UserCall(Value* result, Symbol name, utils::VectorRef<Value*> args)
-    : Base(result, args), name_(name) {}
+UserCall::UserCall(uint32_t id, const type::Type* type, Symbol name, utils::VectorRef<Value*> args)
+    : Base(id, type, args), name_(name) {}
 
 UserCall::~UserCall() = default;
 
-utils::StringStream& UserCall::ToString(utils::StringStream& out) const {
-    Result()->ToString(out);
-    out << " = call(";
-    out << name_.Name() << ", ";
+utils::StringStream& UserCall::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = call " << name_.Name();
+    if (Args().Length() > 0) {
+        out << ", ";
+    }
     EmitArgs(out);
-    out << ")";
     return out;
 }
 
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
index 0662455..1e2b070 100644
--- a/src/tint/ir/user_call.h
+++ b/src/tint/ir/user_call.h
@@ -26,16 +26,17 @@
 class UserCall : public utils::Castable<UserCall, Call> {
   public:
     /// Constructor
-    /// @param result the result value
+    /// @param id the instruction id
+    /// @param type the result type
     /// @param name the function name
     /// @param args the function arguments
-    UserCall(Value* result, Symbol name, utils::VectorRef<Value*> args);
-    UserCall(const UserCall& instr) = delete;
-    UserCall(UserCall&& instr) = delete;
+    UserCall(uint32_t id, const type::Type* type, Symbol name, utils::VectorRef<Value*> args);
+    UserCall(const UserCall& inst) = delete;
+    UserCall(UserCall&& inst) = delete;
     ~UserCall() override;
 
-    UserCall& operator=(const UserCall& instr) = delete;
-    UserCall& operator=(UserCall&& instr) = delete;
+    UserCall& operator=(const UserCall& inst) = delete;
+    UserCall& operator=(UserCall&& inst) = delete;
 
     /// @returns the function name
     Symbol Name() const { return name_; }
@@ -43,7 +44,7 @@
     /// Write the instruction to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    utils::StringStream& ToString(utils::StringStream& out) const override;
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
 
   private:
     Symbol name_{};
diff --git a/src/tint/ir/value.cc b/src/tint/ir/value.cc
index 750fe19..c735cd9 100644
--- a/src/tint/ir/value.cc
+++ b/src/tint/ir/value.cc
@@ -15,7 +15,6 @@
 #include "src/tint/ir/value.h"
 
 #include "src/tint/ir/constant.h"
-#include "src/tint/ir/runtime.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Value);
 
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index e2a642b..7f3f4b7 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -40,8 +40,8 @@
     Value& operator=(Value&&) = delete;
 
     /// Adds an instruction which uses this value.
-    /// @param instr the instruction
-    void AddUsage(const Instruction* instr) { uses_.Add(instr); }
+    /// @param inst the instruction
+    void AddUsage(const Instruction* inst) { uses_.Add(inst); }
 
     /// @returns the vector of instructions which use this value. An instruction will only be
     /// returned once even if that instruction uses the given value multiple times.
@@ -53,7 +53,7 @@
     /// Write the value to the given stream
     /// @param out the stream to write to
     /// @returns the stream
-    virtual utils::StringStream& ToString(utils::StringStream& out) const = 0;
+    virtual utils::StringStream& ToValue(utils::StringStream& out) const = 0;
 
   protected:
     /// Constructor
diff --git a/src/tint/ir/var.cc b/src/tint/ir/var.cc
new file mode 100644
index 0000000..740a35e
--- /dev/null
+++ b/src/tint/ir/var.cc
@@ -0,0 +1,35 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/var.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Var);
+
+namespace tint::ir {
+
+Var::Var(uint32_t id,
+         const type::Type* ty,
+         builtin::AddressSpace address_space,
+         builtin::Access access)
+    : Base(id, ty), address_space_(address_space), access_(access) {}
+
+Var::~Var() = default;
+
+utils::StringStream& Var::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = var " << address_space_ << " " << access_;
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/var.h b/src/tint/ir/var.h
new file mode 100644
index 0000000..456e7d3
--- /dev/null
+++ b/src/tint/ir/var.h
@@ -0,0 +1,63 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_IR_VAR_H_
+#define SRC_TINT_IR_VAR_H_
+
+#include "src/tint/builtin/access.h"
+#include "src/tint/builtin/address_space.h"
+#include "src/tint/ir/instruction.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class Var : public utils::Castable<Var, Instruction> {
+  public:
+    /// Constructor
+    /// @param id the instruction id
+    /// @param type the type
+    /// @param address_space the address space of the var
+    /// @param access the access mode of the var
+    Var(uint32_t id,
+        const type::Type* type,
+        builtin::AddressSpace address_space,
+        builtin::Access access);
+    Var(const Var& inst) = delete;
+    Var(Var&& inst) = delete;
+    ~Var() override;
+
+    Var& operator=(const Var& inst) = delete;
+    Var& operator=(Var&& inst) = delete;
+
+    /// @returns the address space
+    builtin::AddressSpace AddressSpace() const { return address_space_; }
+
+    /// @returns the access mode
+    builtin::Access Access() const { return access_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
+
+  private:
+    builtin::AddressSpace address_space_;
+    builtin::Access access_;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_VAR_H_
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index b4f5eea..49b14fc 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -1470,7 +1470,7 @@
 
     /// @param name the symbol string
     /// @return a Symbol with the given name
-    Symbol Sym(const std::string& name) { return Symbols().Register(name); }
+    Symbol Sym(std::string_view name) { return Symbols().Register(name); }
 
     /// @param enumerator the enumerator
     /// @return a Symbol with the given enum value
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 098ab17..b9160ce 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -131,6 +131,43 @@
     return {};
 }
 
+static std::string name(AttributeKind kind) {
+    switch (kind) {
+        case AttributeKind::kAlign:
+            return "@align";
+        case AttributeKind::kBinding:
+            return "@binding";
+        case AttributeKind::kBuiltin:
+            return "@builtin";
+        case AttributeKind::kDiagnostic:
+            return "@diagnostic";
+        case AttributeKind::kGroup:
+            return "@group";
+        case AttributeKind::kId:
+            return "@id";
+        case AttributeKind::kInterpolate:
+            return "@interpolate";
+        case AttributeKind::kInvariant:
+            return "@invariant";
+        case AttributeKind::kLocation:
+            return "@location";
+        case AttributeKind::kOffset:
+            return "@offset";
+        case AttributeKind::kMustUse:
+            return "@must_use";
+        case AttributeKind::kSize:
+            return "@size";
+        case AttributeKind::kStage:
+            return "@stage";
+        case AttributeKind::kStride:
+            return "@stride";
+        case AttributeKind::kWorkgroup:
+            return "@workgroup_size";
+        case AttributeKind::kBindingAndGroup:
+            return "@binding";
+    }
+    return "<unknown>";
+}
 namespace FunctionInputAndOutputTests {
 using FunctionParameterAttributeTest = TestWithParams;
 TEST_P(FunctionParameterAttributeTest, IsValid) {
@@ -144,11 +181,16 @@
 
     if (params.should_pass) {
         EXPECT_TRUE(r()->Resolve()) << r()->error();
+    } else if (params.kind == AttributeKind::kLocation || params.kind == AttributeKind::kBuiltin ||
+               params.kind == AttributeKind::kInvariant ||
+               params.kind == AttributeKind::kInterpolate) {
+        EXPECT_FALSE(r()->Resolve());
+        EXPECT_EQ(r()->error(), "error: " + name(params.kind) +
+                                    " is not valid for non-entry point function parameters");
     } else {
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(r()->error(),
-                  "error: attribute is not valid for non-entry point function "
-                  "parameters");
+                  "error: " + name(params.kind) + " is not valid for function parameters");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -184,9 +226,9 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(),
-                  "error: attribute is not valid for non-entry point function "
-                  "return types");
+        EXPECT_EQ(r()->error(), "error: " + name(params.kind) +
+                                    " is not valid for non-entry point function "
+                                    "return types");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -234,10 +276,11 @@
         } else if (params.kind == AttributeKind::kInterpolate ||
                    params.kind == AttributeKind::kLocation ||
                    params.kind == AttributeKind::kInvariant) {
-            EXPECT_EQ(r()->error(),
-                      "12:34 error: attribute is not valid for compute shader inputs");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for compute shader inputs");
         } else {
-            EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for function parameters");
         }
     }
 }
@@ -277,7 +320,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for function parameters");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -331,7 +375,8 @@
                       "12:34 error: invariant attribute must only be applied to a "
                       "position builtin");
         } else {
-            EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for function parameters");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for function parameters");
         }
     }
 }
@@ -378,12 +423,12 @@
         } else if (params.kind == AttributeKind::kInterpolate ||
                    params.kind == AttributeKind::kLocation ||
                    params.kind == AttributeKind::kInvariant) {
-            EXPECT_EQ(r()->error(),
-                      "12:34 error: attribute is not valid for compute shader output");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for compute shader output");
         } else {
-            EXPECT_EQ(r()->error(),
-                      "12:34 error: attribute is not valid for entry point return "
-                      "types");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for entry point return "
+                                        "types");
         }
     }
 }
@@ -434,8 +479,8 @@
                       R"(34:56 error: duplicate location attribute
 12:34 note: first attribute declared here)");
         } else {
-            EXPECT_EQ(r()->error(),
-                      R"(12:34 error: attribute is not valid for entry point return types)");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for entry point return types");
         }
     }
 }
@@ -484,8 +529,8 @@
                       R"(34:56 error: multiple entry point IO attributes
 12:34 note: previously consumed @location)");
         } else {
-            EXPECT_EQ(r()->error(),
-                      R"(12:34 error: attribute is not valid for entry point return types)");
+            EXPECT_EQ(r()->error(), "12:34 error: " + name(params.kind) +
+                                        " is not valid for entry point return types");
         }
     }
 }
@@ -591,7 +636,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for struct declarations");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for struct declarations");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -628,7 +674,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for structure members");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for struct members");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -871,7 +918,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for array types");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for array types");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -898,7 +946,6 @@
     auto& params = GetParam();
 
     auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
-    auto* attr = attrs[0];
     if (IsBindingAttribute(params.kind)) {
         GlobalVar("a", ty.sampler(type::SamplerKind::kSampler), attrs);
     } else {
@@ -910,8 +957,8 @@
     } else {
         EXPECT_FALSE(r()->Resolve());
         if (!IsBindingAttribute(params.kind)) {
-            EXPECT_EQ(r()->error(), "12:34 error: attribute '" + attr->Name() +
-                                        "' is not valid for module-scope 'var'");
+            EXPECT_EQ(r()->error(),
+                      "12:34 error: " + name(params.kind) + " is not valid for module-scope 'var'");
         }
     }
 }
@@ -944,13 +991,22 @@
 12:34 note: first attribute declared here)");
 }
 
-TEST_F(VariableAttributeTest, LocalVariable) {
+TEST_F(VariableAttributeTest, LocalVar) {
     auto* v = Var("a", ty.f32(), utils::Vector{Binding(Source{{12, 34}}, 2_a)});
 
     WrapInFunction(v);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: attributes are not valid on local variables");
+    EXPECT_EQ(r()->error(), "12:34 error: @binding is not valid for function-scope 'var'");
+}
+
+TEST_F(VariableAttributeTest, LocalLet) {
+    auto* v = Let("a", utils::Vector{Binding(Source{{12, 34}}, 2_a)}, Expr(1_a));
+
+    WrapInFunction(v);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @binding is not valid for 'let' declaration");
 }
 
 using ConstantAttributeTest = TestWithParams;
@@ -965,7 +1021,7 @@
     } else {
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(r()->error(),
-                  "12:34 error: attribute is not valid for module-scope 'const' declaration");
+                  "12:34 error: " + name(params.kind) + " is not valid for 'const' declaration");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -987,17 +1043,14 @@
                                          TestParams{AttributeKind::kWorkgroup, false},
                                          TestParams{AttributeKind::kBindingAndGroup, false}));
 
-TEST_F(ConstantAttributeTest, DuplicateAttribute) {
+TEST_F(ConstantAttributeTest, InvalidAttribute) {
     GlobalConst("a", ty.f32(), Expr(1.23_f),
                 utils::Vector{
                     Id(Source{{12, 34}}, 0_a),
-                    Id(Source{{56, 78}}, 1_a),
                 });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate id attribute
-12:34 note: first attribute declared here)");
+    EXPECT_EQ(r()->error(), "12:34 error: @id is not valid for 'const' declaration");
 }
 
 using OverrideAttributeTest = TestWithParams;
@@ -1010,7 +1063,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for 'override' declaration");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for 'override' declaration");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1056,7 +1110,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for switch statements");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for switch statements");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1089,7 +1144,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for switch body");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for switch body");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1122,7 +1178,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for if statements");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for if statements");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1155,7 +1212,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for for statements");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for for statements");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1188,7 +1246,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for loop statements");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for loop statements");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1221,7 +1280,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        EXPECT_EQ(r()->error(), "12:34 error: attribute is not valid for while statements");
+        EXPECT_EQ(r()->error(),
+                  "12:34 error: " + name(params.kind) + " is not valid for while statements");
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
@@ -1251,7 +1311,8 @@
             EXPECT_TRUE(r()->Resolve()) << r()->error();
         } else {
             EXPECT_FALSE(r()->Resolve());
-            EXPECT_EQ(r()->error(), "error: attribute is not valid for block statements");
+            EXPECT_EQ(r()->error(),
+                      "error: " + name(GetParam().kind) + " is not valid for block statements");
         }
     }
 };
diff --git a/src/tint/resolver/builtin_structs.cc b/src/tint/resolver/builtin_structs.cc
new file mode 100644
index 0000000..4e45c16
--- /dev/null
+++ b/src/tint/resolver/builtin_structs.cc
@@ -0,0 +1,239 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/builtin_structs.h"
+
+#include <algorithm>
+#include <string>
+#include <utility>
+
+#include "src/tint/program_builder.h"
+#include "src/tint/switch.h"
+#include "src/tint/type/abstract_float.h"
+#include "src/tint/type/abstract_int.h"
+#include "src/tint/type/vector.h"
+
+namespace tint::resolver {
+
+namespace {
+
+struct NameAndType {
+    std::string_view name;
+    const type::Type* type;
+};
+
+type::Struct* BuildStruct(ProgramBuilder& b,
+                          builtin::Builtin name,
+                          std::initializer_list<NameAndType> member_names_and_types) {
+    uint32_t offset = 0;
+    uint32_t max_align = 0;
+    utils::Vector<const type::StructMember*, 4> members;
+    for (auto& m : member_names_and_types) {
+        uint32_t align = std::max<uint32_t>(m.type->Align(), 1);
+        uint32_t size = m.type->Size();
+        offset = utils::RoundUp(align, offset);
+        max_align = std::max(max_align, align);
+        members.Push(b.create<type::StructMember>(
+            /* name */ b.Sym(m.name),
+            /* type */ m.type,
+            /* index */ static_cast<uint32_t>(members.Length()),
+            /* offset */ offset,
+            /* align */ align,
+            /* size */ size,
+            /* attributes */ type::StructMemberAttributes{}));
+        offset += size;
+    }
+    uint32_t size_without_padding = offset;
+    uint32_t size_with_padding = utils::RoundUp(max_align, offset);
+    return b.create<type::Struct>(
+        /* name */ b.Sym(name),
+        /* members */ std::move(members),
+        /* align */ max_align,
+        /* size */ size_with_padding,
+        /* size_no_padding */ size_without_padding);
+}
+}  // namespace
+
+constexpr std::array kModfVecF32Names{
+    builtin::Builtin::kModfResultVec2F32,
+    builtin::Builtin::kModfResultVec3F32,
+    builtin::Builtin::kModfResultVec4F32,
+};
+constexpr std::array kModfVecF16Names{
+    builtin::Builtin::kModfResultVec2F16,
+    builtin::Builtin::kModfResultVec3F16,
+    builtin::Builtin::kModfResultVec4F16,
+};
+constexpr std::array kModfVecAbstractNames{
+    builtin::Builtin::kModfResultVec2Abstract,
+    builtin::Builtin::kModfResultVec3Abstract,
+    builtin::Builtin::kModfResultVec4Abstract,
+};
+
+type::Struct* CreateModfResult(ProgramBuilder& b, const type::Type* ty) {
+    return Switch(
+        ty,
+        [&](const type::F32*) {
+            return BuildStruct(b, builtin::Builtin::kModfResultF32, {{"fract", ty}, {"whole", ty}});
+        },  //
+        [&](const type::F16*) {
+            return BuildStruct(b, builtin::Builtin::kModfResultF16, {{"fract", ty}, {"whole", ty}});
+        },
+        [&](const type::AbstractFloat*) {
+            auto* abstract = BuildStruct(b, builtin::Builtin::kModfResultAbstract,
+                                         {{"fract", ty}, {"whole", ty}});
+            auto* f32 = b.create<type::F32>();
+            auto* f16 = b.create<type::F16>();
+            abstract->SetConcreteTypes(utils::Vector{
+                BuildStruct(b, builtin::Builtin::kModfResultF32, {{"fract", f32}, {"whole", f32}}),
+                BuildStruct(b, builtin::Builtin::kModfResultF16, {{"fract", f16}, {"whole", f16}}),
+            });
+            return abstract;
+        },
+        [&](const type::Vector* vec) {
+            auto width = vec->Width();
+            return Switch(
+                vec->type(),  //
+                [&](const type::F32*) {
+                    return BuildStruct(b, kModfVecF32Names[width - 2],
+                                       {{"fract", vec}, {"whole", vec}});
+                },
+                [&](const type::F16*) {
+                    return BuildStruct(b, kModfVecF16Names[width - 2],
+                                       {{"fract", vec}, {"whole", vec}});
+                },
+                [&](const type::AbstractFloat*) {
+                    auto* vec_f32 = b.create<type::Vector>(b.create<type::F32>(), width);
+                    auto* vec_f16 = b.create<type::Vector>(b.create<type::F16>(), width);
+                    auto* abstract = BuildStruct(b, kModfVecAbstractNames[width - 2],
+                                                 {{"fract", vec}, {"whole", vec}});
+                    abstract->SetConcreteTypes(utils::Vector{
+                        BuildStruct(b, kModfVecF32Names[width - 2],
+                                    {{"fract", vec_f32}, {"whole", vec_f32}}),
+                        BuildStruct(b, kModfVecF16Names[width - 2],
+                                    {{"fract", vec_f16}, {"whole", vec_f16}}),
+                    });
+                    return abstract;
+                },
+                [&](Default) {
+                    TINT_ICE(Resolver, b.Diagnostics())
+                        << "unhandled modf type: " << b.FriendlyName(ty);
+                    return nullptr;
+                });
+        },
+        [&](Default) {
+            TINT_ICE(Resolver, b.Diagnostics()) << "unhandled modf type: " << b.FriendlyName(ty);
+            return nullptr;
+        });
+}
+
+constexpr std::array kFrexpVecF32Names{
+    builtin::Builtin::kFrexpResultVec2F32,
+    builtin::Builtin::kFrexpResultVec3F32,
+    builtin::Builtin::kFrexpResultVec4F32,
+};
+constexpr std::array kFrexpVecF16Names{
+    builtin::Builtin::kFrexpResultVec2F16,
+    builtin::Builtin::kFrexpResultVec3F16,
+    builtin::Builtin::kFrexpResultVec4F16,
+};
+constexpr std::array kFrexpVecAbstractNames{
+    builtin::Builtin::kFrexpResultVec2Abstract,
+    builtin::Builtin::kFrexpResultVec3Abstract,
+    builtin::Builtin::kFrexpResultVec4Abstract,
+};
+type::Struct* CreateFrexpResult(ProgramBuilder& b, const type::Type* ty) {
+    return Switch(
+        ty,  //
+        [&](const type::F32*) {
+            auto* i32 = b.create<type::I32>();
+            return BuildStruct(b, builtin::Builtin::kFrexpResultF32, {{"fract", ty}, {"exp", i32}});
+        },
+        [&](const type::F16*) {
+            auto* i32 = b.create<type::I32>();
+            return BuildStruct(b, builtin::Builtin::kFrexpResultF16, {{"fract", ty}, {"exp", i32}});
+        },
+        [&](const type::AbstractFloat*) {
+            auto* f32 = b.create<type::F32>();
+            auto* f16 = b.create<type::F16>();
+            auto* i32 = b.create<type::I32>();
+            auto* ai = b.create<type::AbstractInt>();
+            auto* abstract = BuildStruct(b, builtin::Builtin::kFrexpResultAbstract,
+                                         {{"fract", ty}, {"exp", ai}});
+            abstract->SetConcreteTypes(utils::Vector{
+                BuildStruct(b, builtin::Builtin::kFrexpResultF32, {{"fract", f32}, {"exp", i32}}),
+                BuildStruct(b, builtin::Builtin::kFrexpResultF16, {{"fract", f16}, {"exp", i32}}),
+            });
+            return abstract;
+        },
+        [&](const type::Vector* vec) {
+            auto width = vec->Width();
+            return Switch(
+                vec->type(),  //
+                [&](const type::F32*) {
+                    auto* vec_i32 = b.create<type::Vector>(b.create<type::I32>(), width);
+                    return BuildStruct(b, kFrexpVecF32Names[width - 2],
+                                       {{"fract", ty}, {"exp", vec_i32}});
+                },
+                [&](const type::F16*) {
+                    auto* vec_i32 = b.create<type::Vector>(b.create<type::I32>(), width);
+                    return BuildStruct(b, kFrexpVecF16Names[width - 2],
+                                       {{"fract", ty}, {"exp", vec_i32}});
+                },
+                [&](const type::AbstractFloat*) {
+                    auto* vec_f32 = b.create<type::Vector>(b.create<type::F32>(), width);
+                    auto* vec_f16 = b.create<type::Vector>(b.create<type::F16>(), width);
+                    auto* vec_i32 = b.create<type::Vector>(b.create<type::I32>(), width);
+                    auto* vec_ai = b.create<type::Vector>(b.create<type::AbstractInt>(), width);
+                    auto* abstract = BuildStruct(b, kFrexpVecAbstractNames[width - 2],
+                                                 {{"fract", ty}, {"exp", vec_ai}});
+                    abstract->SetConcreteTypes(utils::Vector{
+                        BuildStruct(b, kFrexpVecF32Names[width - 2],
+                                    {{"fract", vec_f32}, {"exp", vec_i32}}),
+                        BuildStruct(b, kFrexpVecF16Names[width - 2],
+                                    {{"fract", vec_f16}, {"exp", vec_i32}}),
+                    });
+                    return abstract;
+                },
+                [&](Default) {
+                    TINT_ICE(Resolver, b.Diagnostics())
+                        << "unhandled frexp type: " << b.FriendlyName(ty);
+                    return nullptr;
+                });
+        },
+        [&](Default) {
+            TINT_ICE(Resolver, b.Diagnostics()) << "unhandled frexp type: " << b.FriendlyName(ty);
+            return nullptr;
+        });
+}
+
+type::Struct* CreateAtomicCompareExchangeResult(ProgramBuilder& b, const type::Type* ty) {
+    return Switch(
+        ty,  //
+        [&](const type::I32*) {
+            return BuildStruct(b, builtin::Builtin::kAtomicCompareExchangeResultI32,
+                               {{"old_value", ty}, {"exchanged", b.create<type::Bool>()}});
+        },
+        [&](const type::U32*) {
+            return BuildStruct(b, builtin::Builtin::kAtomicCompareExchangeResultU32,
+                               {{"old_value", ty}, {"exchanged", b.create<type::Bool>()}});
+        },
+        [&](Default) {
+            TINT_ICE(Resolver, b.Diagnostics())
+                << "unhandled atomic_compare_exchange type: " << b.FriendlyName(ty);
+            return nullptr;
+        });
+}
+
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/builtin_structs.h b/src/tint/resolver/builtin_structs.h
new file mode 100644
index 0000000..b8f56c7
--- /dev/null
+++ b/src/tint/resolver/builtin_structs.h
@@ -0,0 +1,49 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_RESOLVER_BUILTIN_STRUCTS_H_
+#define SRC_TINT_RESOLVER_BUILTIN_STRUCTS_H_
+
+// Forward declarations
+namespace tint {
+class ProgramBuilder;
+}  // namespace tint
+namespace tint::type {
+class Struct;
+class Type;
+}  // namespace tint::type
+
+namespace tint::resolver {
+
+/**
+ * @param ty the type of the `fract` and `whole` struct members.
+ * @return the builtin struct type for a modf() builtin call.
+ */
+type::Struct* CreateModfResult(ProgramBuilder& b, const type::Type* ty);
+
+/**
+ * @param fract the type of the `fract` struct member.
+ * @return the builtin struct type for a frexp() builtin call.
+ */
+type::Struct* CreateFrexpResult(ProgramBuilder& b, const type::Type* fract);
+
+/**
+ * @param ty the type of the `old_value` struct member.
+ * @return the builtin struct type for a atomic_compare_exchange() builtin call.
+ */
+type::Struct* CreateAtomicCompareExchangeResult(ProgramBuilder& b, const type::Type* ty);
+
+}  // namespace tint::resolver
+
+#endif  // SRC_TINT_RESOLVER_BUILTIN_STRUCTS_H_
diff --git a/src/tint/resolver/builtin_structs_test.cc b/src/tint/resolver/builtin_structs_test.cc
new file mode 100644
index 0000000..9adf209
--- /dev/null
+++ b/src/tint/resolver/builtin_structs_test.cc
@@ -0,0 +1,73 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/resolver.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+
+#include "gmock/gmock.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+// access
+////////////////////////////////////////////////////////////////////////////////
+using ResolverBuiltinStructs = ResolverTestWithParam<builtin::Builtin>;
+
+TEST_P(ResolverBuiltinStructs, Resolve) {
+    Enable(builtin::Extension::kF16);
+
+    // var<private> p : NAME;
+    auto* var = GlobalVar("p", ty(GetParam()), builtin::AddressSpace::kPrivate);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    auto* str = As<type::Struct>(TypeOf(var)->UnwrapRef());
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Name().Name(), utils::ToString(GetParam()));
+    EXPECT_FALSE(Is<sem::Struct>(str));
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         ResolverBuiltinStructs,
+                         testing::Values(builtin::Builtin::kAtomicCompareExchangeResultI32,
+                                         builtin::Builtin::kAtomicCompareExchangeResultU32,
+                                         builtin::Builtin::kFrexpResultAbstract,
+                                         builtin::Builtin::kFrexpResultF16,
+                                         builtin::Builtin::kFrexpResultF32,
+                                         builtin::Builtin::kFrexpResultVec2Abstract,
+                                         builtin::Builtin::kFrexpResultVec2F16,
+                                         builtin::Builtin::kFrexpResultVec2F32,
+                                         builtin::Builtin::kFrexpResultVec3Abstract,
+                                         builtin::Builtin::kFrexpResultVec3F16,
+                                         builtin::Builtin::kFrexpResultVec3F32,
+                                         builtin::Builtin::kFrexpResultVec4Abstract,
+                                         builtin::Builtin::kFrexpResultVec4F16,
+                                         builtin::Builtin::kFrexpResultVec4F32,
+                                         builtin::Builtin::kModfResultAbstract,
+                                         builtin::Builtin::kModfResultF16,
+                                         builtin::Builtin::kModfResultF32,
+                                         builtin::Builtin::kModfResultVec2Abstract,
+                                         builtin::Builtin::kModfResultVec2F16,
+                                         builtin::Builtin::kModfResultVec2F32,
+                                         builtin::Builtin::kModfResultVec3Abstract,
+                                         builtin::Builtin::kModfResultVec3F16,
+                                         builtin::Builtin::kModfResultVec3F32,
+                                         builtin::Builtin::kModfResultVec4Abstract,
+                                         builtin::Builtin::kModfResultVec4F16,
+                                         builtin::Builtin::kModfResultVec4F32));
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 8cfbc67..60c01f7 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -925,7 +925,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -956,7 +956,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -985,7 +985,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -1020,7 +1020,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -1174,7 +1174,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -1205,7 +1205,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -1234,7 +1234,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
@@ -1269,7 +1269,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
+    auto* ty = TypeOf(call)->As<type::Struct>();
     ASSERT_NE(ty, nullptr);
     ASSERT_EQ(ty->Members().Length(), 2u);
 
diff --git a/src/tint/resolver/builtins_validation_test.cc b/src/tint/resolver/builtins_validation_test.cc
index a985a1d..d5f8c4f 100644
--- a/src/tint/resolver/builtins_validation_test.cc
+++ b/src/tint/resolver/builtins_validation_test.cc
@@ -948,7 +948,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -961,7 +961,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -978,7 +978,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -995,7 +995,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -1012,7 +1012,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -1025,7 +1025,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -1042,7 +1042,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
@@ -1059,7 +1059,7 @@
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* res_ty = TypeOf(builtin)->As<sem::Struct>();
+    auto* res_ty = TypeOf(builtin)->As<type::Struct>();
     ASSERT_TRUE(res_ty != nullptr);
     auto members = res_ty->Members();
     ASSERT_EQ(members.Length(), 2u);
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 2cef1d1..7aecfac 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -166,7 +166,7 @@
         ASSERT_NE(value, nullptr);
         EXPECT_TYPE(value->Type(), sem->Type());
 
-        if (value->Type()->Is<sem::Struct>()) {
+        if (value->Type()->Is<type::Struct>()) {
             // The result type of the constant-evaluated expression is a structure.
             // Compare each of the fields individually.
             for (size_t i = 0; i < expected_case.values.Length(); i++) {
diff --git a/src/tint/resolver/const_eval_construction_test.cc b/src/tint/resolver/const_eval_construction_test.cc
index 361edbf..d8efe8e 100644
--- a/src/tint/resolver/const_eval_construction_test.cc
+++ b/src/tint/resolver/const_eval_construction_test.cc
@@ -1445,7 +1445,7 @@
     ASSERT_NE(sem, nullptr);
     auto* arr = sem->Type()->As<type::Array>();
     ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
+    EXPECT_TRUE(arr->ElemType()->Is<type::Struct>());
     EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
     EXPECT_TRUE(sem->ConstantValue()->AnyZero());
     EXPECT_TRUE(sem->ConstantValue()->AllZero());
@@ -1662,7 +1662,7 @@
     ASSERT_NE(sem, nullptr);
     auto* arr = sem->Type()->As<type::Array>();
     ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
+    EXPECT_TRUE(arr->ElemType()->Is<type::Struct>());
     EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
     EXPECT_FALSE(sem->ConstantValue()->AnyZero());
     EXPECT_FALSE(sem->ConstantValue()->AllZero());
@@ -1787,7 +1787,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 3u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -1828,7 +1828,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 5u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -1875,7 +1875,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 3u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -1928,7 +1928,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 5u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -1999,7 +1999,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 2u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -2009,14 +2009,14 @@
 
     EXPECT_TRUE(sem->ConstantValue()->Index(0)->AnyZero());
     EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<type::Struct>());
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->ValueAs<i32>(), 0_i);
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->ValueAs<u32>(), 0_u);
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->ValueAs<f32>(), 0_f);
 
     EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
     EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<type::Struct>());
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->ValueAs<i32>(), 0_i);
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->ValueAs<u32>(), 0_u);
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->ValueAs<f32>(), 0_f);
@@ -2039,7 +2039,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 5u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -2091,7 +2091,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 5u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -2163,7 +2163,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 2u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
@@ -2173,14 +2173,14 @@
 
     EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
     EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<type::Struct>());
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->ValueAs<i32>(), 1_i);
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->ValueAs<u32>(), 2_u);
     EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->ValueAs<f32>(), 3_f);
 
     EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
     EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<type::Struct>());
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->ValueAs<i32>(), 4_i);
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->ValueAs<u32>(), 0_u);
     EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->ValueAs<f32>(), 6_f);
@@ -2199,7 +2199,7 @@
 
     auto* sem = Sem().Get(expr);
     ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
+    auto* str = sem->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 2u);
     ASSERT_NE(sem->ConstantValue(), nullptr);
diff --git a/src/tint/resolver/const_eval_member_access_test.cc b/src/tint/resolver/const_eval_member_access_test.cc
index 9a74e6c..ee92714 100644
--- a/src/tint/resolver/const_eval_member_access_test.cc
+++ b/src/tint/resolver/const_eval_member_access_test.cc
@@ -41,7 +41,7 @@
 
     auto* outer = Sem().Get(outer_expr);
     ASSERT_NE(outer, nullptr);
-    auto* str = outer->Type()->As<sem::Struct>();
+    auto* str = outer->Type()->As<type::Struct>();
     ASSERT_NE(str, nullptr);
     EXPECT_EQ(str->Members().Length(), 2u);
     ASSERT_NE(outer->ConstantValue(), nullptr);
@@ -53,7 +53,7 @@
     ASSERT_NE(o1->ConstantValue(), nullptr);
     EXPECT_FALSE(o1->ConstantValue()->AnyZero());
     EXPECT_FALSE(o1->ConstantValue()->AllZero());
-    EXPECT_TRUE(o1->ConstantValue()->Type()->Is<sem::Struct>());
+    EXPECT_TRUE(o1->ConstantValue()->Type()->Is<type::Struct>());
     EXPECT_EQ(o1->ConstantValue()->Index(0)->ValueAs<i32>(), 1_i);
     EXPECT_EQ(o1->ConstantValue()->Index(1)->ValueAs<u32>(), 2_u);
     EXPECT_EQ(o1->ConstantValue()->Index(2)->ValueAs<f32>(), 3_f);
diff --git a/src/tint/resolver/entry_point_validation_test.cc b/src/tint/resolver/entry_point_validation_test.cc
index dac6cd2..eb2d779 100644
--- a/src/tint/resolver/entry_point_validation_test.cc
+++ b/src/tint/resolver/entry_point_validation_test.cc
@@ -1084,7 +1084,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: attribute is not valid for compute shader output)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @location is not valid for compute shader output)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocation_Output) {
@@ -1099,7 +1099,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: attribute is not valid for compute shader inputs)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @location is not valid for compute shader inputs)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocationStructMember_Output) {
@@ -1119,7 +1119,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: attribute is not valid for compute shader output\n"
+              "12:34 error: @location is not valid for compute shader output\n"
               "56:78 note: while analyzing entry point 'main'");
 }
 
@@ -1138,7 +1138,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: attribute is not valid for compute shader inputs\n"
+              "12:34 error: @location is not valid for compute shader inputs\n"
               "56:78 note: while analyzing entry point 'main'");
 }
 
diff --git a/src/tint/resolver/inferred_type_test.cc b/src/tint/resolver/inferred_type_test.cc
index ab2478b..4545419 100644
--- a/src/tint/resolver/inferred_type_test.cc
+++ b/src/tint/resolver/inferred_type_test.cc
@@ -151,9 +151,9 @@
     auto* str = Structure("S", utils::Vector{member});
 
     auto* expected_type = create<sem::Struct>(
-        str, str->source, str->name->symbol,
-        utils::Vector{create<sem::StructMember>(member, member->source, member->name->symbol,
-                                                create<type::I32>(), 0u, 0u, 0u, 4u, std::nullopt)},
+        str, str->name->symbol,
+        utils::Vector{create<sem::StructMember>(member, member->name->symbol, create<type::I32>(),
+                                                0u, 0u, 0u, 4u, type::StructMemberAttributes{})},
         0u, 4u, 4u);
 
     auto* ctor_expr = Call(ty.Of(str));
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index fc226eb..4295bb3 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/ast/binary_expression.h"
 #include "src/tint/program_builder.h"
+#include "src/tint/resolver/builtin_structs.h"
 #include "src/tint/sem/evaluation_stage.h"
 #include "src/tint/sem/pipeline_stage_set.h"
 #include "src/tint/sem/value_constructor.h"
@@ -819,170 +820,26 @@
     return false;
 }
 
-struct NameAndType {
-    std::string name;
-    const type::Type* type;
-};
-sem::Struct* build_struct(ProgramBuilder& b,
-                          std::string name,
-                          std::initializer_list<NameAndType> member_names_and_types) {
-    uint32_t offset = 0;
-    uint32_t max_align = 0;
-    utils::Vector<const sem::StructMember*, 4> members;
-    for (auto& m : member_names_and_types) {
-        uint32_t align = std::max<uint32_t>(m.type->Align(), 1);
-        uint32_t size = m.type->Size();
-        offset = utils::RoundUp(align, offset);
-        max_align = std::max(max_align, align);
-        members.Push(b.create<sem::StructMember>(
-            /* declaration */ nullptr,
-            /* source */ Source{},
-            /* name */ b.Sym(m.name),
-            /* type */ m.type,
-            /* index */ static_cast<uint32_t>(members.Length()),
-            /* offset */ offset,
-            /* align */ align,
-            /* size */ size,
-            /* location */ std::nullopt));
-        offset += size;
-    }
-    uint32_t size_without_padding = offset;
-    uint32_t size_with_padding = utils::RoundUp(max_align, offset);
-    return b.create<sem::Struct>(
-        /* declaration */ nullptr,
-        /* source */ Source{},
-        /* name */ b.Sym(name),
-        /* members */ std::move(members),
-        /* align */ max_align,
-        /* size */ size_with_padding,
-        /* size_no_padding */ size_without_padding);
+const type::Struct* build_modf_result(MatchState& state, const type::Type* el) {
+    return CreateModfResult(state.builder, el);
 }
 
-const sem::Struct* build_modf_result(MatchState& state, const type::Type* el) {
-    auto build_f32 = [&] {
-        auto* ty = state.builder.create<type::F32>();
-        return build_struct(state.builder, "__modf_result_f32", {{"fract", ty}, {"whole", ty}});
-    };
-    auto build_f16 = [&] {
-        auto* ty = state.builder.create<type::F16>();
-        return build_struct(state.builder, "__modf_result_f16", {{"fract", ty}, {"whole", ty}});
-    };
-
-    return Switch(
-        el,                                             //
-        [&](const type::F32*) { return build_f32(); },  //
-        [&](const type::F16*) { return build_f16(); },  //
-        [&](const type::AbstractFloat*) {
-            auto* abstract = build_struct(state.builder, "__modf_result_abstract",
-                                          {{"fract", el}, {"whole", el}});
-            abstract->SetConcreteTypes(utils::Vector{build_f32(), build_f16()});
-            return abstract;
-        },
-        [&](Default) {
-            TINT_ICE(Resolver, state.builder.Diagnostics())
-                << "unhandled modf type: " << state.builder.FriendlyName(el);
-            return nullptr;
-        });
+const type::Struct* build_modf_result_vec(MatchState& state, Number& n, const type::Type* el) {
+    auto* vec = state.builder.create<type::Vector>(el, n.Value());
+    return CreateModfResult(state.builder, vec);
 }
 
-const sem::Struct* build_modf_result_vec(MatchState& state, Number& n, const type::Type* el) {
-    auto prefix = "__modf_result_vec" + std::to_string(n.Value());
-    auto build_f32 = [&] {
-        auto* vec =
-            state.builder.create<type::Vector>(state.builder.create<type::F32>(), n.Value());
-        return build_struct(state.builder, prefix + "_f32", {{"fract", vec}, {"whole", vec}});
-    };
-    auto build_f16 = [&] {
-        auto* vec =
-            state.builder.create<type::Vector>(state.builder.create<type::F16>(), n.Value());
-        return build_struct(state.builder, prefix + "_f16", {{"fract", vec}, {"whole", vec}});
-    };
-
-    return Switch(
-        el,                                             //
-        [&](const type::F32*) { return build_f32(); },  //
-        [&](const type::F16*) { return build_f16(); },  //
-        [&](const type::AbstractFloat*) {
-            auto* vec = state.builder.create<type::Vector>(el, n.Value());
-            auto* abstract =
-                build_struct(state.builder, prefix + "_abstract", {{"fract", vec}, {"whole", vec}});
-            abstract->SetConcreteTypes(utils::Vector{build_f32(), build_f16()});
-            return abstract;
-        },
-        [&](Default) {
-            TINT_ICE(Resolver, state.builder.Diagnostics())
-                << "unhandled modf type: " << state.builder.FriendlyName(el);
-            return nullptr;
-        });
+const type::Struct* build_frexp_result(MatchState& state, const type::Type* el) {
+    return CreateFrexpResult(state.builder, el);
 }
 
-const sem::Struct* build_frexp_result(MatchState& state, const type::Type* el) {
-    auto build_f32 = [&] {
-        auto* f = state.builder.create<type::F32>();
-        auto* i = state.builder.create<type::I32>();
-        return build_struct(state.builder, "__frexp_result_f32", {{"fract", f}, {"exp", i}});
-    };
-    auto build_f16 = [&] {
-        auto* f = state.builder.create<type::F16>();
-        auto* i = state.builder.create<type::I32>();
-        return build_struct(state.builder, "__frexp_result_f16", {{"fract", f}, {"exp", i}});
-    };
-
-    return Switch(
-        el,                                             //
-        [&](const type::F32*) { return build_f32(); },  //
-        [&](const type::F16*) { return build_f16(); },  //
-        [&](const type::AbstractFloat*) {
-            auto* i = state.builder.create<type::AbstractInt>();
-            auto* abstract =
-                build_struct(state.builder, "__frexp_result_abstract", {{"fract", el}, {"exp", i}});
-            abstract->SetConcreteTypes(utils::Vector{build_f32(), build_f16()});
-            return abstract;
-        },
-        [&](Default) {
-            TINT_ICE(Resolver, state.builder.Diagnostics())
-                << "unhandled frexp type: " << state.builder.FriendlyName(el);
-            return nullptr;
-        });
+const type::Struct* build_frexp_result_vec(MatchState& state, Number& n, const type::Type* el) {
+    auto* vec = state.builder.create<type::Vector>(el, n.Value());
+    return CreateFrexpResult(state.builder, vec);
 }
 
-const sem::Struct* build_frexp_result_vec(MatchState& state, Number& n, const type::Type* el) {
-    auto prefix = "__frexp_result_vec" + std::to_string(n.Value());
-    auto build_f32 = [&] {
-        auto* f = state.builder.create<type::Vector>(state.builder.create<type::F32>(), n.Value());
-        auto* e = state.builder.create<type::Vector>(state.builder.create<type::I32>(), n.Value());
-        return build_struct(state.builder, prefix + "_f32", {{"fract", f}, {"exp", e}});
-    };
-    auto build_f16 = [&] {
-        auto* f = state.builder.create<type::Vector>(state.builder.create<type::F16>(), n.Value());
-        auto* e = state.builder.create<type::Vector>(state.builder.create<type::I32>(), n.Value());
-        return build_struct(state.builder, prefix + "_f16", {{"fract", f}, {"exp", e}});
-    };
-
-    return Switch(
-        el,                                             //
-        [&](const type::F32*) { return build_f32(); },  //
-        [&](const type::F16*) { return build_f16(); },  //
-        [&](const type::AbstractFloat*) {
-            auto* f = state.builder.create<type::Vector>(el, n.Value());
-            auto* e = state.builder.create<type::Vector>(state.builder.create<type::AbstractInt>(),
-                                                         n.Value());
-            auto* abstract =
-                build_struct(state.builder, prefix + "_abstract", {{"fract", f}, {"exp", e}});
-            abstract->SetConcreteTypes(utils::Vector{build_f32(), build_f16()});
-            return abstract;
-        },
-        [&](Default) {
-            TINT_ICE(Resolver, state.builder.Diagnostics())
-                << "unhandled frexp type: " << state.builder.FriendlyName(el);
-            return nullptr;
-        });
-}
-
-const sem::Struct* build_atomic_compare_exchange_result(MatchState& state, const type::Type* ty) {
-    return build_struct(state.builder, "__atomic_compare_exchange_result" + ty->FriendlyName(),
-                        {{"old_value", const_cast<type::Type*>(ty)},
-                         {"exchanged", state.builder.create<type::Bool>()}});
+const type::Struct* build_atomic_compare_exchange_result(MatchState& state, const type::Type* ty) {
+    return CreateAtomicCompareExchangeResult(state.builder, ty);
 }
 
 /// ParameterInfo describes a parameter
diff --git a/src/tint/resolver/materialize_test.cc b/src/tint/resolver/materialize_test.cc
index 6fa2010..94d916c 100644
--- a/src/tint/resolver/materialize_test.cc
+++ b/src/tint/resolver/materialize_test.cc
@@ -1253,11 +1253,11 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::F32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::AbstractFloat>());
 }
 
@@ -1269,12 +1269,12 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::F32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(
         abstract_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::AbstractFloat>());
@@ -1291,11 +1291,11 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::F16>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::AbstractFloat>());
 }
 
@@ -1309,12 +1309,12 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::F16>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(
         abstract_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::AbstractFloat>());
@@ -1328,12 +1328,12 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::F32>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->Is<type::I32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::AbstractFloat>());
     ASSERT_TRUE(abstract_str->Members()[1]->Type()->Is<type::AbstractInt>());
 }
@@ -1346,14 +1346,14 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::F32>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->As<type::Vector>()->type()->Is<type::I32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(
         abstract_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::AbstractFloat>());
@@ -1372,12 +1372,12 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::F16>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->Is<type::I32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::AbstractFloat>());
     ASSERT_TRUE(abstract_str->Members()[1]->Type()->Is<type::AbstractInt>());
 }
@@ -1392,14 +1392,14 @@
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
     auto* materialize = sem->As<sem::Materialize>();
-    ASSERT_TRUE(materialize->Type()->Is<sem::Struct>());
-    auto* concrete_str = materialize->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Type()->Is<type::Struct>());
+    auto* concrete_str = materialize->Type()->As<type::Struct>();
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->Is<type::Vector>());
     ASSERT_TRUE(concrete_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::F16>());
     ASSERT_TRUE(concrete_str->Members()[1]->Type()->As<type::Vector>()->type()->Is<type::I32>());
-    ASSERT_TRUE(materialize->Expr()->Type()->Is<sem::Struct>());
-    auto* abstract_str = materialize->Expr()->Type()->As<sem::Struct>();
+    ASSERT_TRUE(materialize->Expr()->Type()->Is<type::Struct>());
+    auto* abstract_str = materialize->Expr()->Type()->As<type::Struct>();
     ASSERT_TRUE(abstract_str->Members()[0]->Type()->Is<type::Vector>());
     ASSERT_TRUE(
         abstract_str->Members()[0]->Type()->As<type::Vector>()->type()->Is<type::AbstractFloat>());
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 58050b9..de29692 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -43,6 +43,7 @@
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ast/workgroup_attribute.h"
 #include "src/tint/builtin/builtin.h"
+#include "src/tint/resolver/builtin_structs.h"
 #include "src/tint/resolver/uniformity.h"
 #include "src/tint/sem/break_if_statement.h"
 #include "src/tint/sem/builtin_enum_expression.h"
@@ -246,6 +247,20 @@
         }
     }
 
+    for (auto* attribute : v->attributes) {
+        Mark(attribute);
+        bool ok = Switch(
+            attribute,  //
+            [&](const ast::InternalAttribute* attr) -> bool { return InternalAttribute(attr); },
+            [&](Default) {
+                ErrorInvalidAttribute(attribute, "'let' declaration");
+                return false;
+            });
+        if (!ok) {
+            return nullptr;
+        }
+    }
+
     if (!v->initializer) {
         AddError("'let' declaration must have an initializer", v->source);
         return nullptr;
@@ -339,37 +354,51 @@
         /* constant_value */ nullptr, std::nullopt, std::nullopt);
     sem->SetInitializer(rhs);
 
-    if (auto* id_attr = ast::GetAttribute<ast::IdAttribute>(v->attributes)) {
-        ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@id"};
-        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+    for (auto* attribute : v->attributes) {
+        Mark(attribute);
+        bool ok = Switch(
+            attribute,  //
+            [&](const ast::IdAttribute* attr) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@id"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-        auto* materialized = Materialize(ValueExpression(id_attr->expr));
-        if (!materialized) {
+                auto* materialized = Materialize(ValueExpression(attr->expr));
+                if (!materialized) {
+                    return false;
+                }
+                if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
+                    AddError("@id must be an i32 or u32 value", attr->source);
+                    return false;
+                }
+
+                auto const_value = materialized->ConstantValue();
+                auto value = const_value->ValueAs<AInt>();
+                if (value < 0) {
+                    AddError("@id value must be non-negative", attr->source);
+                    return false;
+                }
+                if (value > std::numeric_limits<decltype(OverrideId::value)>::max()) {
+                    AddError(
+                        "@id value must be between 0 and " +
+                            std::to_string(std::numeric_limits<decltype(OverrideId::value)>::max()),
+                        attr->source);
+                    return false;
+                }
+
+                auto o = OverrideId{static_cast<decltype(OverrideId::value)>(value)};
+                sem->SetOverrideId(o);
+
+                // Track the constant IDs that are specified in the shader.
+                override_ids_.Add(o, sem);
+                return true;
+            },
+            [&](Default) {
+                ErrorInvalidAttribute(attribute, "'override' declaration");
+                return false;
+            });
+        if (!ok) {
             return nullptr;
         }
-        if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
-            AddError("@id must be an i32 or u32 value", id_attr->source);
-            return nullptr;
-        }
-
-        auto const_value = materialized->ConstantValue();
-        auto value = const_value->ValueAs<AInt>();
-        if (value < 0) {
-            AddError("@id value must be non-negative", id_attr->source);
-            return nullptr;
-        }
-        if (value > std::numeric_limits<decltype(OverrideId::value)>::max()) {
-            AddError("@id value must be between 0 and " +
-                         std::to_string(std::numeric_limits<decltype(OverrideId::value)>::max()),
-                     id_attr->source);
-            return nullptr;
-        }
-
-        auto o = OverrideId{static_cast<decltype(OverrideId::value)>(value)};
-        sem->SetOverrideId(o);
-
-        // Track the constant IDs that are specified in the shader.
-        override_ids_.Add(o, sem);
     }
 
     builder_->Sem().Add(v, sem);
@@ -392,6 +421,18 @@
         return nullptr;
     }
 
+    for (auto* attribute : c->attributes) {
+        Mark(attribute);
+        bool ok = Switch(attribute,  //
+                         [&](Default) {
+                             ErrorInvalidAttribute(attribute, "'const' declaration");
+                             return false;
+                         });
+        if (!ok) {
+            return nullptr;
+        }
+    }
+
     const sem::ValueExpression* rhs = nullptr;
     {
         ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const initializer"};
@@ -528,72 +569,98 @@
 
     sem::Variable* sem = nullptr;
     if (is_global) {
+        bool has_io_address_space = address_space == builtin::AddressSpace::kIn ||
+                                    address_space == builtin::AddressSpace::kOut;
+
+        std::optional<uint32_t> group, binding, location;
+        for (auto* attribute : var->attributes) {
+            Mark(attribute);
+            enum Status { kSuccess, kErrored, kInvalid };
+            auto res = Switch(
+                attribute,  //
+                [&](const ast::BindingAttribute* attr) {
+                    auto value = BindingAttribute(attr);
+                    if (!value) {
+                        return kErrored;
+                    }
+                    binding = value.Get();
+                    return kSuccess;
+                },
+                [&](const ast::GroupAttribute* attr) {
+                    auto value = GroupAttribute(attr);
+                    if (!value) {
+                        return kErrored;
+                    }
+                    group = value.Get();
+                    return kSuccess;
+                },
+                [&](const ast::LocationAttribute* attr) {
+                    if (!has_io_address_space) {
+                        return kInvalid;
+                    }
+                    auto value = LocationAttribute(attr);
+                    if (!value) {
+                        return kErrored;
+                    }
+                    location = value.Get();
+                    return kSuccess;
+                },
+                [&](const ast::BuiltinAttribute* attr) {
+                    if (!has_io_address_space) {
+                        return kInvalid;
+                    }
+                    return BuiltinAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InterpolateAttribute* attr) {
+                    if (!has_io_address_space) {
+                        return kInvalid;
+                    }
+                    return InterpolateAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InvariantAttribute* attr) {
+                    if (!has_io_address_space) {
+                        return kInvalid;
+                    }
+                    return InvariantAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InternalAttribute* attr) {
+                    return InternalAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](Default) { return kInvalid; });
+
+            switch (res) {
+                case kSuccess:
+                    break;
+                case kErrored:
+                    return nullptr;
+                case kInvalid:
+                    ErrorInvalidAttribute(attribute, "module-scope 'var'");
+                    return nullptr;
+            }
+        }
+
         std::optional<sem::BindingPoint> binding_point;
-        if (var->HasBindingPoint()) {
-            uint32_t binding = 0;
-            {
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-                auto* attr = ast::GetAttribute<ast::BindingAttribute>(var->attributes);
-                auto* materialized = Materialize(ValueExpression(attr->expr));
-                if (!materialized) {
-                    return nullptr;
-                }
-                if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
-                    AddError("@binding must be an i32 or u32 value", attr->source);
-                    return nullptr;
-                }
-
-                auto const_value = materialized->ConstantValue();
-                auto value = const_value->ValueAs<AInt>();
-                if (value < 0) {
-                    AddError("@binding value must be non-negative", attr->source);
-                    return nullptr;
-                }
-                binding = u32(value);
-            }
-
-            uint32_t group = 0;
-            {
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@group"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-                auto* attr = ast::GetAttribute<ast::GroupAttribute>(var->attributes);
-                auto* materialized = Materialize(ValueExpression(attr->expr));
-                if (!materialized) {
-                    return nullptr;
-                }
-                if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
-                    AddError("@group must be an i32 or u32 value", attr->source);
-                    return nullptr;
-                }
-
-                auto const_value = materialized->ConstantValue();
-                auto value = const_value->ValueAs<AInt>();
-                if (value < 0) {
-                    AddError("@group value must be non-negative", attr->source);
-                    return nullptr;
-                }
-                group = u32(value);
-            }
-            binding_point = {group, binding};
+        if (group && binding) {
+            binding_point = sem::BindingPoint{group.value(), binding.value()};
         }
-
-        std::optional<uint32_t> location;
-        if (auto* attr = ast::GetAttribute<ast::LocationAttribute>(var->attributes)) {
-            auto value = LocationAttribute(attr);
-            if (!value) {
-                return nullptr;
-            }
-            location = value.Get();
-        }
-
         sem = builder_->create<sem::GlobalVariable>(
             var, var_ty, sem::EvaluationStage::kRuntime, address_space, access,
             /* constant_value */ nullptr, binding_point, location);
 
     } else {
+        for (auto* attribute : var->attributes) {
+            Mark(attribute);
+            bool ok = Switch(
+                attribute,
+                [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
+                [&](Default) {
+                    ErrorInvalidAttribute(attribute, "function-scope 'var'");
+                    return false;
+                });
+            if (!ok) {
+                return nullptr;
+            }
+        }
         sem = builder_->create<sem::LocalVariable>(var, var_ty, sem::EvaluationStage::kRuntime,
                                                    address_space, access, current_statement_,
                                                    /* constant_value */ nullptr);
@@ -604,18 +671,93 @@
     return sem;
 }
 
-sem::Parameter* Resolver::Parameter(const ast::Parameter* param, uint32_t index) {
+sem::Parameter* Resolver::Parameter(const ast::Parameter* param,
+                                    const ast::Function* func,
+                                    uint32_t index) {
     Mark(param->name);
 
     auto add_note = [&] {
         AddNote("while instantiating parameter " + param->name->symbol.Name(), param->source);
     };
 
-    for (auto* attr : param->attributes) {
-        if (!Attribute(attr)) {
-            return nullptr;
+    std::optional<uint32_t> location, group, binding;
+
+    if (func->IsEntryPoint()) {
+        for (auto* attribute : param->attributes) {
+            Mark(attribute);
+            bool ok = Switch(
+                attribute,  //
+                [&](const ast::LocationAttribute* attr) {
+                    auto value = LocationAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    location = value.Get();
+                    return true;
+                },
+                [&](const ast::BuiltinAttribute* attr) -> bool { return BuiltinAttribute(attr); },
+                [&](const ast::InvariantAttribute* attr) -> bool {
+                    return InvariantAttribute(attr);
+                },
+                [&](const ast::InterpolateAttribute* attr) -> bool {
+                    return InterpolateAttribute(attr);
+                },
+                [&](const ast::InternalAttribute* attr) -> bool { return InternalAttribute(attr); },
+                [&](const ast::GroupAttribute* attr) -> bool {
+                    if (validator_.IsValidationEnabled(
+                            param->attributes, ast::DisabledValidation::kEntryPointParameter)) {
+                        ErrorInvalidAttribute(attribute, "function parameters");
+                        return false;
+                    }
+                    auto value = GroupAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    group = value.Get();
+                    return true;
+                },
+                [&](const ast::BindingAttribute* attr) -> bool {
+                    if (validator_.IsValidationEnabled(
+                            param->attributes, ast::DisabledValidation::kEntryPointParameter)) {
+                        ErrorInvalidAttribute(attribute, "function parameters");
+                        return false;
+                    }
+                    auto value = BindingAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    binding = value.Get();
+                    return true;
+                },
+                [&](Default) {
+                    ErrorInvalidAttribute(attribute, "function parameters");
+                    return false;
+                });
+            if (!ok) {
+                return nullptr;
+            }
+        }
+    } else {
+        for (auto* attribute : param->attributes) {
+            Mark(attribute);
+            bool ok = Switch(
+                attribute,  //
+                [&](const ast::InternalAttribute* attr) -> bool { return InternalAttribute(attr); },
+                [&](Default) {
+                    if (attribute->IsAnyOf<ast::LocationAttribute, ast::BuiltinAttribute,
+                                           ast::InvariantAttribute, ast::InterpolateAttribute>()) {
+                        ErrorInvalidAttribute(attribute, "non-entry point function parameters");
+                    } else {
+                        ErrorInvalidAttribute(attribute, "function parameters");
+                    }
+                    return false;
+                });
+            if (!ok) {
+                return nullptr;
+            }
         }
     }
+
     if (!validator_.NoDuplicateAttributes(param->attributes)) {
         return nullptr;
     }
@@ -641,72 +783,22 @@
     }
 
     std::optional<sem::BindingPoint> binding_point;
-    if (param->HasBindingPoint()) {
-        binding_point = sem::BindingPoint{};
-        {
-            ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding value"};
-            TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-            auto* attr = ast::GetAttribute<ast::BindingAttribute>(param->attributes);
-            auto* materialized = Materialize(ValueExpression(attr->expr));
-            if (!materialized) {
-                return nullptr;
-            }
-            binding_point->binding = materialized->ConstantValue()->ValueAs<u32>();
-        }
-        {
-            ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@group value"};
-            TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-            auto* attr = ast::GetAttribute<ast::GroupAttribute>(param->attributes);
-            auto* materialized = Materialize(ValueExpression(attr->expr));
-            if (!materialized) {
-                return nullptr;
-            }
-            binding_point->group = materialized->ConstantValue()->ValueAs<u32>();
-        }
-    }
-
-    std::optional<uint32_t> location;
-    if (auto* attr = ast::GetAttribute<ast::LocationAttribute>(param->attributes)) {
-        auto value = LocationAttribute(attr);
-        if (!value) {
-            return nullptr;
-        }
-        location = value.Get();
+    if (group && binding) {
+        binding_point = sem::BindingPoint{group.value(), binding.value()};
     }
 
     auto* sem = builder_->create<sem::Parameter>(
         param, index, ty, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
         sem::ParameterUsage::kNone, binding_point, location);
     builder_->Sem().Add(param, sem);
+
+    if (!validator_.Parameter(sem)) {
+        return nullptr;
+    }
+
     return sem;
 }
 
-utils::Result<uint32_t> Resolver::LocationAttribute(const ast::LocationAttribute* attr) {
-    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location value"};
-    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-    auto* materialized = Materialize(ValueExpression(attr->expr));
-    if (!materialized) {
-        return utils::Failure;
-    }
-
-    if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
-        AddError("@location must be an i32 or u32 value", attr->source);
-        return utils::Failure;
-    }
-
-    auto const_value = materialized->ConstantValue();
-    auto value = const_value->ValueAs<AInt>();
-    if (value < 0) {
-        AddError("@location value must be non-negative", attr->source);
-        return utils::Failure;
-    }
-
-    return static_cast<uint32_t>(value);
-}
-
 builtin::Access Resolver::DefaultAccessForAddressSpace(builtin::AddressSpace address_space) {
     // https://gpuweb.github.io/gpuweb/wgsl/#storage-class
     switch (address_space) {
@@ -795,12 +887,6 @@
         return nullptr;
     }
 
-    for (auto* attr : v->attributes) {
-        if (!Attribute(attr)) {
-            return nullptr;
-        }
-    }
-
     if (!validator_.NoDuplicateAttributes(v->attributes)) {
         return nullptr;
     }
@@ -859,8 +945,28 @@
 
     validator_.DiagnosticFilters().Push();
     TINT_DEFER(validator_.DiagnosticFilters().Pop());
-    for (auto* attr : decl->attributes) {
-        if (!Attribute(attr)) {
+
+    for (auto* attribute : decl->attributes) {
+        Mark(attribute);
+        bool ok = Switch(
+            attribute,
+            [&](const ast::DiagnosticAttribute* attr) { return DiagnosticAttribute(attr); },
+            [&](const ast::StageAttribute* attr) { return StageAttribute(attr); },
+            [&](const ast::MustUseAttribute* attr) { return MustUseAttribute(attr); },
+            [&](const ast::WorkgroupAttribute* attr) {
+                auto value = WorkgroupAttribute(attr);
+                if (!value) {
+                    return false;
+                }
+                func->SetWorkgroupSize(value.Get());
+                return true;
+            },
+            [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
+            [&](Default) {
+                ErrorInvalidAttribute(attribute, "functions");
+                return false;
+            });
+        if (!ok) {
             return nullptr;
         }
     }
@@ -883,19 +989,15 @@
             }
         }
 
-        auto* p = Parameter(param, parameter_index++);
+        auto* p = Parameter(param, decl, parameter_index++);
         if (!p) {
             return nullptr;
         }
 
-        if (!validator_.Parameter(decl, p)) {
-            return nullptr;
-        }
-
         func->AddParameter(p);
 
         auto* p_ty = const_cast<type::Type*>(p->Type());
-        if (auto* str = p_ty->As<sem::Struct>()) {
+        if (auto* str = p_ty->As<type::Struct>()) {
             switch (decl->PipelineStage()) {
                 case ast::PipelineStage::kVertex:
                     str->AddUsage(type::PipelineStageUsage::kVertexInput);
@@ -924,22 +1026,77 @@
     }
     func->SetReturnType(return_type);
 
-    // Determine if the return type has a location
-    for (auto* attr : decl->return_type_attributes) {
-        if (!Attribute(attr)) {
-            return nullptr;
-        }
+    if (decl->IsEntryPoint()) {
+        // Determine if the return type has a location
+        bool permissive = validator_.IsValidationDisabled(
+                              decl->attributes, ast::DisabledValidation::kEntryPointParameter) ||
+                          validator_.IsValidationDisabled(
+                              decl->attributes, ast::DisabledValidation::kFunctionParameter);
+        for (auto* attribute : decl->return_type_attributes) {
+            Mark(attribute);
+            enum Status { kSuccess, kErrored, kInvalid };
+            auto res = Switch(
+                attribute,  //
+                [&](const ast::LocationAttribute* attr) {
+                    auto value = LocationAttribute(attr);
+                    if (!value) {
+                        return kErrored;
+                    }
+                    func->SetReturnLocation(value.Get());
+                    return kSuccess;
+                },
+                [&](const ast::BuiltinAttribute* attr) {
+                    return BuiltinAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InternalAttribute* attr) {
+                    return InternalAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InterpolateAttribute* attr) {
+                    return InterpolateAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::InvariantAttribute* attr) {
+                    return InvariantAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::BindingAttribute* attr) {
+                    if (!permissive) {
+                        return kInvalid;
+                    }
+                    return BindingAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](const ast::GroupAttribute* attr) {
+                    if (!permissive) {
+                        return kInvalid;
+                    }
+                    return GroupAttribute(attr) ? kSuccess : kErrored;
+                },
+                [&](Default) { return kInvalid; });
 
-        if (auto* loc_attr = attr->As<ast::LocationAttribute>()) {
-            auto value = LocationAttribute(loc_attr);
-            if (!value) {
+            switch (res) {
+                case kSuccess:
+                    break;
+                case kErrored:
+                    return nullptr;
+                case kInvalid:
+                    ErrorInvalidAttribute(attribute, "entry point return types");
+                    return nullptr;
+            }
+        }
+    } else {
+        for (auto* attribute : decl->return_type_attributes) {
+            Mark(attribute);
+            bool ok = Switch(attribute,  //
+                             [&](Default) {
+                                 ErrorInvalidAttribute(attribute,
+                                                       "non-entry point function return types");
+                                 return false;
+                             });
+            if (!ok) {
                 return nullptr;
             }
-            func->SetReturnLocation(value.Get());
         }
     }
 
-    if (auto* str = return_type->As<sem::Struct>()) {
+    if (auto* str = return_type->As<type::Struct>()) {
         if (!ApplyAddressSpaceUsageToType(builtin::AddressSpace::kUndefined, str, decl->source)) {
             AddNote("while instantiating return type for " + decl->name->symbol.Name(),
                     decl->source);
@@ -963,10 +1120,6 @@
 
     ApplyDiagnosticSeverities(func);
 
-    if (!WorkgroupSize(decl)) {
-        return nullptr;
-    }
-
     if (decl->IsEntryPoint()) {
         entry_points_.Push(func);
     }
@@ -1015,94 +1168,6 @@
     return func;
 }
 
-bool Resolver::WorkgroupSize(const ast::Function* func) {
-    // Set work-group size defaults.
-    sem::WorkgroupSize ws;
-    for (size_t i = 0; i < 3; i++) {
-        ws[i] = 1;
-    }
-
-    auto* attr = ast::GetAttribute<ast::WorkgroupAttribute>(func->attributes);
-    if (!attr) {
-        return true;
-    }
-
-    auto values = attr->Values();
-    utils::Vector<const sem::ValueExpression*, 3> args;
-    utils::Vector<const type::Type*, 3> arg_tys;
-
-    constexpr const char* kErrBadExpr =
-        "workgroup_size argument must be a constant or override-expression of type "
-        "abstract-integer, i32 or u32";
-
-    for (size_t i = 0; i < 3; i++) {
-        // Each argument to this attribute can either be a literal, an identifier for a
-        // module-scope constants, a const-expression, or nullptr if not specified.
-        auto* value = values[i];
-        if (!value) {
-            break;
-        }
-        const auto* expr = ValueExpression(value);
-        if (!expr) {
-            return false;
-        }
-        auto* ty = expr->Type();
-        if (!ty->IsAnyOf<type::I32, type::U32, type::AbstractInt>()) {
-            AddError(kErrBadExpr, value->source);
-            return false;
-        }
-
-        if (expr->Stage() != sem::EvaluationStage::kConstant &&
-            expr->Stage() != sem::EvaluationStage::kOverride) {
-            AddError(kErrBadExpr, value->source);
-            return false;
-        }
-
-        args.Push(expr);
-        arg_tys.Push(ty);
-    }
-
-    auto* common_ty = type::Type::Common(arg_tys);
-    if (!common_ty) {
-        AddError("workgroup_size arguments must be of the same type, either i32 or u32",
-                 attr->source);
-        return false;
-    }
-
-    // If all arguments are abstract-integers, then materialize to i32.
-    if (common_ty->Is<type::AbstractInt>()) {
-        common_ty = builder_->create<type::I32>();
-    }
-
-    for (size_t i = 0; i < args.Length(); i++) {
-        auto* materialized = Materialize(args[i], common_ty);
-        if (!materialized) {
-            return false;
-        }
-        if (auto* value = materialized->ConstantValue()) {
-            if (value->ValueAs<AInt>() < 1) {
-                AddError("workgroup_size argument must be at least 1", values[i]->source);
-                return false;
-            }
-            ws[i] = value->ValueAs<u32>();
-        } else {
-            ws[i] = std::nullopt;
-        }
-    }
-
-    uint64_t total_size = static_cast<uint64_t>(ws[0].value_or(1));
-    for (size_t i = 1; i < 3; i++) {
-        total_size *= static_cast<uint64_t>(ws[i].value_or(1));
-        if (total_size > 0xffffffff) {
-            AddError("total workgroup grid size cannot exceed 0xffffffff", values[i]->source);
-            return false;
-        }
-    }
-
-    current_function_->SetWorkgroupSize(std::move(ws));
-    return true;
-}
-
 bool Resolver::Statements(utils::VectorRef<const ast::Statement*> stmts) {
     sem::Behaviors behaviors{sem::Behavior::kNext};
 
@@ -1696,7 +1761,7 @@
             }
             return nullptr;
         },
-        [&](const sem::Struct* s) -> const type::Type* {
+        [&](const type::Struct* s) -> const type::Type* {
             if (auto tys = s->ConcreteTypes(); !tys.IsEmpty()) {
                 return target_ty ? target_ty : tys[0];
             }
@@ -2078,7 +2143,7 @@
                 }
                 return call;
             },
-            [&](const sem::Struct* str) -> sem::Call* {
+            [&](const type::Struct* str) -> sem::Call* {
                 auto* call_target = struct_ctors_.GetOrCreate(
                     StructConstructorSig{{str, args.Length(), args_stage}},
                     [&]() -> sem::ValueConstructor* {
@@ -2336,6 +2401,7 @@
     auto check_no_tmpl_args = [&](type::Type* ty) -> type::Type* {
         return TINT_LIKELY(CheckNotTemplated("type", ident)) ? ty : nullptr;
     };
+    auto af = [&] { return b.create<type::AbstractFloat>(); };
     auto f32 = [&] { return b.create<type::F32>(); };
     auto i32 = [&] { return b.create<type::I32>(); };
     auto u32 = [&] { return b.create<type::U32>(); };
@@ -2586,9 +2652,6 @@
             return nullptr;
         }
 
-        if (TINT_UNLIKELY(!el_ty)) {
-            return nullptr;
-        }
         if (TINT_UNLIKELY(!validator_.Vector(el_ty, ident->source))) {
             return nullptr;
         }
@@ -2740,9 +2803,60 @@
             return storage_texture(type::TextureDimension::k2dArray);
         case builtin::Builtin::kTextureStorage3D:
             return storage_texture(type::TextureDimension::k3d);
-        case builtin::Builtin::kPackedVec3: {
+        case builtin::Builtin::kPackedVec3:
             return packed_vec3_t();
-        }
+        case builtin::Builtin::kAtomicCompareExchangeResultI32:
+            return CreateAtomicCompareExchangeResult(*builder_, i32());
+        case builtin::Builtin::kAtomicCompareExchangeResultU32:
+            return CreateAtomicCompareExchangeResult(*builder_, u32());
+        case builtin::Builtin::kFrexpResultAbstract:
+            return CreateFrexpResult(*builder_, af());
+        case builtin::Builtin::kFrexpResultF16:
+            return CreateFrexpResult(*builder_, f16());
+        case builtin::Builtin::kFrexpResultF32:
+            return CreateFrexpResult(*builder_, f32());
+        case builtin::Builtin::kFrexpResultVec2Abstract:
+            return CreateFrexpResult(*builder_, vec(af(), 2));
+        case builtin::Builtin::kFrexpResultVec2F16:
+            return CreateFrexpResult(*builder_, vec(f16(), 2));
+        case builtin::Builtin::kFrexpResultVec2F32:
+            return CreateFrexpResult(*builder_, vec(f32(), 2));
+        case builtin::Builtin::kFrexpResultVec3Abstract:
+            return CreateFrexpResult(*builder_, vec(af(), 3));
+        case builtin::Builtin::kFrexpResultVec3F16:
+            return CreateFrexpResult(*builder_, vec(f16(), 3));
+        case builtin::Builtin::kFrexpResultVec3F32:
+            return CreateFrexpResult(*builder_, vec(f32(), 3));
+        case builtin::Builtin::kFrexpResultVec4Abstract:
+            return CreateFrexpResult(*builder_, vec(af(), 4));
+        case builtin::Builtin::kFrexpResultVec4F16:
+            return CreateFrexpResult(*builder_, vec(f16(), 4));
+        case builtin::Builtin::kFrexpResultVec4F32:
+            return CreateFrexpResult(*builder_, vec(f32(), 4));
+        case builtin::Builtin::kModfResultAbstract:
+            return CreateModfResult(*builder_, af());
+        case builtin::Builtin::kModfResultF16:
+            return CreateModfResult(*builder_, f16());
+        case builtin::Builtin::kModfResultF32:
+            return CreateModfResult(*builder_, f32());
+        case builtin::Builtin::kModfResultVec2Abstract:
+            return CreateModfResult(*builder_, vec(af(), 2));
+        case builtin::Builtin::kModfResultVec2F16:
+            return CreateModfResult(*builder_, vec(f16(), 2));
+        case builtin::Builtin::kModfResultVec2F32:
+            return CreateModfResult(*builder_, vec(f32(), 2));
+        case builtin::Builtin::kModfResultVec3Abstract:
+            return CreateModfResult(*builder_, vec(af(), 3));
+        case builtin::Builtin::kModfResultVec3F16:
+            return CreateModfResult(*builder_, vec(f16(), 3));
+        case builtin::Builtin::kModfResultVec3F32:
+            return CreateModfResult(*builder_, vec(f32(), 3));
+        case builtin::Builtin::kModfResultVec4Abstract:
+            return CreateModfResult(*builder_, vec(af(), 4));
+        case builtin::Builtin::kModfResultVec4F16:
+            return CreateModfResult(*builder_, vec(f16(), 4));
+        case builtin::Builtin::kModfResultVec4F32:
+            return CreateModfResult(*builder_, vec(f32(), 4));
         case builtin::Builtin::kUndefined:
             break;
     }
@@ -3134,10 +3248,10 @@
 
     return Switch(
         storage_ty,  //
-        [&](const sem::Struct* str) -> sem::ValueExpression* {
+        [&](const type::Struct* str) -> sem::ValueExpression* {
             auto symbol = expr->member->symbol;
 
-            const sem::StructMember* member = nullptr;
+            const type::StructMember* member = nullptr;
             for (auto* m : str->Members()) {
                 if (m->Name() == symbol) {
                     member = m;
@@ -3424,38 +3538,207 @@
     return sem;
 }
 
-bool Resolver::Attribute(const ast::Attribute* attr) {
-    Mark(attr);
-    return Switch(
-        attr,  //
-        [&](const ast::BuiltinAttribute* b) { return BuiltinAttribute(b); },
-        [&](const ast::DiagnosticAttribute* d) { return DiagnosticControl(d->control); },
-        [&](const ast::InterpolateAttribute* i) { return InterpolateAttribute(i); },
-        [&](const ast::InternalAttribute* i) { return InternalAttribute(i); },
-        [&](Default) { return true; });
+utils::Result<uint32_t> Resolver::LocationAttribute(const ast::LocationAttribute* attr) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location value"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+    auto* materialized = Materialize(ValueExpression(attr->expr));
+    if (!materialized) {
+        return utils::Failure;
+    }
+
+    if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
+        AddError("@location must be an i32 or u32 value", attr->source);
+        return utils::Failure;
+    }
+
+    auto const_value = materialized->ConstantValue();
+    auto value = const_value->ValueAs<AInt>();
+    if (value < 0) {
+        AddError("@location value must be non-negative", attr->source);
+        return utils::Failure;
+    }
+
+    return static_cast<uint32_t>(value);
 }
 
-bool Resolver::BuiltinAttribute(const ast::BuiltinAttribute* attr) {
+utils::Result<uint32_t> Resolver::BindingAttribute(const ast::BindingAttribute* attr) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+    auto* materialized = Materialize(ValueExpression(attr->expr));
+    if (!materialized) {
+        return utils::Failure;
+    }
+    if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
+        AddError("@binding must be an i32 or u32 value", attr->source);
+        return utils::Failure;
+    }
+
+    auto const_value = materialized->ConstantValue();
+    auto value = const_value->ValueAs<AInt>();
+    if (value < 0) {
+        AddError("@binding value must be non-negative", attr->source);
+        return utils::Failure;
+    }
+    return static_cast<uint32_t>(value);
+}
+
+utils::Result<uint32_t> Resolver::GroupAttribute(const ast::GroupAttribute* attr) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@group"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+    auto* materialized = Materialize(ValueExpression(attr->expr));
+    if (!materialized) {
+        return utils::Failure;
+    }
+    if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
+        AddError("@group must be an i32 or u32 value", attr->source);
+        return utils::Failure;
+    }
+
+    auto const_value = materialized->ConstantValue();
+    auto value = const_value->ValueAs<AInt>();
+    if (value < 0) {
+        AddError("@group value must be non-negative", attr->source);
+        return utils::Failure;
+    }
+    return static_cast<uint32_t>(value);
+}
+
+utils::Result<sem::WorkgroupSize> Resolver::WorkgroupAttribute(
+    const ast::WorkgroupAttribute* attr) {
+    // Set work-group size defaults.
+    sem::WorkgroupSize ws;
+    for (size_t i = 0; i < 3; i++) {
+        ws[i] = 1;
+    }
+
+    auto values = attr->Values();
+    utils::Vector<const sem::ValueExpression*, 3> args;
+    utils::Vector<const type::Type*, 3> arg_tys;
+
+    constexpr const char* kErrBadExpr =
+        "workgroup_size argument must be a constant or override-expression of type "
+        "abstract-integer, i32 or u32";
+
+    for (size_t i = 0; i < 3; i++) {
+        // Each argument to this attribute can either be a literal, an identifier for a
+        // module-scope constants, a const-expression, or nullptr if not specified.
+        auto* value = values[i];
+        if (!value) {
+            break;
+        }
+        const auto* expr = ValueExpression(value);
+        if (!expr) {
+            return utils::Failure;
+        }
+        auto* ty = expr->Type();
+        if (!ty->IsAnyOf<type::I32, type::U32, type::AbstractInt>()) {
+            AddError(kErrBadExpr, value->source);
+            return utils::Failure;
+        }
+
+        if (expr->Stage() != sem::EvaluationStage::kConstant &&
+            expr->Stage() != sem::EvaluationStage::kOverride) {
+            AddError(kErrBadExpr, value->source);
+            return utils::Failure;
+        }
+
+        args.Push(expr);
+        arg_tys.Push(ty);
+    }
+
+    auto* common_ty = type::Type::Common(arg_tys);
+    if (!common_ty) {
+        AddError("workgroup_size arguments must be of the same type, either i32 or u32",
+                 attr->source);
+        return utils::Failure;
+    }
+
+    // If all arguments are abstract-integers, then materialize to i32.
+    if (common_ty->Is<type::AbstractInt>()) {
+        common_ty = builder_->create<type::I32>();
+    }
+
+    for (size_t i = 0; i < args.Length(); i++) {
+        auto* materialized = Materialize(args[i], common_ty);
+        if (!materialized) {
+            return utils::Failure;
+        }
+        if (auto* value = materialized->ConstantValue()) {
+            if (value->ValueAs<AInt>() < 1) {
+                AddError("workgroup_size argument must be at least 1", values[i]->source);
+                return utils::Failure;
+            }
+            ws[i] = value->ValueAs<u32>();
+        } else {
+            ws[i] = std::nullopt;
+        }
+    }
+
+    uint64_t total_size = static_cast<uint64_t>(ws[0].value_or(1));
+    for (size_t i = 1; i < 3; i++) {
+        total_size *= static_cast<uint64_t>(ws[i].value_or(1));
+        if (total_size > 0xffffffff) {
+            AddError("total workgroup grid size cannot exceed 0xffffffff", values[i]->source);
+            return utils::Failure;
+        }
+    }
+
+    return ws;
+}
+
+utils::Result<tint::builtin::BuiltinValue> Resolver::BuiltinAttribute(
+    const ast::BuiltinAttribute* attr) {
     auto* builtin_expr = BuiltinValueExpression(attr->builtin);
     if (!builtin_expr) {
-        return false;
+        return utils::Failure;
     }
     // Apply the resolved tint::sem::BuiltinEnumExpression<tint::builtin::BuiltinValue> to the
     // attribute.
     builder_->Sem().Add(attr, builtin_expr);
+    return builtin_expr->Value();
+}
+
+bool Resolver::DiagnosticAttribute(const ast::DiagnosticAttribute* attr) {
+    return DiagnosticControl(attr->control);
+}
+
+bool Resolver::StageAttribute(const ast::StageAttribute*) {
     return true;
 }
 
-bool Resolver::InterpolateAttribute(const ast::InterpolateAttribute* attr) {
-    if (!InterpolationType(attr->type)) {
-        return false;
-    }
-    if (attr->sampling && !InterpolationSampling(attr->sampling)) {
-        return false;
-    }
+bool Resolver::MustUseAttribute(const ast::MustUseAttribute*) {
     return true;
 }
 
+bool Resolver::InvariantAttribute(const ast::InvariantAttribute*) {
+    return true;
+}
+
+bool Resolver::StrideAttribute(const ast::StrideAttribute*) {
+    return true;
+}
+
+utils::Result<builtin::Interpolation> Resolver::InterpolateAttribute(
+    const ast::InterpolateAttribute* attr) {
+    builtin::Interpolation out;
+    auto* type = InterpolationType(attr->type);
+    if (!type) {
+        return utils::Failure;
+    }
+    out.type = type->Value();
+    if (attr->sampling) {
+        auto* sampling = InterpolationSampling(attr->sampling);
+        if (!sampling) {
+            return utils::Failure;
+        }
+        out.sampling = sampling->Value();
+    }
+    return out;
+}
+
 bool Resolver::InternalAttribute(const ast::InternalAttribute* attr) {
     for (auto* dep : attr->dependencies) {
         if (!Expression(dep)) {
@@ -3576,24 +3859,30 @@
         return false;
     }
 
-    for (auto* attr : attributes) {
-        Mark(attr);
-        if (auto* sd = attr->As<ast::StrideAttribute>()) {
-            // If the element type is not plain, then el_ty->Align() may be 0, in which case we
-            // could get a DBZ in ArrayStrideAttribute(). In this case, validation will error
-            // about the invalid array element type (which is tested later), so this is just a
-            // seatbelt.
-            if (IsPlain(el_ty)) {
-                explicit_stride = sd->stride;
-                if (!validator_.ArrayStrideAttribute(sd, el_ty->Size(), el_ty->Align())) {
-                    return false;
+    for (auto* attribute : attributes) {
+        Mark(attribute);
+        bool ok = Switch(
+            attribute,  //
+            [&](const ast::StrideAttribute* attr) {
+                // If the element type is not plain, then el_ty->Align() may be 0, in which case we
+                // could get a DBZ in ArrayStrideAttribute(). In this case, validation will error
+                // about the invalid array element type (which is tested later), so this is just a
+                // seatbelt.
+                if (IsPlain(el_ty)) {
+                    explicit_stride = attr->stride;
+                    if (!validator_.ArrayStrideAttribute(attr, el_ty->Size(), el_ty->Align())) {
+                        return false;
+                    }
                 }
-            }
-            continue;
+                return true;
+            },
+            [&](Default) {
+                ErrorInvalidAttribute(attribute, "array types");
+                return false;
+            });
+        if (!ok) {
+            return false;
         }
-
-        AddError("attribute is not valid for array types", attr->source);
-        return false;
     }
 
     return true;
@@ -3677,8 +3966,18 @@
     if (!validator_.NoDuplicateAttributes(str->attributes)) {
         return nullptr;
     }
-    for (auto* attr : str->attributes) {
-        Mark(attr);
+
+    for (auto* attribute : str->attributes) {
+        Mark(attribute);
+        bool ok = Switch(
+            attribute, [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
+            [&](Default) {
+                ErrorInvalidAttribute(attribute, "struct declarations");
+                return false;
+            });
+        if (!ok) {
+            return nullptr;
+        }
     }
 
     utils::Vector<const sem::StructMember*, 8> sem_members;
@@ -3730,89 +4029,88 @@
         bool has_offset_attr = false;
         bool has_align_attr = false;
         bool has_size_attr = false;
-        std::optional<uint32_t> location;
-        for (auto* attr : member->attributes) {
-            if (!Attribute(attr)) {
-                return nullptr;
-            }
+        type::StructMemberAttributes attributes;
+        for (auto* attribute : member->attributes) {
+            Mark(attribute);
             bool ok = Switch(
-                attr,  //
-                [&](const ast::StructMemberOffsetAttribute* o) {
-                    // Offset attributes are not part of the WGSL spec, but are emitted
-                    // by the SPIR-V reader.
+                attribute,  //
+                [&](const ast::StructMemberOffsetAttribute* attr) {
+                    // Offset attributes are not part of the WGSL spec, but are emitted by the
+                    // SPIR-V reader.
+
                     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
                                                        "@offset value"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(ValueExpression(o->expr));
+                    auto* materialized = Materialize(ValueExpression(attr->expr));
                     if (!materialized) {
                         return false;
                     }
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@offset must be constant expression", o->expr->source);
+                        AddError("@offset must be constant expression", attr->expr->source);
                         return false;
                     }
                     offset = const_value->ValueAs<uint64_t>();
 
                     if (offset < struct_size) {
-                        AddError("offsets must be in ascending order", o->source);
+                        AddError("offsets must be in ascending order", attr->source);
                         return false;
                     }
                     has_offset_attr = true;
                     return true;
                 },
-                [&](const ast::StructMemberAlignAttribute* a) {
+                [&](const ast::StructMemberAlignAttribute* attr) {
                     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(ValueExpression(a->expr));
+                    auto* materialized = Materialize(ValueExpression(attr->expr));
                     if (!materialized) {
                         return false;
                     }
                     if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
-                        AddError("@align must be an i32 or u32 value", a->source);
+                        AddError("@align must be an i32 or u32 value", attr->source);
                         return false;
                     }
 
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@align must be constant expression", a->source);
+                        AddError("@align must be constant expression", attr->source);
                         return false;
                     }
                     auto value = const_value->ValueAs<AInt>();
 
                     if (value <= 0 || !utils::IsPowerOfTwo(value)) {
                         AddError("@align value must be a positive, power-of-two integer",
-                                 a->source);
+                                 attr->source);
                         return false;
                     }
                     align = u32(value);
                     has_align_attr = true;
                     return true;
                 },
-                [&](const ast::StructMemberSizeAttribute* s) {
+                [&](const ast::StructMemberSizeAttribute* attr) {
                     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(ValueExpression(s->expr));
+                    auto* materialized = Materialize(ValueExpression(attr->expr));
                     if (!materialized) {
                         return false;
                     }
                     if (!materialized->Type()->IsAnyOf<type::U32, type::I32>()) {
-                        AddError("@size must be an i32 or u32 value", s->source);
+                        AddError("@size must be an i32 or u32 value", attr->source);
                         return false;
                     }
 
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@size must be constant expression", s->expr->source);
+                        AddError("@size must be constant expression", attr->expr->source);
                         return false;
                     }
                     {
                         auto value = const_value->ValueAs<AInt>();
                         if (value <= 0) {
-                            AddError("@size must be a positive integer", s->source);
+                            AddError("@size must be a positive integer", attr->source);
                             return false;
                         }
                     }
@@ -3820,24 +4118,56 @@
                     if (value < size) {
                         AddError("@size must be at least as big as the type's size (" +
                                      std::to_string(size) + ")",
-                                 s->source);
+                                 attr->source);
                         return false;
                     }
                     size = u32(value);
                     has_size_attr = true;
                     return true;
                 },
-                [&](const ast::LocationAttribute* loc_attr) {
-                    auto value = LocationAttribute(loc_attr);
+                [&](const ast::LocationAttribute* attr) {
+                    auto value = LocationAttribute(attr);
                     if (!value) {
                         return false;
                     }
-                    location = value.Get();
+                    attributes.location = value.Get();
                     return true;
                 },
-                [&](Default) {
-                    // The validator will check attributes can be applied to the struct member.
+                [&](const ast::BuiltinAttribute* attr) {
+                    auto value = BuiltinAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.builtin = value.Get();
                     return true;
+                },
+                [&](const ast::InterpolateAttribute* attr) {
+                    auto value = InterpolateAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.interpolation = value.Get();
+                    return true;
+                },
+                [&](const ast::InvariantAttribute* attr) {
+                    if (!InvariantAttribute(attr)) {
+                        return false;
+                    }
+                    attributes.invariant = true;
+                    return true;
+                },
+                [&](const ast::StrideAttribute* attr) {
+                    if (validator_.IsValidationEnabled(
+                            member->attributes, ast::DisabledValidation::kIgnoreStrideAttribute)) {
+                        ErrorInvalidAttribute(attribute, "struct members");
+                        return false;
+                    }
+                    return StrideAttribute(attr);
+                },
+                [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
+                [&](Default) {
+                    ErrorInvalidAttribute(attribute, "struct members");
+                    return false;
                 });
             if (!ok) {
                 return nullptr;
@@ -3859,9 +4189,9 @@
         }
 
         auto* sem_member = builder_->create<sem::StructMember>(
-            member, member->source, member->name->symbol, type,
-            static_cast<uint32_t>(sem_members.Length()), static_cast<uint32_t>(offset),
-            static_cast<uint32_t>(align), static_cast<uint32_t>(size), location);
+            member, member->name->symbol, type, static_cast<uint32_t>(sem_members.Length()),
+            static_cast<uint32_t>(offset), static_cast<uint32_t>(align),
+            static_cast<uint32_t>(size), attributes);
         builder_->Sem().Add(member, sem_member);
         sem_members.Push(sem_member);
 
@@ -3884,14 +4214,13 @@
     }
 
     auto* out = builder_->create<sem::Struct>(
-        str, str->source, str->name->symbol, std::move(sem_members),
-        static_cast<uint32_t>(struct_align), static_cast<uint32_t>(struct_size),
-        static_cast<uint32_t>(size_no_padding));
+        str, str->name->symbol, std::move(sem_members), static_cast<uint32_t>(struct_align),
+        static_cast<uint32_t>(struct_size), static_cast<uint32_t>(size_no_padding));
 
     for (size_t i = 0; i < sem_members.Length(); i++) {
         auto* mem_type = sem_members[i]->Type();
         if (mem_type->Is<type::Atomic>()) {
-            atomic_composite_info_.Add(out, &sem_members[i]->Source());
+            atomic_composite_info_.Add(out, &sem_members[i]->Declaration()->source);
             break;
         } else {
             if (auto found = atomic_composite_info_.Get(mem_type)) {
@@ -3999,14 +4328,16 @@
         }
 
         // Handle switch body attributes.
-        for (auto* attr : stmt->body_attributes) {
-            Mark(attr);
-            if (auto* dc = attr->As<ast::DiagnosticAttribute>()) {
-                if (!DiagnosticControl(dc->control)) {
+        for (auto* attribute : stmt->body_attributes) {
+            Mark(attribute);
+            bool ok = Switch(
+                attribute,
+                [&](const ast::DiagnosticAttribute* attr) { return DiagnosticAttribute(attr); },
+                [&](Default) {
+                    ErrorInvalidAttribute(attribute, "switch body");
                     return false;
-                }
-            } else {
-                AddError("attribute is not valid for switch body", attr->source);
+                });
+            if (!ok) {
                 return false;
             }
         }
@@ -4049,14 +4380,6 @@
             return false;
         }
 
-        for (auto* attr : stmt->variable->attributes) {
-            Mark(attr);
-            if (!attr->Is<ast::InternalAttribute>()) {
-                AddError("attributes are not valid on local variables", attr->source);
-                return false;
-            }
-        }
-
         current_compound_statement_->AddDecl(variable->As<sem::LocalVariable>());
 
         if (auto* ctor = variable->Initializer()) {
@@ -4242,7 +4565,7 @@
                 utils::StringStream err;
                 err << "while analyzing structure member " << sem_.TypeNameOf(str) << "."
                     << member->Name().Name();
-                AddNote(err.str(), member->Source());
+                AddNote(err.str(), member->Declaration()->source);
                 return false;
             }
         }
@@ -4289,16 +4612,16 @@
 
     // Helper to handle attributes that are supported on certain types of statement.
     auto handle_attributes = [&](auto* stmt, sem::Statement* sem_stmt, const char* use) {
-        for (auto* attr : stmt->attributes) {
-            Mark(attr);
-            if (auto* dc = attr->template As<ast::DiagnosticAttribute>()) {
-                if (!DiagnosticControl(dc->control)) {
+        for (auto* attribute : stmt->attributes) {
+            Mark(attribute);
+            bool ok = Switch(
+                attribute,  //
+                [&](const ast::DiagnosticAttribute* attr) { return DiagnosticAttribute(attr); },
+                [&](Default) {
+                    ErrorInvalidAttribute(attribute, use);
                     return false;
-                }
-            } else {
-                utils::StringStream ss;
-                ss << "attribute is not valid for " << use;
-                AddError(ss.str(), attr->source);
+                });
+            if (!ok) {
                 return false;
             }
         }
@@ -4401,6 +4724,10 @@
     sem_.NoteDeclarationSource(resolved.Node());
 }
 
+void Resolver::ErrorInvalidAttribute(const ast::Attribute* attr, std::string_view use) {
+    AddError("@" + attr->Name() + " is not valid for " + std::string(use), attr->source);
+}
+
 void Resolver::AddError(const std::string& msg, const Source& source) const {
     diagnostics_.add_error(diag::System::Resolver, msg, source);
 }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index edc088a..5d13d25 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -312,17 +312,50 @@
     /// current_function_
     bool WorkgroupSize(const ast::Function*);
 
-    /// Resolves the attribute @p attr
-    /// @returns true on success, false on failure
-    bool Attribute(const ast::Attribute* attr);
-
     /// Resolves the `@builtin` attribute @p attr
+    /// @returns the builtin value on success
+    utils::Result<tint::builtin::BuiltinValue> BuiltinAttribute(const ast::BuiltinAttribute* attr);
+
+    /// Resolves the `@location` attribute @p attr
+    /// @returns the location value on success.
+    utils::Result<uint32_t> LocationAttribute(const ast::LocationAttribute* attr);
+
+    /// Resolves the `@binding` attribute @p attr
+    /// @returns the binding value on success.
+    utils::Result<uint32_t> BindingAttribute(const ast::BindingAttribute* attr);
+
+    /// Resolves the `@group` attribute @p attr
+    /// @returns the group value on success.
+    utils::Result<uint32_t> GroupAttribute(const ast::GroupAttribute* attr);
+
+    /// Resolves the `@workgroup_size` attribute @p attr
+    /// @returns the workgroup size on success.
+    utils::Result<sem::WorkgroupSize> WorkgroupAttribute(const ast::WorkgroupAttribute* attr);
+
+    /// Resolves the `@diagnostic` attribute @p attr
     /// @returns true on success, false on failure
-    bool BuiltinAttribute(const ast::BuiltinAttribute* attr);
+    bool DiagnosticAttribute(const ast::DiagnosticAttribute* attr);
+
+    /// Resolves the stage attribute @p attr
+    /// @returns true on success, false on failure
+    bool StageAttribute(const ast::StageAttribute* attr);
+
+    /// Resolves the `@must_use` attribute @p attr
+    /// @returns true on success, false on failure
+    bool MustUseAttribute(const ast::MustUseAttribute* attr);
+
+    /// Resolves the `@invariant` attribute @p attr
+    /// @returns true on success, false on failure
+    bool InvariantAttribute(const ast::InvariantAttribute*);
+
+    /// Resolves the `@stride` attribute @p attr
+    /// @returns true on success, false on failure
+    bool StrideAttribute(const ast::StrideAttribute*);
 
     /// Resolves the `@interpolate` attribute @p attr
     /// @returns true on success, false on failure
-    bool InterpolateAttribute(const ast::InterpolateAttribute* attr);
+    utils::Result<builtin::Interpolation> InterpolateAttribute(
+        const ast::InterpolateAttribute* attr);
 
     /// Resolves the internal attribute @p attr
     /// @returns true on success, false on failure
@@ -427,12 +460,11 @@
     /// nullptr is returned.
     /// @note the caller is expected to validate the parameter
     /// @param param the AST parameter
+    /// @param func the AST function that owns the parameter
     /// @param index the index of the parameter
-    sem::Parameter* Parameter(const ast::Parameter* param, uint32_t index);
-
-    /// @returns the location value for a `@location` attribute, validating the value's range and
-    /// type.
-    utils::Result<uint32_t> LocationAttribute(const ast::LocationAttribute* attr);
+    sem::Parameter* Parameter(const ast::Parameter* param,
+                              const ast::Function* func,
+                              uint32_t index);
 
     /// Records the address space usage for the given type, and any transient
     /// dependencies of the type. Validates that the type can be used for the
@@ -497,6 +529,11 @@
                                            const ResolvedIdentifier& resolved,
                                            std::string_view wanted);
 
+    /// Raises an error that the attribute is not valid for the given use.
+    /// @param attr the invalue attribute
+    /// @param use the thing that the attribute was applied to
+    void ErrorInvalidAttribute(const ast::Attribute* attr, std::string_view use);
+
     /// Adds the given error message to the diagnostics
     void AddError(const std::string& msg, const Source& source) const;
 
@@ -523,7 +560,7 @@
     // It is a tuple of the structure type, number of arguments provided and earliest evaluation
     // stage.
     using StructConstructorSig =
-        utils::UnorderedKeyWrapper<std::tuple<const sem::Struct*, size_t, sem::EvaluationStage>>;
+        utils::UnorderedKeyWrapper<std::tuple<const type::Struct*, size_t, sem::EvaluationStage>>;
 
     /// ExprEvalStageConstraint describes a constraint on when expressions can be evaluated.
     struct ExprEvalStageConstraint {
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index 5df2fae..dfe42ae 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -1265,7 +1265,7 @@
     EXPECT_TRUE(sma->Member()->Type()->Is<type::F32>());
     EXPECT_EQ(sma->Object()->Declaration(), mem->object);
     EXPECT_EQ(sma->Member()->Index(), 1u);
-    EXPECT_EQ(sma->Member()->Declaration()->name->symbol, Symbols().Get("second_member"));
+    EXPECT_EQ(sma->Member()->Name().Name(), "second_member");
 }
 
 TEST_F(ResolverTest, Expr_MemberAccessor_Struct_Alias) {
diff --git a/src/tint/resolver/struct_address_space_use_test.cc b/src/tint/resolver/struct_address_space_use_test.cc
index aeb5c31..e97a6ac 100644
--- a/src/tint/resolver/struct_address_space_use_test.cc
+++ b/src/tint/resolver/struct_address_space_use_test.cc
@@ -32,7 +32,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_TRUE(sem->AddressSpaceUsage().empty());
 }
@@ -44,7 +44,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kUndefined));
 }
@@ -56,7 +56,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kUndefined));
 }
@@ -68,7 +68,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kPrivate));
 }
@@ -80,7 +80,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kPrivate));
 }
@@ -92,7 +92,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kPrivate));
 }
@@ -104,7 +104,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kPrivate));
 }
@@ -116,7 +116,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kFunction));
 }
@@ -128,7 +128,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kFunction));
 }
@@ -140,7 +140,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kFunction));
 }
@@ -152,7 +152,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kFunction));
 }
@@ -166,7 +166,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->AddressSpaceUsage(), UnorderedElementsAre(builtin::AddressSpace::kUniform,
                                                                builtin::AddressSpace::kStorage,
diff --git a/src/tint/resolver/struct_pipeline_stage_use_test.cc b/src/tint/resolver/struct_pipeline_stage_use_test.cc
index 4707650..b4857f0 100644
--- a/src/tint/resolver/struct_pipeline_stage_use_test.cc
+++ b/src/tint/resolver/struct_pipeline_stage_use_test.cc
@@ -34,7 +34,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_TRUE(sem->PipelineStageUses().empty());
 }
@@ -46,7 +46,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_TRUE(sem->PipelineStageUses().empty());
 }
@@ -59,7 +59,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_TRUE(sem->PipelineStageUses().empty());
 }
@@ -74,7 +74,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kVertexInput));
@@ -90,7 +90,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kVertexOutput));
@@ -104,7 +104,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kFragmentInput));
@@ -118,7 +118,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kFragmentOutput));
@@ -135,7 +135,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kComputeInput));
@@ -154,7 +154,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kVertexOutput,
@@ -170,7 +170,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kFragmentInput));
@@ -184,10 +184,10 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     ASSERT_EQ(1u, sem->Members().Length());
-    EXPECT_EQ(3u, sem->Members()[0]->Location());
+    EXPECT_EQ(3u, sem->Members()[0]->Attributes().location);
 }
 
 TEST_F(ResolverPipelineStageUseTest, StructUsedAsShaderReturnTypeViaAlias) {
@@ -200,7 +200,7 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     EXPECT_THAT(sem->PipelineStageUses(),
                 UnorderedElementsAre(type::PipelineStageUsage::kFragmentOutput));
@@ -214,10 +214,10 @@
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-    auto* sem = TypeOf(s)->As<sem::Struct>();
+    auto* sem = TypeOf(s)->As<type::Struct>();
     ASSERT_NE(sem, nullptr);
     ASSERT_EQ(1u, sem->Members().Length());
-    EXPECT_EQ(3u, sem->Members()[0]->Location());
+    EXPECT_EQ(3u, sem->Members()[0]->Attributes().location);
 }
 
 }  // namespace
diff --git a/src/tint/resolver/unresolved_identifier_test.cc b/src/tint/resolver/unresolved_identifier_test.cc
index e52b858..5802005 100644
--- a/src/tint/resolver/unresolved_identifier_test.cc
+++ b/src/tint/resolver/unresolved_identifier_test.cc
@@ -43,7 +43,7 @@
     Func("f",
          utils::Vector{
              Param("p", ty.i32(), utils::Vector{Builtin(Expr(Source{{12, 34}}, "positon"))})},
-         ty.void_(), utils::Empty);
+         ty.void_(), utils::Empty, utils::Vector{Stage(ast::PipelineStage::kVertex)});
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: unresolved builtin value 'positon'
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 97eae88..63f2b4d 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -200,7 +200,7 @@
 // https://gpuweb.github.io/gpuweb/wgsl/#plain-types-section
 bool Validator::IsPlain(const type::Type* type) const {
     return type->is_scalar() ||
-           type->IsAnyOf<type::Atomic, type::Vector, type::Matrix, type::Array, sem::Struct>();
+           type->IsAnyOf<type::Atomic, type::Vector, type::Matrix, type::Array, type::Struct>();
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#fixed-footprint-types
@@ -214,7 +214,7 @@
             return !arr->Count()->Is<type::RuntimeArrayCount>() &&
                    IsFixedFootprint(arr->ElemType());
         },
-        [&](const sem::Struct* str) {
+        [&](const type::Struct* str) {
             for (auto* member : str->Members()) {
                 if (!IsFixedFootprint(member->Type())) {
                     return false;
@@ -235,7 +235,7 @@
         [&](const type::Vector* vec) { return IsHostShareable(vec->type()); },
         [&](const type::Matrix* mat) { return IsHostShareable(mat->type()); },
         [&](const type::Array* arr) { return IsHostShareable(arr->ElemType()); },
-        [&](const sem::Struct* str) {
+        [&](const type::Struct* str) {
             for (auto* member : str->Members()) {
                 if (!IsHostShareable(member->Type())) {
                     return false;
@@ -397,11 +397,11 @@
 
     auto is_uniform_struct_or_array = [address_space](const type::Type* ty) {
         return address_space == builtin::AddressSpace::kUniform &&
-               ty->IsAnyOf<type::Array, sem::Struct>();
+               ty->IsAnyOf<type::Array, type::Struct>();
     };
 
     auto is_uniform_struct = [address_space](const type::Type* ty) {
-        return address_space == builtin::AddressSpace::kUniform && ty->Is<sem::Struct>();
+        return address_space == builtin::AddressSpace::kUniform && ty->Is<type::Struct>();
     };
 
     auto required_alignment_of = [&](const type::Type* ty) {
@@ -413,7 +413,7 @@
         return required_align;
     };
 
-    auto member_name_of = [](const sem::StructMember* sm) { return sm->Name().Name(); };
+    auto member_name_of = [](const type::StructMember* sm) { return sm->Name().Name(); };
 
     // Only validate the [type + address space] once
     if (!valid_type_storage_layouts_.Add(TypeAndAddressSpace{store_ty, address_space})) {
@@ -445,7 +445,7 @@
 
             // Recurse into the member type.
             if (!AddressSpaceLayout(m->Type(), address_space, m->Declaration()->type->source)) {
-                AddNote("see layout of struct:\n" + str->Layout(), str->Source());
+                AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
                 note_usage();
                 return false;
             }
@@ -461,13 +461,13 @@
                              "' is currently at offset " + std::to_string(m->Offset()) +
                              ". Consider setting @align(" + std::to_string(required_align) +
                              ") on this member",
-                         m->Source());
+                         m->Declaration()->source);
 
-                AddNote("see layout of struct:\n" + str->Layout(), str->Source());
+                AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
 
                 if (auto* member_str = m->Type()->As<sem::Struct>()) {
                     AddNote("and layout of struct member:\n" + member_str->Layout(),
-                            member_str->Source());
+                            member_str->Declaration()->source);
                 }
 
                 note_usage();
@@ -483,19 +483,19 @@
                     !enabled_extensions_.Contains(
                         builtin::Extension::kChromiumInternalRelaxedUniformLayout)) {
                     AddError(
-                        "uniform storage requires that the number of bytes between the "
-                        "start of the previous member of type struct and the current "
-                        "member be a multiple of 16 bytes, but there are currently " +
+                        "uniform storage requires that the number of bytes between the start of "
+                        "the previous member of type struct and the current member be a multiple "
+                        "of 16 bytes, but there are currently " +
                             std::to_string(prev_to_curr_offset) + " bytes between '" +
                             member_name_of(prev_member) + "' and '" + member_name_of(m) +
                             "'. Consider setting @align(16) on this member",
-                        m->Source());
+                        m->Declaration()->source);
 
-                    AddNote("see layout of struct:\n" + str->Layout(), str->Source());
+                    AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
 
                     auto* prev_member_str = prev_member->Type()->As<sem::Struct>();
                     AddNote("and layout of previous member struct:\n" + prev_member_str->Layout(),
-                            prev_member_str->Source());
+                            prev_member_str->Declaration()->source);
                     note_usage();
                     return false;
                 }
@@ -606,32 +606,10 @@
                 return false;
             }
 
-            for (auto* attr : decl->attributes) {
-                bool is_shader_io_attribute =
-                    attr->IsAnyOf<ast::BuiltinAttribute, ast::InterpolateAttribute,
-                                  ast::InvariantAttribute, ast::LocationAttribute>();
-                bool has_io_address_space = global->AddressSpace() == builtin::AddressSpace::kIn ||
-                                            global->AddressSpace() == builtin::AddressSpace::kOut;
-                if (!attr->IsAnyOf<ast::BindingAttribute, ast::GroupAttribute,
-                                   ast::InternalAttribute>() &&
-                    (!is_shader_io_attribute || !has_io_address_space)) {
-                    AddError("attribute '" + attr->Name() + "' is not valid for module-scope 'var'",
-                             attr->source);
-                    return false;
-                }
-            }
-
             return Var(global);
         },
         [&](const ast::Override*) { return Override(global, override_ids); },
-        [&](const ast::Const*) {
-            if (!decl->attributes.IsEmpty()) {
-                AddError("attribute is not valid for module-scope 'const' declaration",
-                         decl->attributes[0]->source);
-                return false;
-            }
-            return Const(global);
-        },
+        [&](const ast::Const*) { return Const(global); },
         [&](Default) {
             TINT_ICE(Resolver, diagnostics_)
                 << "Validator::GlobalVariable() called with a unknown variable type: "
@@ -773,9 +751,6 @@
                     ast::GetAttribute<ast::IdAttribute>((*var)->Declaration()->attributes)->source);
                 return false;
             }
-        } else {
-            AddError("attribute is not valid for 'override' declaration", attr->source);
-            return false;
         }
     }
 
@@ -792,28 +767,13 @@
     return true;
 }
 
-bool Validator::Parameter(const ast::Function* func, const sem::Variable* var) const {
+bool Validator::Parameter(const sem::Variable* var) const {
     auto* decl = var->Declaration();
 
     if (IsValidationDisabled(decl->attributes, ast::DisabledValidation::kFunctionParameter)) {
         return true;
     }
 
-    for (auto* attr : decl->attributes) {
-        if (!func->IsEntryPoint() && !attr->Is<ast::InternalAttribute>()) {
-            AddError("attribute is not valid for non-entry point function parameters",
-                     attr->source);
-            return false;
-        }
-        if (!attr->IsAnyOf<ast::BuiltinAttribute, ast::InvariantAttribute, ast::LocationAttribute,
-                           ast::InterpolateAttribute, ast::InternalAttribute>() &&
-            (IsValidationEnabled(decl->attributes,
-                                 ast::DisabledValidation::kEntryPointParameter))) {
-            AddError("attribute is not valid for function parameters", attr->source);
-            return false;
-        }
-    }
-
     if (auto* ref = var->Type()->As<type::Pointer>()) {
         if (IsValidationEnabled(decl->attributes, ast::DisabledValidation::kIgnoreAddressSpace)) {
             bool ok = false;
@@ -1028,14 +988,7 @@
                 }
                 return true;
             },
-            [&](Default) {
-                if (!attr->IsAnyOf<ast::DiagnosticAttribute, ast::StageAttribute,
-                                   ast::InternalAttribute>()) {
-                    AddError("attribute is not valid for functions", attr->source);
-                    return false;
-                }
-                return true;
-            });
+            [&](Default) { return true; });
         if (!ok) {
             return false;
         }
@@ -1069,24 +1022,6 @@
             TINT_ICE(Resolver, diagnostics_)
                 << "Function " << decl->name->symbol.Name() << " has no body";
         }
-
-        for (auto* attr : decl->return_type_attributes) {
-            if (!decl->IsEntryPoint()) {
-                AddError("attribute is not valid for non-entry point function return types",
-                         attr->source);
-                return false;
-            }
-            if (!attr->IsAnyOf<ast::BuiltinAttribute, ast::InternalAttribute,
-                               ast::LocationAttribute, ast::InterpolateAttribute,
-                               ast::InvariantAttribute>() &&
-                (IsValidationEnabled(decl->attributes,
-                                     ast::DisabledValidation::kEntryPointParameter) &&
-                 IsValidationEnabled(decl->attributes,
-                                     ast::DisabledValidation::kFunctionParameter))) {
-                AddError("attribute is not valid for entry point return types", attr->source);
-                return false;
-            }
-        }
     }
 
     if (decl->IsEntryPoint()) {
@@ -1196,19 +1131,19 @@
             if (is_invalid_compute_shader_attribute) {
                 std::string input_or_output =
                     param_or_ret == ParamOrRetType::kParameter ? "inputs" : "output";
-                AddError("attribute is not valid for compute shader " + input_or_output,
+                AddError("@" + attr->Name() + " is not valid for compute shader " + input_or_output,
                          attr->source);
                 return false;
             }
         }
 
         if (IsValidationEnabled(attrs, ast::DisabledValidation::kEntryPointParameter)) {
-            if (is_struct_member && ty->Is<sem::Struct>()) {
+            if (is_struct_member && ty->Is<type::Struct>()) {
                 AddError("nested structures cannot be used for entry point IO", source);
                 return false;
             }
 
-            if (!ty->Is<sem::Struct>() && !pipeline_io_attribute) {
+            if (!ty->Is<type::Struct>() && !pipeline_io_attribute) {
                 std::string err = "missing entry point IO attribute";
                 if (!is_struct_member) {
                     err += (param_or_ret == ParamOrRetType::kParameter ? " on parameter"
@@ -1278,9 +1213,9 @@
             if (auto* str = ty->As<sem::Struct>()) {
                 for (auto* member : str->Members()) {
                     if (!validate_entry_point_attributes_inner(
-                            member->Declaration()->attributes, member->Type(), member->Source(),
-                            param_or_ret,
-                            /*is_struct_member*/ true, member->Location())) {
+                            member->Declaration()->attributes, member->Type(),
+                            member->Declaration()->source, param_or_ret,
+                            /*is_struct_member*/ true, member->Attributes().location)) {
                         AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
                                 decl->source);
                         return false;
@@ -1828,7 +1763,7 @@
 }
 
 bool Validator::StructureInitializer(const ast::CallExpression* ctor,
-                                     const sem::Struct* struct_type) const {
+                                     const type::Struct* struct_type) const {
     if (!struct_type->IsConstructible()) {
         AddError("structure constructor has non-constructible type", ctor->source);
         return false;
@@ -2131,7 +2066,7 @@
 
 bool Validator::Structure(const sem::Struct* str, ast::PipelineStage stage) const {
     if (str->Members().IsEmpty()) {
-        AddError("structures must have at least one member", str->Source());
+        AddError("structures must have at least one member", str->Declaration()->source);
         return false;
     }
 
@@ -2141,7 +2076,7 @@
             if (r->Count()->Is<type::RuntimeArrayCount>()) {
                 if (member != str->Members().Back()) {
                     AddError("runtime arrays may only appear as the last member of a struct",
-                             member->Source());
+                             member->Declaration()->source);
                     return false;
                 }
             }
@@ -2153,7 +2088,7 @@
         } else if (!IsFixedFootprint(member->Type())) {
             AddError(
                 "a struct that contains a runtime array cannot be nested inside another struct",
-                member->Source());
+                member->Declaration()->source);
             return false;
         }
 
@@ -2170,9 +2105,10 @@
                 },
                 [&](const ast::LocationAttribute* location) {
                     has_location = true;
-                    TINT_ASSERT(Resolver, member->Location().has_value());
-                    if (!LocationAttribute(location, member->Location().value(), member->Type(),
-                                           locations, stage, member->Source())) {
+                    TINT_ASSERT(Resolver, member->Attributes().location.has_value());
+                    if (!LocationAttribute(location, member->Attributes().location.value(),
+                                           member->Type(), locations, stage,
+                                           member->Declaration()->source)) {
                         return false;
                     }
                     return true;
@@ -2205,24 +2141,7 @@
                     }
                     return true;
                 },
-                [&](Default) {
-                    if (!attr->IsAnyOf<ast::BuiltinAttribute,             //
-                                       ast::InternalAttribute,            //
-                                       ast::InterpolateAttribute,         //
-                                       ast::InvariantAttribute,           //
-                                       ast::LocationAttribute,            //
-                                       ast::StructMemberOffsetAttribute,  //
-                                       ast::StructMemberAlignAttribute>()) {
-                        if (attr->Is<ast::StrideAttribute>() &&
-                            IsValidationDisabled(member->Declaration()->attributes,
-                                                 ast::DisabledValidation::kIgnoreStrideAttribute)) {
-                            return true;
-                        }
-                        AddError("attribute is not valid for structure members", attr->source);
-                        return false;
-                    }
-                    return true;
-                });
+                [&](Default) { return true; });
             if (!ok) {
                 return false;
             }
@@ -2241,13 +2160,6 @@
         }
     }
 
-    for (auto* attr : str->Declaration()->attributes) {
-        if (!(attr->IsAnyOf<ast::InternalAttribute>())) {
-            AddError("attribute is not valid for struct declarations", attr->source);
-            return false;
-        }
-    }
-
     return true;
 }
 
@@ -2260,7 +2172,8 @@
                                   const bool is_input) const {
     std::string inputs_or_output = is_input ? "inputs" : "output";
     if (stage == ast::PipelineStage::kCompute) {
-        AddError("attribute is not valid for compute shader " + inputs_or_output, loc_attr->source);
+        AddError("@" + loc_attr->Name() + " is not valid for compute shader " + inputs_or_output,
+                 loc_attr->source);
         return false;
     }
 
@@ -2652,8 +2565,8 @@
             }
             return true;
         },
-        [&](const sem::Struct*) { return check_sub_atomics(); },  //
-        [&](const type::Array*) { return check_sub_atomics(); },  //
+        [&](const type::Struct*) { return check_sub_atomics(); },  //
+        [&](const type::Array*) { return check_sub_atomics(); },   //
         [&](Default) { return true; });
 }
 
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index e0e3051..999bc49 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -348,10 +348,9 @@
     bool Matrix(const type::Type* el_ty, const Source& source) const;
 
     /// Validates a function parameter
-    /// @param func the function the variable is for
     /// @param var the variable to validate
     /// @returns true on success, false otherwise
-    bool Parameter(const ast::Function* func, const sem::Variable* var) const;
+    bool Parameter(const sem::Variable* var) const;
 
     /// Validates a return
     /// @param ret the return statement to validate
@@ -398,7 +397,7 @@
     /// @param struct_type the type of the structure
     /// @returns true on success, false otherwise
     bool StructureInitializer(const ast::CallExpression* ctor,
-                              const sem::Struct* struct_type) const;
+                              const type::Struct* struct_type) const;
 
     /// Validates a switch statement
     /// @param s the switch to validate
diff --git a/src/tint/resolver/variable_test.cc b/src/tint/resolver/variable_test.cc
index 7af6027..5daee08 100644
--- a/src/tint/resolver/variable_test.cc
+++ b/src/tint/resolver/variable_test.cc
@@ -81,8 +81,8 @@
     EXPECT_TRUE(TypeOf(f)->As<type::Reference>()->StoreType()->Is<type::F32>());
     EXPECT_TRUE(TypeOf(h)->As<type::Reference>()->StoreType()->Is<type::F16>());
     EXPECT_TRUE(TypeOf(b)->As<type::Reference>()->StoreType()->Is<type::Bool>());
-    EXPECT_TRUE(TypeOf(s)->As<type::Reference>()->StoreType()->Is<sem::Struct>());
-    EXPECT_TRUE(TypeOf(a)->As<type::Reference>()->StoreType()->Is<sem::Struct>());
+    EXPECT_TRUE(TypeOf(s)->As<type::Reference>()->StoreType()->Is<type::Struct>());
+    EXPECT_TRUE(TypeOf(a)->As<type::Reference>()->StoreType()->Is<type::Struct>());
 
     EXPECT_EQ(Sem().Get(i)->Initializer(), nullptr);
     EXPECT_EQ(Sem().Get(u)->Initializer(), nullptr);
@@ -161,8 +161,8 @@
     EXPECT_TRUE(TypeOf(f)->As<type::Reference>()->StoreType()->Is<type::F32>());
     EXPECT_TRUE(TypeOf(h)->As<type::Reference>()->StoreType()->Is<type::F16>());
     EXPECT_TRUE(TypeOf(b)->As<type::Reference>()->StoreType()->Is<type::Bool>());
-    EXPECT_TRUE(TypeOf(s)->As<type::Reference>()->StoreType()->Is<sem::Struct>());
-    EXPECT_TRUE(TypeOf(a)->As<type::Reference>()->StoreType()->Is<sem::Struct>());
+    EXPECT_TRUE(TypeOf(s)->As<type::Reference>()->StoreType()->Is<type::Struct>());
+    EXPECT_TRUE(TypeOf(a)->As<type::Reference>()->StoreType()->Is<type::Struct>());
 
     EXPECT_EQ(Sem().Get(i)->Initializer()->Declaration(), i_c);
     EXPECT_EQ(Sem().Get(u)->Initializer()->Declaration(), u_c);
@@ -383,7 +383,7 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
-// Function-scope 'let'
+// 'let' declaration
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 TEST_F(ResolverVariableTest, LocalLet) {
     // struct S { i : i32; }
@@ -444,8 +444,8 @@
     ASSERT_TRUE(TypeOf(f)->Is<type::F32>());
     ASSERT_TRUE(TypeOf(h)->Is<type::F16>());
     ASSERT_TRUE(TypeOf(b)->Is<type::Bool>());
-    ASSERT_TRUE(TypeOf(s)->Is<sem::Struct>());
-    ASSERT_TRUE(TypeOf(a)->Is<sem::Struct>());
+    ASSERT_TRUE(TypeOf(s)->Is<type::Struct>());
+    ASSERT_TRUE(TypeOf(a)->Is<type::Struct>());
     ASSERT_TRUE(TypeOf(p)->Is<type::Pointer>());
     ASSERT_TRUE(TypeOf(p)->As<type::Pointer>()->StoreType()->Is<type::I32>());
 
@@ -924,7 +924,7 @@
     ASSERT_TRUE(TypeOf(c_vu32)->Is<type::Vector>());
     ASSERT_TRUE(TypeOf(c_vf32)->Is<type::Vector>());
     ASSERT_TRUE(TypeOf(c_mf32)->Is<type::Matrix>());
-    ASSERT_TRUE(TypeOf(c_s)->Is<sem::Struct>());
+    ASSERT_TRUE(TypeOf(c_s)->Is<type::Struct>());
 
     EXPECT_TRUE(Sem().Get(c_i32)->ConstantValue()->AllZero());
     EXPECT_TRUE(Sem().Get(c_u32)->ConstantValue()->AllZero());
@@ -987,7 +987,7 @@
     ASSERT_TRUE(TypeOf(c_vaf)->Is<type::Vector>());
     ASSERT_TRUE(TypeOf(c_mf32)->Is<type::Matrix>());
     ASSERT_TRUE(TypeOf(c_maf32)->Is<type::Matrix>());
-    ASSERT_TRUE(TypeOf(c_s)->Is<sem::Struct>());
+    ASSERT_TRUE(TypeOf(c_s)->Is<type::Struct>());
 
     EXPECT_TRUE(Sem().Get(c_i32)->ConstantValue()->AllZero());
     EXPECT_TRUE(Sem().Get(c_u32)->ConstantValue()->AllZero());
diff --git a/src/tint/sem/member_accessor_expression.cc b/src/tint/sem/member_accessor_expression.cc
index 9ad15f0..573f670 100644
--- a/src/tint/sem/member_accessor_expression.cc
+++ b/src/tint/sem/member_accessor_expression.cc
@@ -41,7 +41,7 @@
                                        const Statement* statement,
                                        const constant::Value* constant,
                                        const ValueExpression* object,
-                                       const StructMember* member,
+                                       const type::StructMember* member,
                                        bool has_side_effects,
                                        const Variable* root_ident /* = nullptr */)
     : Base(declaration,
diff --git a/src/tint/sem/member_accessor_expression.h b/src/tint/sem/member_accessor_expression.h
index cea9f2d..b2abdec 100644
--- a/src/tint/sem/member_accessor_expression.h
+++ b/src/tint/sem/member_accessor_expression.h
@@ -22,9 +22,9 @@
 namespace tint::ast {
 class MemberAccessorExpression;
 }  // namespace tint::ast
-namespace tint::sem {
+namespace tint::type {
 class StructMember;
-}  // namespace tint::sem
+}  // namespace tint::type
 
 namespace tint::sem {
 
@@ -81,7 +81,7 @@
                        const Statement* statement,
                        const constant::Value* constant,
                        const ValueExpression* object,
-                       const StructMember* member,
+                       const type::StructMember* member,
                        bool has_side_effects,
                        const Variable* root_ident = nullptr);
 
@@ -89,10 +89,10 @@
     ~StructMemberAccess() override;
 
     /// @returns the structure member
-    StructMember const* Member() const { return member_; }
+    type::StructMember const* Member() const { return member_; }
 
   private:
-    StructMember const* const member_;
+    type::StructMember const* const member_;
 };
 
 /// Swizzle holds the semantic information for a ast::MemberAccessorExpression
diff --git a/src/tint/sem/struct.cc b/src/tint/sem/struct.cc
index 413231a..e6c0e73 100644
--- a/src/tint/sem/struct.cc
+++ b/src/tint/sem/struct.cc
@@ -22,26 +22,28 @@
 namespace tint::sem {
 
 Struct::Struct(const ast::Struct* declaration,
-               tint::Source source,
                Symbol name,
                utils::VectorRef<const StructMember*> members,
                uint32_t align,
                uint32_t size,
                uint32_t size_no_padding)
-    : Base(source, name, members, align, size, size_no_padding), declaration_(declaration) {}
+    : Base(name, members, align, size, size_no_padding), declaration_(declaration) {
+    TINT_ASSERT(Semantic, declaration != nullptr);
+}
 
 Struct::~Struct() = default;
 
 StructMember::StructMember(const ast::StructMember* declaration,
-                           tint::Source source,
                            Symbol name,
                            const type::Type* type,
                            uint32_t index,
                            uint32_t offset,
                            uint32_t align,
                            uint32_t size,
-                           std::optional<uint32_t> location)
-    : Base(source, name, type, index, offset, align, size, location), declaration_(declaration) {}
+                           const type::StructMemberAttributes& attributes)
+    : Base(name, type, index, offset, align, size, attributes), declaration_(declaration) {
+    TINT_ASSERT(Semantic, declaration != nullptr);
+}
 
 StructMember::~StructMember() = default;
 
diff --git a/src/tint/sem/struct.h b/src/tint/sem/struct.h
index 2b437af..fd59dcd 100644
--- a/src/tint/sem/struct.h
+++ b/src/tint/sem/struct.h
@@ -38,19 +38,17 @@
 namespace tint::sem {
 
 /// Struct holds the semantic information for structures.
+/// Unlike type::Struct, sem::Struct has an AST declaration node.
 class Struct final : public utils::Castable<Struct, type::Struct> {
   public:
     /// Constructor
     /// @param declaration the AST structure declaration
-    /// @param source the source of the structure
     /// @param name the name of the structure
     /// @param members the structure members
     /// @param align the byte alignment of the structure
     /// @param size the byte size of the structure
-    /// @param size_no_padding size of the members without the end of structure
-    /// alignment padding
+    /// @param size_no_padding size of the members without the end of structure alignment padding
     Struct(const ast::Struct* declaration,
-           tint::Source source,
            Symbol name,
            utils::VectorRef<const StructMember*> members,
            uint32_t align,
@@ -73,27 +71,26 @@
 };
 
 /// StructMember holds the semantic information for structure members.
+/// Unlike type::StructMember, sem::StructMember has an AST declaration node.
 class StructMember final : public utils::Castable<StructMember, type::StructMember> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
-    /// @param source the source of the struct member
     /// @param name the name of the structure member
     /// @param type the type of the member
     /// @param index the index of the member in the structure
     /// @param offset the byte offset from the base of the structure
     /// @param align the byte alignment of the member
     /// @param size the byte size of the member
-    /// @param location the location attribute, if present
+    /// @param attributes the optional attributes
     StructMember(const ast::StructMember* declaration,
-                 tint::Source source,
                  Symbol name,
                  const type::Type* type,
                  uint32_t index,
                  uint32_t offset,
                  uint32_t align,
                  uint32_t size,
-                 std::optional<uint32_t> location);
+                 const type::StructMemberAttributes& attributes);
 
     /// Destructor
     ~StructMember() override;
diff --git a/src/tint/sem/struct_test.cc b/src/tint/sem/struct_test.cc
index 424930d..c267234 100644
--- a/src/tint/sem/struct_test.cc
+++ b/src/tint/sem/struct_test.cc
@@ -26,8 +26,8 @@
     auto name = Sym("S");
     auto* impl = create<ast::Struct>(Ident(name), utils::Empty, utils::Empty);
     auto* ptr = impl;
-    auto* s = create<sem::Struct>(impl, impl->source, impl->name->symbol, utils::Empty,
-                                  4u /* align */, 8u /* size */, 16u /* size_no_padding */);
+    auto* s = create<sem::Struct>(impl, impl->name->symbol, utils::Empty, 4u /* align */,
+                                  8u /* size */, 16u /* size_no_padding */);
     EXPECT_EQ(s->Declaration(), ptr);
     EXPECT_EQ(s->Align(), 4u);
     EXPECT_EQ(s->Size(), 8u);
@@ -36,11 +36,11 @@
 
 TEST_F(SemStructTest, Equals) {
     auto* a_impl = create<ast::Struct>(Ident("a"), utils::Empty, utils::Empty);
-    auto* a = create<sem::Struct>(a_impl, a_impl->source, a_impl->name->symbol, utils::Empty,
-                                  4u /* align */, 4u /* size */, 4u /* size_no_padding */);
+    auto* a = create<sem::Struct>(a_impl, a_impl->name->symbol, utils::Empty, 4u /* align */,
+                                  4u /* size */, 4u /* size_no_padding */);
     auto* b_impl = create<ast::Struct>(Ident("b"), utils::Empty, utils::Empty);
-    auto* b = create<sem::Struct>(b_impl, b_impl->source, b_impl->name->symbol, utils::Empty,
-                                  4u /* align */, 4u /* size */, 4u /* size_no_padding */);
+    auto* b = create<sem::Struct>(b_impl, b_impl->name->symbol, utils::Empty, 4u /* align */,
+                                  4u /* size */, 4u /* size_no_padding */);
 
     EXPECT_TRUE(a->Equals(*a));
     EXPECT_FALSE(a->Equals(*b));
@@ -50,8 +50,8 @@
 TEST_F(SemStructTest, FriendlyName) {
     auto name = Sym("my_struct");
     auto* impl = create<ast::Struct>(Ident(name), utils::Empty, utils::Empty);
-    auto* s = create<sem::Struct>(impl, impl->source, impl->name->symbol, utils::Empty,
-                                  4u /* align */, 4u /* size */, 4u /* size_no_padding */);
+    auto* s = create<sem::Struct>(impl, impl->name->symbol, utils::Empty, 4u /* align */,
+                                  4u /* size */, 4u /* size_no_padding */);
     EXPECT_EQ(s->FriendlyName(), "my_struct");
 }
 
diff --git a/src/tint/templates/enums.tmpl.inc b/src/tint/templates/enums.tmpl.inc
index ad46942..adc06e0 100644
--- a/src/tint/templates/enums.tmpl.inc
+++ b/src/tint/templates/enums.tmpl.inc
@@ -191,7 +191,7 @@
             benchmark::DoNotOptimize(result);
         }
     }
-}
+} // NOLINT(readability/fn_size)
 
 BENCHMARK({{$enum}}Parser);
 {{- end -}}
diff --git a/src/tint/transform/array_length_from_uniform.cc b/src/tint/transform/array_length_from_uniform.cc
index 98d4a6b..1c88483 100644
--- a/src/tint/transform/array_length_from_uniform.cc
+++ b/src/tint/transform/array_length_from_uniform.cc
@@ -147,7 +147,7 @@
             const ast::Expression* total_size = total_storage_buffer_size;
             auto* storage_buffer_type = storage_buffer_sem->Type()->UnwrapRef();
             const type::Array* array_type = nullptr;
-            if (auto* str = storage_buffer_type->As<sem::Struct>()) {
+            if (auto* str = storage_buffer_type->As<type::Struct>()) {
                 // The variable is a struct, so subtract the byte offset of the array
                 // member.
                 auto* array_member_sem = str->Members().Back();
diff --git a/src/tint/transform/calculate_array_length.cc b/src/tint/transform/calculate_array_length.cc
index 743a992..4a3598d 100644
--- a/src/tint/transform/calculate_array_length.cc
+++ b/src/tint/transform/calculate_array_length.cc
@@ -204,7 +204,7 @@
 
                             const type::Array* array_type = Switch(
                                 storage_buffer_type->StoreType(),
-                                [&](const sem::Struct* str) {
+                                [&](const type::Struct* str) {
                                     // The variable is a struct, so subtract the byte offset of
                                     // the array member.
                                     auto* array_member_sem = str->Members().Back();
diff --git a/src/tint/transform/canonicalize_entry_point_io.cc b/src/tint/transform/canonicalize_entry_point_io.cc
index 840f8ff..9e9368a 100644
--- a/src/tint/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/transform/canonicalize_entry_point_io.cc
@@ -371,7 +371,7 @@
         // list to pass them through to the inner function.
         utils::Vector<const ast::Expression*, 8> inner_struct_values;
         for (auto* member : str->Members()) {
-            if (TINT_UNLIKELY(member->Type()->Is<sem::Struct>())) {
+            if (TINT_UNLIKELY(member->Type()->Is<type::Struct>())) {
                 TINT_ICE(Transform, ctx.dst->Diagnostics()) << "nested IO struct";
                 continue;
             }
@@ -380,8 +380,8 @@
 
             auto attributes =
                 CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate);
-            auto* input_expr =
-                AddInput(name, member->Type(), member->Location(), std::move(attributes));
+            auto* input_expr = AddInput(name, member->Type(), member->Attributes().location,
+                                        std::move(attributes));
             inner_struct_values.Push(input_expr);
         }
 
@@ -400,7 +400,7 @@
         bool do_interpolate = func_ast->PipelineStage() != ast::PipelineStage::kFragment;
         if (auto* str = inner_ret_type->As<sem::Struct>()) {
             for (auto* member : str->Members()) {
-                if (TINT_UNLIKELY(member->Type()->Is<sem::Struct>())) {
+                if (TINT_UNLIKELY(member->Type()->Is<type::Struct>())) {
                     TINT_ICE(Transform, ctx.dst->Diagnostics()) << "nested IO struct";
                     continue;
                 }
@@ -410,8 +410,8 @@
                     CloneShaderIOAttributes(member->Declaration()->attributes, do_interpolate);
 
                 // Extract the original structure member.
-                AddOutput(name, member->Type(), member->Location(), std::move(attributes),
-                          ctx.dst->MemberAccessor(original_result, name));
+                AddOutput(name, member->Type(), member->Attributes().location,
+                          std::move(attributes), ctx.dst->MemberAccessor(original_result, name));
             }
         } else if (!inner_ret_type->Is<type::Void>()) {
             auto attributes =
@@ -639,7 +639,7 @@
         // aggregated into a single structure.
         if (!func_sem->Parameters().IsEmpty()) {
             for (auto* param : func_sem->Parameters()) {
-                if (param->Type()->Is<sem::Struct>()) {
+                if (param->Type()->Is<type::Struct>()) {
                     ProcessStructParameter(param);
                 } else {
                     ProcessNonStructParameter(param);
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index 564451c..181518d 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -522,7 +522,7 @@
                         auto* offset = b.Add("offset", u32(i * mat_ty->ColumnStride()));
                         values.Push(b.Call(load, offset));
                     }
-                } else if (auto* str = el_ty->As<sem::Struct>()) {
+                } else if (auto* str = el_ty->As<type::Struct>()) {
                     for (auto* member : str->Members()) {
                         auto* offset = b.Add("offset", u32(member->Offset()));
                         Symbol load = LoadFunc(member->Type()->UnwrapRef(), address_space, buffer);
@@ -607,7 +607,7 @@
                         }
                         return stmts;
                     },
-                    [&](const sem::Struct* str) {
+                    [&](const type::Struct* str) {
                         utils::Vector<const ast::Statement*, 8> stmts;
                         for (auto* member : str->Members()) {
                             auto* offset = b.Add("offset", u32(member->Offset()));
@@ -660,8 +660,8 @@
 
             // For intrinsics that return a struct, there is no AST node for it, so create one now.
             if (intrinsic->Type() == builtin::Function::kAtomicCompareExchangeWeak) {
-                auto* str = intrinsic->ReturnType()->As<sem::Struct>();
-                TINT_ASSERT(Transform, str && str->Declaration() == nullptr);
+                auto* str = intrinsic->ReturnType()->As<type::Struct>();
+                TINT_ASSERT(Transform, str);
 
                 utils::Vector<const ast::StructMember*, 8> ast_members;
                 ast_members.Reserve(str->Members().Length());
@@ -869,7 +869,7 @@
                 }
             } else {
                 if (auto access = state.TakeAccess(accessor->object)) {
-                    auto* str_ty = access.type->As<sem::Struct>();
+                    auto* str_ty = access.type->As<type::Struct>();
                     auto* member = str_ty->FindMember(accessor->member->symbol);
                     auto offset = member->Offset();
                     state.AddAccess(accessor, {
diff --git a/src/tint/transform/decompose_strided_matrix.cc b/src/tint/transform/decompose_strided_matrix.cc
index dd03dc2..6443095 100644
--- a/src/tint/transform/decompose_strided_matrix.cc
+++ b/src/tint/transform/decompose_strided_matrix.cc
@@ -70,7 +70,7 @@
     // Scan the program for all storage and uniform structure matrix members with
     // a custom stride attribute. Replace these matrices with an equivalent array,
     // and populate the `decomposed` map with the members that have been replaced.
-    utils::Hashmap<const ast::StructMember*, MatrixInfo, 8> decomposed;
+    utils::Hashmap<const type::StructMember*, MatrixInfo, 8> decomposed;
     for (auto* node : src->ASTNodes().Objects()) {
         if (auto* str = node->As<ast::Struct>()) {
             auto* str_ty = src->Sem().Get(str);
@@ -98,7 +98,7 @@
                 auto* replacement =
                     b.Member(member->Offset(), ctx.Clone(member->Name()), info.array(ctx.dst));
                 ctx.Replace(member->Declaration(), replacement);
-                decomposed.Add(member->Declaration(), info);
+                decomposed.Add(member, info);
             }
         }
     }
@@ -114,7 +114,7 @@
     ctx.ReplaceAll(
         [&](const ast::IndexAccessorExpression* expr) -> const ast::IndexAccessorExpression* {
             if (auto* access = src->Sem().Get<sem::StructMemberAccess>(expr->object)) {
-                if (decomposed.Contains(access->Member()->Declaration())) {
+                if (decomposed.Contains(access->Member())) {
                     auto* obj = ctx.CloneWithoutTransform(expr->object);
                     auto* idx = ctx.Clone(expr->index);
                     return b.IndexAccessor(obj, idx);
@@ -131,7 +131,7 @@
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> mat_to_arr;
     ctx.ReplaceAll([&](const ast::AssignmentStatement* stmt) -> const ast::Statement* {
         if (auto* access = src->Sem().Get<sem::StructMemberAccess>(stmt->lhs)) {
-            if (auto info = decomposed.Find(access->Member()->Declaration())) {
+            if (auto info = decomposed.Find(access->Member())) {
                 auto fn = utils::GetOrCreate(mat_to_arr, *info, [&] {
                     auto name =
                         b.Symbols().New("mat" + std::to_string(info->matrix->columns()) + "x" +
@@ -170,7 +170,7 @@
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> arr_to_mat;
     ctx.ReplaceAll([&](const ast::MemberAccessorExpression* expr) -> const ast::Expression* {
         if (auto* access = src->Sem().Get(expr)->UnwrapLoad()->As<sem::StructMemberAccess>()) {
-            if (auto info = decomposed.Find(access->Member()->Declaration())) {
+            if (auto info = decomposed.Find(access->Member())) {
                 auto fn = utils::GetOrCreate(arr_to_mat, *info, [&] {
                     auto name =
                         b.Symbols().New("arr_to_mat" + std::to_string(info->matrix->columns()) +
diff --git a/src/tint/transform/demote_to_helper.cc b/src/tint/transform/demote_to_helper.cc
index c853ef0..46e9203 100644
--- a/src/tint/transform/demote_to_helper.cc
+++ b/src/tint/transform/demote_to_helper.cc
@@ -186,7 +186,7 @@
                             // original member values over to it.
 
                             // Declare a struct to hold the result values.
-                            auto* result_struct = sem_call->Type()->As<sem::Struct>();
+                            auto* result_struct = sem_call->Type()->As<type::Struct>();
                             auto* atomic_ty = result_struct->Members()[0]->Type();
                             result_ty = b.ty(
                                 utils::GetOrCreate(atomic_cmpxchg_result_types, atomic_ty, [&]() {
diff --git a/src/tint/transform/first_index_offset.cc b/src/tint/transform/first_index_offset.cc
index 59588e2..c8d4a26 100644
--- a/src/tint/transform/first_index_offset.cc
+++ b/src/tint/transform/first_index_offset.cc
@@ -79,7 +79,7 @@
 
     // Map of builtin usages
     std::unordered_map<const sem::Variable*, const char*> builtin_vars;
-    std::unordered_map<const sem::StructMember*, const char*> builtin_members;
+    std::unordered_map<const type::StructMember*, const char*> builtin_members;
 
     bool has_vertex_or_instance_index = false;
 
diff --git a/src/tint/transform/localize_struct_array_assignment.cc b/src/tint/transform/localize_struct_array_assignment.cc
index f5f8744..775da6a 100644
--- a/src/tint/transform/localize_struct_array_assignment.cc
+++ b/src/tint/transform/localize_struct_array_assignment.cc
@@ -60,7 +60,7 @@
                     continue;
                 }
                 auto og = GetOriginatingTypeAndAddressSpace(assign_stmt);
-                if (!(og.first->Is<sem::Struct>() &&
+                if (!(og.first->Is<type::Struct>() &&
                       (og.second == builtin::AddressSpace::kFunction ||
                        og.second == builtin::AddressSpace::kPrivate))) {
                     continue;
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.cc b/src/tint/transform/module_scope_var_to_entry_point_param.cc
index a394ef6..14e16bb 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/transform/module_scope_var_to_entry_point_param.cc
@@ -54,7 +54,7 @@
         return true;
     } else if (auto* ary = type->As<type::Array>()) {
         return ContainsMatrix(ary->ElemType());
-    } else if (auto* str = type->As<sem::Struct>()) {
+    } else if (auto* str = type->As<type::Struct>()) {
         for (auto* member : str->Members()) {
             if (ContainsMatrix(member->Type())) {
                 return true;
@@ -85,11 +85,6 @@
                 return;
             }
 
-            if (!str->Declaration()) {
-                // The struct is a built-in structure that we do not need to declare.
-                return;
-            }
-
             // Recurse into members.
             for (auto* member : str->Members()) {
                 CloneStructTypes(member->Type());
diff --git a/src/tint/transform/num_workgroups_from_uniform.cc b/src/tint/transform/num_workgroups_from_uniform.cc
index 18889f4..60e9a72 100644
--- a/src/tint/transform/num_workgroups_from_uniform.cc
+++ b/src/tint/transform/num_workgroups_from_uniform.cc
@@ -99,10 +99,7 @@
             }
 
             for (auto* member : str->Members()) {
-                auto* builtin =
-                    ast::GetAttribute<ast::BuiltinAttribute>(member->Declaration()->attributes);
-                if (!builtin ||
-                    src->Sem().Get(builtin)->Value() != builtin::BuiltinValue::kNumWorkgroups) {
+                if (member->Attributes().builtin != builtin::BuiltinValue::kNumWorkgroups) {
                     continue;
                 }
 
diff --git a/src/tint/transform/packed_vec3.cc b/src/tint/transform/packed_vec3.cc
index 9ef78a6..16d8af4 100644
--- a/src/tint/transform/packed_vec3.cc
+++ b/src/tint/transform/packed_vec3.cc
@@ -159,7 +159,7 @@
                 }
                 return {};
             },
-            [&](const sem::Struct* str) -> ast::Type {
+            [&](const type::Struct* str) -> ast::Type {
                 if (ContainsVec3(str)) {
                     auto name = rewritten_structs.GetOrCreate(str, [&]() {
                         utils::Vector<const ast::StructMember*, 4> members;
@@ -170,12 +170,14 @@
                                 // Copy the member attributes.
                                 bool needs_align = true;
                                 utils::Vector<const ast::Attribute*, 4> attributes;
-                                for (auto* attr : member->Declaration()->attributes) {
-                                    if (attr->IsAnyOf<ast::StructMemberAlignAttribute,
-                                                      ast::StructMemberOffsetAttribute>()) {
-                                        needs_align = false;
+                                if (auto* sem_mem = member->As<sem::StructMember>()) {
+                                    for (auto* attr : sem_mem->Declaration()->attributes) {
+                                        if (attr->IsAnyOf<ast::StructMemberAlignAttribute,
+                                                          ast::StructMemberOffsetAttribute>()) {
+                                            needs_align = false;
+                                        }
+                                        attributes.Push(ctx.Clone(attr));
                                     }
-                                    attributes.Push(ctx.Clone(attr));
                                 }
                                 // If the alignment wasn't already specified, add an attribute to
                                 // make sure that we don't alter the alignment when using the packed
@@ -187,12 +189,17 @@
                                                       std::move(attributes)));
                             } else {
                                 // No vec3s, just clone the member as is.
-                                members.Push(ctx.Clone(member->Declaration()));
+                                if (auto* sem_mem = member->As<sem::StructMember>()) {
+                                    members.Push(ctx.Clone(sem_mem->Declaration()));
+                                } else {
+                                    members.Push(b.Member(ctx.Clone(member->Name()), new_type,
+                                                          utils::Empty));
+                                }
                             }
                         }
                         // Create the new structure.
-                        auto struct_name = b.Symbols().New(str->Declaration()->name->symbol.Name() +
-                                                           "_tint_packed_vec3");
+                        auto struct_name =
+                            b.Symbols().New(str->Name().Name() + "_tint_packed_vec3");
                         b.Structure(struct_name, std::move(members));
                         return struct_name;
                     });
@@ -246,7 +253,7 @@
             [&](const type::Matrix* mat) {
                 copy_array_elements(mat->columns(), mat->ColumnType());
             },
-            [&](const sem::Struct* str) {
+            [&](const type::Struct* str) {
                 // Copy the struct members over one at a time, packing/unpacking as necessary.
                 for (auto* member : str->Members()) {
                     const ast::Expression* element =
diff --git a/src/tint/transform/packed_vec3_test.cc b/src/tint/transform/packed_vec3_test.cc
index dbc28f7..a7f829f 100644
--- a/src/tint/transform/packed_vec3_test.cc
+++ b/src/tint/transform/packed_vec3_test.cc
@@ -4328,7 +4328,7 @@
         // The first member should have an alignment of 16 bytes, a size of 12 bytes, and the second
         // member should have an offset of 12 bytes.
         auto* sem_str = got.program.Sem().Get(vars[0]);
-        auto* str_ty = sem_str->Type()->UnwrapRef()->As<sem::Struct>();
+        auto* str_ty = sem_str->Type()->UnwrapRef()->As<type::Struct>();
         ASSERT_NE(str_ty, nullptr);
         ASSERT_EQ(str_ty->Members().Length(), 2u);
         EXPECT_EQ(str_ty->Members()[0]->Align(), 16u);
diff --git a/src/tint/transform/pad_structs.cc b/src/tint/transform/pad_structs.cc
index 90f2e59..cfa8c90 100644
--- a/src/tint/transform/pad_structs.cc
+++ b/src/tint/transform/pad_structs.cc
@@ -84,7 +84,7 @@
             new_members.Push(b.Member(name, type));
 
             uint32_t size = ty->Size();
-            if (ty->Is<sem::Struct>() && str->UsedAs(builtin::AddressSpace::kUniform)) {
+            if (ty->Is<type::Struct>() && str->UsedAs(builtin::AddressSpace::kUniform)) {
                 // std140 structs should be padded out to 16 bytes.
                 size = utils::RoundUp(16u, size);
             } else if (auto* array_ty = ty->As<type::Array>()) {
diff --git a/src/tint/transform/preserve_padding.cc b/src/tint/transform/preserve_padding.cc
index 6df1a1c..0e4daee 100644
--- a/src/tint/transform/preserve_padding.cc
+++ b/src/tint/transform/preserve_padding.cc
@@ -160,12 +160,12 @@
                     return body;
                 });
             },
-            [&](const sem::Struct* str) {
+            [&](const type::Struct* str) {
                 // Call a helper function that assigns each member separately.
                 return call_helper([&]() {
                     utils::Vector<const ast::Statement*, 8> body;
                     for (auto member : str->Members()) {
-                        auto name = member->Declaration()->name->symbol.Name();
+                        auto name = member->Name().Name();
                         body.Push(MakeAssignment(member->Type(),
                                                  b.MemberAccessor(b.Deref(kDestParamName), name),
                                                  b.MemberAccessor(kValueParamName, name)));
@@ -199,7 +199,7 @@
                 }
                 return HasPadding(col_ty);
             },
-            [&](const sem::Struct* str) {
+            [&](const type::Struct* str) {
                 uint32_t current_offset = 0;
                 for (auto* member : str->Members()) {
                     if (member->Offset() > current_offset) {
diff --git a/src/tint/transform/renamer.cc b/src/tint/transform/renamer.cc
index 14b8c12..92589b1 100644
--- a/src/tint/transform/renamer.cc
+++ b/src/tint/transform/renamer.cc
@@ -1281,8 +1281,8 @@
                 if (sem->Is<sem::Swizzle>()) {
                     preserved_identifiers.Add(accessor->member);
                 } else if (auto* str_expr = src->Sem().GetVal(accessor->object)) {
-                    if (auto* ty = str_expr->Type()->UnwrapRef()->As<sem::Struct>()) {
-                        if (ty->Declaration() == nullptr) {  // Builtin structure
+                    if (auto* ty = str_expr->Type()->UnwrapRef()->As<type::Struct>()) {
+                        if (!ty->Is<sem::Struct>()) {  // Builtin structure
                             preserved_identifiers.Add(accessor->member);
                         }
                     }
diff --git a/src/tint/transform/spirv_atomic.cc b/src/tint/transform/spirv_atomic.cc
index 82b955c..d58d58b 100644
--- a/src/tint/transform/spirv_atomic.cc
+++ b/src/tint/transform/spirv_atomic.cc
@@ -53,7 +53,7 @@
     ProgramBuilder b;
     /// The clone context
     CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
-    std::unordered_map<const ast::Struct*, ForkedStruct> forked_structs;
+    std::unordered_map<const type::Struct*, ForkedStruct> forked_structs;
     std::unordered_set<const sem::Variable*> atomic_variables;
     utils::UniqueVector<const sem::ValueExpression*, 8> atomic_expressions;
 
@@ -123,7 +123,8 @@
         if (!forked_structs.empty()) {
             ctx.ReplaceAll([&](const ast::Struct* str) {
                 // Is `str` a structure we need to fork?
-                if (auto it = forked_structs.find(str); it != forked_structs.end()) {
+                auto* str_ty = ctx.src->Sem().Get(str);
+                if (auto it = forked_structs.find(str_ty); it != forked_structs.end()) {
                     const auto& forked = it->second;
 
                     // Re-create the structure swapping in the atomic-flavoured members
@@ -154,10 +155,10 @@
     }
 
   private:
-    ForkedStruct& Fork(const ast::Struct* str) {
+    ForkedStruct& Fork(const type::Struct* str) {
         auto& forked = forked_structs[str];
         if (!forked.name.IsValid()) {
-            forked.name = b.Symbols().New(str->name->symbol.Name() + "_atomic");
+            forked.name = b.Symbols().New(str->Name().Name() + "_atomic");
         }
         return forked;
     }
@@ -179,7 +180,7 @@
                     // Fork the struct (the first time) and mark member(s) that need to be made
                     // atomic.
                     auto* member = access->Member();
-                    Fork(member->Struct()->Declaration()).atomic_members.emplace(member->Index());
+                    Fork(member->Struct()).atomic_members.emplace(member->Index());
                     atomic_expressions.Add(access->Object());
                 },
                 [&](const sem::IndexAccessorExpression* index) {
@@ -198,7 +199,7 @@
             ty,  //
             [&](const type::I32*) { return b.ty.atomic(CreateASTTypeFor(ctx, ty)); },
             [&](const type::U32*) { return b.ty.atomic(CreateASTTypeFor(ctx, ty)); },
-            [&](const sem::Struct* str) { return b.ty(Fork(str->Declaration()).name); },
+            [&](const type::Struct* str) { return b.ty(Fork(str).name); },
             [&](const type::Array* arr) {
                 if (arr->Count()->Is<type::RuntimeArrayCount>()) {
                     return b.ty.array(AtomicTypeFor(arr->ElemType()));
@@ -231,7 +232,7 @@
                 (atomic_variables.count(e->RootIdentifier()) != 0)) {
                 // If it's a struct member, make sure it's one we marked as atomic
                 if (auto* ma = e->As<sem::StructMemberAccess>()) {
-                    auto it = forked_structs.find(ma->Member()->Struct()->Declaration());
+                    auto it = forked_structs.find(ma->Member()->Struct());
                     if (it != forked_structs.end()) {
                         auto& forked = it->second;
                         return forked.atomic_members.count(ma->Member()->Index()) != 0;
diff --git a/src/tint/transform/std140.cc b/src/tint/transform/std140.cc
index b22a6e0..fa4fade 100644
--- a/src/tint/transform/std140.cc
+++ b/src/tint/transform/std140.cc
@@ -144,7 +144,7 @@
 
         // Scan structures for members that need forking
         for (auto* ty : src->Types()) {
-            if (auto* str = ty->As<sem::Struct>()) {
+            if (auto* str = ty->As<type::Struct>()) {
                 if (str->UsedAs(builtin::AddressSpace::kUniform)) {
                     for (auto* member : str->Members()) {
                         if (needs_fork(member->Type())) {
@@ -226,11 +226,11 @@
     utils::Hashset<const sem::Variable*, 8> std140_uniforms;
 
     // Map of original structure to 'std140' forked structure
-    utils::Hashmap<const sem::Struct*, Symbol, 8> std140_structs;
+    utils::Hashmap<const type::Struct*, Symbol, 8> std140_structs;
 
     // Map of structure member in src of a matrix type, to list of decomposed column
     // members in ctx.dst.
-    utils::Hashmap<const sem::StructMember*, utils::Vector<const ast::StructMember*, 4>, 8>
+    utils::Hashmap<const type::StructMember*, utils::Vector<const ast::StructMember*, 4>, 8>
         std140_mat_members;
 
     /// Describes a matrix that has been forked to a std140-structure holding the decomposed column
@@ -403,7 +403,7 @@
     ast::Type Std140Type(const type::Type* ty) {
         return Switch(
             ty,  //
-            [&](const sem::Struct* str) {
+            [&](const type::Struct* str) {
                 if (auto std140 = std140_structs.Find(str)) {
                     return b.ty(*std140);
                 }
@@ -631,7 +631,7 @@
     const std::string ConvertSuffix(const type::Type* ty) {
         return Switch(
             ty,  //
-            [&](const sem::Struct* str) { return str->Name().Name(); },
+            [&](const type::Struct* str) { return str->Name().Name(); },
             [&](const type::Array* arr) {
                 auto count = arr->ConstantCount();
                 if (TINT_UNLIKELY(!count)) {
@@ -694,7 +694,7 @@
 
             Switch(
                 ty,  //
-                [&](const sem::Struct* str) {
+                [&](const type::Struct* str) {
                     // Convert each of the structure members using either a converter function
                     // call, or by reassembling a std140 matrix from column vector members.
                     utils::Vector<const ast::Expression*, 8> args;
@@ -832,7 +832,7 @@
         // As this is accessing only part of the matrix, we just need to pick the right column
         // vector member.
         auto column_idx = std::get<u32>(chain.indices[std140_mat_idx + 1]);
-        if (auto* str = tint::As<sem::Struct>(ty)) {
+        if (auto* str = tint::As<type::Struct>(ty)) {
             // Structure member matrix. The columns are decomposed into the structure.
             auto mat_member_idx = std::get<u32>(chain.indices[std140_mat_idx]);
             auto* mat_member = str->Members()[mat_member_idx];
@@ -913,7 +913,7 @@
                 }
             }
 
-            if (auto* str = tint::As<sem::Struct>(ty)) {
+            if (auto* str = tint::As<type::Struct>(ty)) {
                 // Structure member matrix. The columns are decomposed into the structure.
                 auto mat_member_idx = std::get<u32>(chain.indices[std140_mat_idx]);
                 auto* mat_member = str->Members()[mat_member_idx];
@@ -1012,7 +1012,7 @@
         stmts.Push(b.Decl(let));
 
         utils::Vector<const ast::MemberAccessorExpression*, 4> columns;
-        if (auto* str = tint::As<sem::Struct>(ty)) {
+        if (auto* str = tint::As<type::Struct>(ty)) {
             // Structure member matrix. The columns are decomposed into the structure.
             auto mat_member_idx = std::get<u32>(chain.indices[std140_mat_idx]);
             auto* mat_member = str->Members()[mat_member_idx];
@@ -1133,7 +1133,7 @@
         auto idx = std::get<u32>(access);
         return Switch(
             ty,  //
-            [&](const sem::Struct* str) -> ExprTypeName {
+            [&](const type::Struct* str) -> ExprTypeName {
                 auto* member = str->Members()[idx];
                 auto member_name = member->Name().Name();
                 auto* expr = b.MemberAccessor(lhs, member_name);
diff --git a/src/tint/transform/transform.cc b/src/tint/transform/transform.cc
index 2002d02..453768f 100644
--- a/src/tint/transform/transform.cc
+++ b/src/tint/transform/transform.cc
@@ -143,7 +143,7 @@
         }
         return ctx.dst->ty.array(el, u32(count.value()), std::move(attrs));
     }
-    if (auto* s = ty->As<sem::Struct>()) {
+    if (auto* s = ty->As<type::Struct>()) {
         return ctx.dst->ty(ctx.Clone(s->Name()));
     }
     if (auto* s = ty->As<type::Reference>()) {
diff --git a/src/tint/transform/transform_test.cc b/src/tint/transform/transform_test.cc
index 1201e98..2da1ec5 100644
--- a/src/tint/transform/transform_test.cc
+++ b/src/tint/transform/transform_test.cc
@@ -120,8 +120,8 @@
 TEST_F(CreateASTTypeForTest, Struct) {
     auto str = create([](ProgramBuilder& b) {
         auto* decl = b.Structure("S", {});
-        return b.create<sem::Struct>(decl, decl->source, decl->name->symbol, utils::Empty,
-                                     4u /* align */, 4u /* size */, 4u /* size_no_padding */);
+        return b.create<sem::Struct>(decl, decl->name->symbol, utils::Empty, 4u /* align */,
+                                     4u /* size */, 4u /* size_no_padding */);
     });
 
     ast::CheckIdentifier(str, "S");
diff --git a/src/tint/transform/truncate_interstage_variables.cc b/src/tint/transform/truncate_interstage_variables.cc
index 33c5cf7..1fc0050 100644
--- a/src/tint/transform/truncate_interstage_variables.cc
+++ b/src/tint/transform/truncate_interstage_variables.cc
@@ -83,6 +83,8 @@
         auto* func_sem = sem.Get(func_ast);
         auto* str = func_sem->ReturnType()->As<sem::Struct>();
 
+        // This transform is run after CanonicalizeEntryPointIO transform,
+        // So it is guaranteed that entry point inputs are already grouped in a struct.
         if (TINT_UNLIKELY(!str)) {
             TINT_ICE(Transform, ctx.dst->Diagnostics())
                 << "Entrypoint function return type is non-struct.\n"
@@ -91,20 +93,14 @@
             continue;
         }
 
-        // This transform is run after CanonicalizeEntryPointIO transform,
-        // So it is guaranteed that entry point inputs are already grouped in a struct.
-        const ast::Struct* struct_ty = str->Declaration();
-
         // A prepass to check if any interstage variable locations in the entry point needs
         // truncating. If not we don't really need to handle this entry point.
         utils::Hashset<const sem::StructMember*, 16u> omit_members;
 
-        for (auto* member : struct_ty->members) {
-            if (ast::GetAttribute<ast::LocationAttribute>(member->attributes)) {
-                auto* m = sem.Get(member);
-                uint32_t location = m->Location().value();
-                if (!data->interstage_locations.test(location)) {
-                    omit_members.Add(m);
+        for (auto* member : str->Members()) {
+            if (auto location = member->Attributes().location) {
+                if (!data->interstage_locations.test(location.value())) {
+                    omit_members.Add(member);
                 }
             }
         }
diff --git a/src/tint/transform/vertex_pulling.cc b/src/tint/transform/vertex_pulling.cc
index 064be00..5f704de 100644
--- a/src/tint/transform/vertex_pulling.cc
+++ b/src/tint/transform/vertex_pulling.cc
@@ -826,8 +826,8 @@
                 auto* sem = src->Sem().Get(member);
                 info.type = sem->Type();
 
-                TINT_ASSERT(Transform, sem->Location().has_value());
-                location_info[sem->Location().value()] = info;
+                TINT_ASSERT(Transform, sem->Attributes().location.has_value());
+                location_info[sem->Attributes().location.value()] = info;
                 has_locations = true;
             } else {
                 auto* builtin_attr = ast::GetAttribute<ast::BuiltinAttribute>(member->attributes);
diff --git a/src/tint/transform/zero_init_workgroup_memory.cc b/src/tint/transform/zero_init_workgroup_memory.cc
index 8572ef2..ec4a869 100644
--- a/src/tint/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/transform/zero_init_workgroup_memory.cc
@@ -168,19 +168,16 @@
                 }
             }
 
-            if (auto* str = sem.Get(param)->Type()->As<sem::Struct>()) {
+            if (auto* str = sem.Get(param)->Type()->As<type::Struct>()) {
                 for (auto* member : str->Members()) {
-                    if (auto* builtin_attr = ast::GetAttribute<ast::BuiltinAttribute>(
-                            member->Declaration()->attributes)) {
-                        auto builtin = sem.Get(builtin_attr)->Value();
-                        if (builtin == builtin::BuiltinValue::kLocalInvocationIndex) {
-                            local_index = [=] {
-                                auto* param_expr = b.Expr(ctx.Clone(param->name->symbol));
-                                auto* member_name = ctx.Clone(member->Declaration()->name);
-                                return b.MemberAccessor(param_expr, member_name);
-                            };
-                            break;
-                        }
+                    if (member->Attributes().builtin ==
+                        builtin::BuiltinValue::kLocalInvocationIndex) {
+                        local_index = [=] {
+                            auto* param_expr = b.Expr(ctx.Clone(param->name->symbol));
+                            auto member_name = ctx.Clone(member->Name());
+                            return b.MemberAccessor(param_expr, member_name);
+                        };
+                        break;
                     }
                 }
             }
@@ -318,9 +315,9 @@
             return true;
         }
 
-        if (auto* str = ty->As<sem::Struct>()) {
+        if (auto* str = ty->As<type::Struct>()) {
             for (auto* member : str->Members()) {
-                auto name = ctx.Clone(member->Declaration()->name->symbol);
+                auto name = ctx.Clone(member->Name());
                 auto get_member = [&](uint32_t num_values) {
                     auto s = get_expr(num_values);
                     if (!s) {
@@ -444,7 +441,7 @@
         if (ty->Is<type::Atomic>()) {
             return false;
         }
-        if (auto* str = ty->As<sem::Struct>()) {
+        if (auto* str = ty->As<type::Struct>()) {
             for (auto* member : str->Members()) {
                 if (!CanTriviallyZero(member->Type())) {
                     return false;
diff --git a/src/tint/type/struct.cc b/src/tint/type/struct.cc
index e5c8d4c..c9864cb 100644
--- a/src/tint/type/struct.cc
+++ b/src/tint/type/struct.cc
@@ -52,14 +52,12 @@
 
 }  // namespace
 
-Struct::Struct(tint::Source source,
-               Symbol name,
+Struct::Struct(Symbol name,
                utils::VectorRef<const StructMember*> members,
                uint32_t align,
                uint32_t size,
                uint32_t size_no_padding)
     : Base(utils::Hash(utils::TypeInfo::Of<Struct>().full_hashcode, name), FlagsFrom(members)),
-      source_(source),
       name_(name),
       members_(std::move(members)),
       align_(align),
@@ -169,33 +167,30 @@
     for (const auto& mem : members_) {
         members.Push(mem->Clone(ctx));
     }
-    return ctx.dst.mgr->Get<Struct>(source_, sym, members, align_, size_, size_no_padding_);
+    return ctx.dst.mgr->Get<Struct>(sym, members, align_, size_, size_no_padding_);
 }
 
-StructMember::StructMember(tint::Source source,
-                           Symbol name,
+StructMember::StructMember(Symbol name,
                            const type::Type* type,
                            uint32_t index,
                            uint32_t offset,
                            uint32_t align,
                            uint32_t size,
-                           std::optional<uint32_t> location)
-    : source_(source),
-      name_(name),
+                           const StructMemberAttributes& attributes)
+    : name_(name),
       type_(type),
       index_(index),
       offset_(offset),
       align_(align),
       size_(size),
-      location_(location) {}
+      attributes_(attributes) {}
 
 StructMember::~StructMember() = default;
 
 StructMember* StructMember::Clone(CloneContext& ctx) const {
     auto sym = ctx.dst.st->Register(name_.Name());
     auto* ty = type_->Clone(ctx);
-    return ctx.dst.mgr->Get<StructMember>(source_, sym, ty, index_, offset_, align_, size_,
-                                          location_);
+    return ctx.dst.mgr->Get<StructMember>(sym, ty, index_, offset_, align_, size_, attributes_);
 }
 
 }  // namespace tint::type
diff --git a/src/tint/type/struct.h b/src/tint/type/struct.h
index ea459f5..08119d0 100644
--- a/src/tint/type/struct.h
+++ b/src/tint/type/struct.h
@@ -22,6 +22,7 @@
 #include <unordered_set>
 
 #include "src/tint/builtin/address_space.h"
+#include "src/tint/builtin/interpolation.h"
 #include "src/tint/symbol.h"
 #include "src/tint/type/node.h"
 #include "src/tint/type/type.h"
@@ -48,15 +49,13 @@
 class Struct : public utils::Castable<Struct, Type> {
   public:
     /// Constructor
-    /// @param source the source of the structure
     /// @param name the name of the structure
     /// @param members the structure members
     /// @param align the byte alignment of the structure
     /// @param size the byte size of the structure
     /// @param size_no_padding size of the members without the end of structure
     /// alignment padding
-    Struct(tint::Source source,
-           Symbol name,
+    Struct(Symbol name,
            utils::VectorRef<const StructMember*> members,
            uint32_t align,
            uint32_t size,
@@ -69,9 +68,6 @@
     /// @returns true if the this type is equal to @p other
     bool Equals(const UniqueNode& other) const override;
 
-    /// @returns the source of the structure
-    tint::Source Source() const { return source_; }
-
     /// @returns the name of the structure
     Symbol Name() const { return name_; }
 
@@ -152,7 +148,6 @@
     Struct* Clone(CloneContext& ctx) const override;
 
   private:
-    const tint::Source source_;
     const Symbol name_;
     const utils::Vector<const StructMember*, 4> members_;
     const uint32_t align_;
@@ -163,33 +158,40 @@
     utils::Vector<const Struct*, 2> concrete_types_;
 };
 
+/// Attributes that can be applied to the StructMember
+struct StructMemberAttributes {
+    /// The value of a `@location` attribute
+    std::optional<uint32_t> location;
+    /// The value of a `@builtin` attribute
+    std::optional<builtin::BuiltinValue> builtin;
+    /// The values of a `@interpolate` attribute
+    std::optional<builtin::Interpolation> interpolation;
+    /// True if the member was annotated with `@invariant`
+    bool invariant = false;
+};
+
 /// StructMember holds the type information for structure members.
 class StructMember : public utils::Castable<StructMember, Node> {
   public:
     /// Constructor
-    /// @param source the source of the struct member
     /// @param name the name of the structure member
     /// @param type the type of the member
     /// @param index the index of the member in the structure
     /// @param offset the byte offset from the base of the structure
     /// @param align the byte alignment of the member
     /// @param size the byte size of the member
-    /// @param location the location attribute, if present
-    StructMember(tint::Source source,
-                 Symbol name,
+    /// @param attributes the optional attributes
+    StructMember(Symbol name,
                  const type::Type* type,
                  uint32_t index,
                  uint32_t offset,
                  uint32_t align,
                  uint32_t size,
-                 std::optional<uint32_t> location);
+                 const StructMemberAttributes& attributes);
 
     /// Destructor
     ~StructMember() override;
 
-    /// @returns the source the struct member
-    const tint::Source& Source() const { return source_; }
-
     /// @returns the name of the structure member
     Symbol Name() const { return name_; }
 
@@ -215,15 +217,14 @@
     /// @returns byte size
     uint32_t Size() const { return size_; }
 
-    /// @returns the location, if set
-    std::optional<uint32_t> Location() const { return location_; }
+    /// @returns the optional attributes
+    const StructMemberAttributes& Attributes() const { return attributes_; }
 
     /// @param ctx the clone context
     /// @returns a clone of this struct member
     StructMember* Clone(CloneContext& ctx) const;
 
   private:
-    const tint::Source source_;
     const Symbol name_;
     const type::Struct* struct_;
     const type::Type* type_;
@@ -231,7 +232,7 @@
     const uint32_t offset_;
     const uint32_t align_;
     const uint32_t size_;
-    const std::optional<uint32_t> location_;
+    const StructMemberAttributes attributes_;
 };
 
 }  // namespace tint::type
diff --git a/src/tint/type/struct_test.cc b/src/tint/type/struct_test.cc
index 7bfb437..bb88740 100644
--- a/src/tint/type/struct_test.cc
+++ b/src/tint/type/struct_test.cc
@@ -24,7 +24,7 @@
 
 TEST_F(TypeStructTest, Creation) {
     auto name = Sym("S");
-    auto* s = create<Struct>(Source{}, name, utils::Empty, 4u /* align */, 8u /* size */,
+    auto* s = create<Struct>(name, utils::Empty, 4u /* align */, 8u /* size */,
                              16u /* size_no_padding */);
     EXPECT_EQ(s->Align(), 4u);
     EXPECT_EQ(s->Size(), 8u);
@@ -32,9 +32,9 @@
 }
 
 TEST_F(TypeStructTest, Equals) {
-    auto* a = create<Struct>(Source{}, Sym("a"), utils::Empty, 4u /* align */, 4u /* size */,
+    auto* a = create<Struct>(Sym("a"), utils::Empty, 4u /* align */, 4u /* size */,
                              4u /* size_no_padding */);
-    auto* b = create<Struct>(Source{}, Sym("b"), utils::Empty, 4u /* align */, 4u /* size */,
+    auto* b = create<Struct>(Sym("b"), utils::Empty, 4u /* align */, 4u /* size */,
                              4u /* size_no_padding */);
 
     EXPECT_TRUE(a->Equals(*a));
@@ -44,8 +44,8 @@
 
 TEST_F(TypeStructTest, FriendlyName) {
     auto name = Sym("my_struct");
-    auto* s = create<Struct>(Source{}, name, utils::Empty, 4u /* align */, 4u /* size */,
-                             4u /* size_no_padding */);
+    auto* s =
+        create<Struct>(name, utils::Empty, 4u /* align */, 4u /* size */, 4u /* size_no_padding */);
     EXPECT_EQ(s->FriendlyName(), "my_struct");
 }
 
@@ -101,10 +101,8 @@
     auto* sem = p.Sem().Get(st);
     ASSERT_EQ(2u, sem->Members().Length());
 
-    EXPECT_TRUE(sem->Members()[0]->Location().has_value());
-    EXPECT_EQ(sem->Members()[0]->Location().value(), 1u);
-
-    EXPECT_FALSE(sem->Members()[1]->Location().has_value());
+    EXPECT_EQ(sem->Members()[0]->Attributes().location, 1u);
+    EXPECT_FALSE(sem->Members()[1]->Attributes().location.has_value());
 }
 
 TEST_F(TypeStructTest, IsConstructable) {
@@ -207,12 +205,15 @@
 }
 
 TEST_F(TypeStructTest, Clone) {
+    type::StructMemberAttributes attrs_location_2;
+    attrs_location_2.location = 2;
+
     auto* s = create<Struct>(
-        Source{}, Sym("my_struct"),
-        utils::Vector{create<StructMember>(Source{}, Sym("b"), create<Vector>(create<F32>(), 3u),
-                                           0u, 0u, 16u, 12u, std::optional<uint32_t>{2}),
-                      create<StructMember>(Source{}, Sym("a"), create<I32>(), 1u, 16u, 4u, 4u,
-                                           std::optional<uint32_t>())},
+        Sym("my_struct"),
+        utils::Vector{create<StructMember>(Sym("b"), create<Vector>(create<F32>(), 3u), 0u, 0u, 16u,
+                                           12u, attrs_location_2),
+                      create<StructMember>(Sym("a"), create<I32>(), 1u, 16u, 4u, 4u,
+                                           type::StructMemberAttributes{})},
         4u /* align */, 8u /* size */, 16u /* size_no_padding */);
 
     ProgramID id;
diff --git a/src/tint/type/type_test.cc b/src/tint/type/type_test.cc
index 1565e10..b76a43a 100644
--- a/src/tint/type/type_test.cc
+++ b/src/tint/type/type_test.cc
@@ -45,50 +45,44 @@
     const Matrix* mat4x3_af = create<Matrix>(vec3_af, 4u);
     const Reference* ref_u32 =
         create<Reference>(u32, builtin::AddressSpace::kPrivate, builtin::Access::kReadWrite);
-    const Struct* str_f32 = create<Struct>(Source{},
-                                           Sym("str_f32"),
+    const Struct* str_f32 = create<Struct>(Sym("str_f32"),
                                            utils::Vector{
                                                create<StructMember>(
-                                                   /* source */ Source{},
                                                    /* name */ Sym("x"),
                                                    /* type */ f32,
                                                    /* index */ 0u,
                                                    /* offset */ 0u,
                                                    /* align */ 4u,
                                                    /* size */ 4u,
-                                                   /* location */ std::nullopt),
+                                                   /* attributes */ type::StructMemberAttributes{}),
                                            },
                                            /* align*/ 4u,
                                            /* size*/ 4u,
                                            /* size_no_padding*/ 4u);
-    const Struct* str_f16 = create<Struct>(Source{},
-                                           Sym("str_f16"),
+    const Struct* str_f16 = create<Struct>(Sym("str_f16"),
                                            utils::Vector{
                                                create<StructMember>(
-                                                   /* source */ Source{},
                                                    /* name */ Sym("x"),
                                                    /* type */ f16,
                                                    /* index */ 0u,
                                                    /* offset */ 0u,
                                                    /* align */ 4u,
                                                    /* size */ 4u,
-                                                   /* location */ std::nullopt),
+                                                   /* attributes */ type::StructMemberAttributes{}),
                                            },
                                            /* align*/ 4u,
                                            /* size*/ 4u,
                                            /* size_no_padding*/ 4u);
-    Struct* str_af = create<Struct>(Source{},
-                                    Sym("str_af"),
+    Struct* str_af = create<Struct>(Sym("str_af"),
                                     utils::Vector{
                                         create<StructMember>(
-                                            /* source */ Source{},
                                             /* name */ Sym("x"),
                                             /* type */ af,
                                             /* index */ 0u,
                                             /* offset */ 0u,
                                             /* align */ 4u,
                                             /* size */ 4u,
-                                            /* location */ std::nullopt),
+                                            /* attributes */ type::StructMemberAttributes{}),
                                     },
                                     /* align*/ 4u,
                                     /* size*/ 4u,
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index cd3834f..8fcdd3d 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -814,7 +814,7 @@
             return;
         }
         case builtin::Function::kAtomicCompareExchangeWeak: {
-            EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
+            EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>());
 
             auto* dest = expr->args[0];
             auto* compare_value = expr->args[1];
@@ -1043,7 +1043,7 @@
                       [&](TextBuffer* b, const std::vector<std::string>& params) {
                           // Emit the builtin return type unique to this overload. This does not
                           // exist in the AST, so it will not be generated in Generate().
-                          EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
+                          EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>());
 
                           {
                               auto l = line(b);
@@ -1064,7 +1064,7 @@
                       [&](TextBuffer* b, const std::vector<std::string>& params) {
                           // Emit the builtin return type unique to this overload. This does not
                           // exist in the AST, so it will not be generated in Generate().
-                          EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
+                          EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>());
 
                           {
                               auto l = line(b);
@@ -1769,7 +1769,7 @@
 
 void GeneratorImpl::EmitUniformVariable(const ast::Var* var, const sem::Variable* sem) {
     auto* type = sem->Type()->UnwrapRef();
-    auto* str = type->As<sem::Struct>();
+    auto* str = type->As<type::Struct>();
     if (TINT_UNLIKELY(!str)) {
         TINT_ICE(Writer, builder_.Diagnostics()) << "storage variable must be of struct type";
         return;
@@ -1788,7 +1788,7 @@
 
 void GeneratorImpl::EmitStorageVariable(const ast::Var* var, const sem::Variable* sem) {
     auto* type = sem->Type()->UnwrapRef();
-    auto* str = type->As<sem::Struct>();
+    auto* str = type->As<type::Struct>();
     if (TINT_UNLIKELY(!str)) {
         TINT_ICE(Writer, builder_.Diagnostics()) << "storage variable must be of struct type";
         return;
@@ -2040,7 +2040,7 @@
         for (auto* var : func->params) {
             auto* sem = builder_.Sem().Get(var);
             auto* type = sem->Type();
-            if (TINT_UNLIKELY(!type->Is<sem::Struct>())) {
+            if (TINT_UNLIKELY(!type->Is<type::Struct>())) {
                 // ICE likely indicates that the CanonicalizeEntryPointIO transform was
                 // not run, or a builtin parameter was added after it was run.
                 TINT_ICE(Writer, diagnostics_) << "Unsupported non-struct entry point parameter";
@@ -2132,7 +2132,7 @@
                 EmitConstant(out, constant->Index(i));
             }
         },
-        [&](const sem::Struct* s) {
+        [&](const type::Struct* s) {
             EmitStructType(&helpers_, s);
 
             out << StructName(s);
@@ -2210,7 +2210,7 @@
             }
             EmitZeroValue(out, mat->type());
         }
-    } else if (auto* str = type->As<sem::Struct>()) {
+    } else if (auto* str = type->As<type::Struct>()) {
         EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
         bool first = true;
         ScopedParen sp(out);
@@ -2568,7 +2568,7 @@
         TINT_ICE(Writer, diagnostics_) << "Attempting to emit pointer type. These should have been "
                                           "removed with the SimplifyPointers transform";
     } else if (type->Is<type::Sampler>()) {
-    } else if (auto* str = type->As<sem::Struct>()) {
+    } else if (auto* str = type->As<type::Struct>()) {
         out << StructName(str);
     } else if (auto* tex = type->As<type::Texture>()) {
         if (TINT_UNLIKELY(tex->Is<type::ExternalTexture>())) {
@@ -2669,7 +2669,7 @@
     }
 }
 
-void GeneratorImpl::EmitStructType(TextBuffer* b, const sem::Struct* str) {
+void GeneratorImpl::EmitStructType(TextBuffer* b, const type::Struct* str) {
     auto it = emitted_structs_.emplace(str);
     if (!it.second) {
         return;
@@ -2682,7 +2682,7 @@
     line(b);
 }
 
-void GeneratorImpl::EmitStructMembers(TextBuffer* b, const sem::Struct* str) {
+void GeneratorImpl::EmitStructMembers(TextBuffer* b, const type::Struct* str) {
     ScopedIndent si(b);
     for (auto* mem : str->Members()) {
         auto name = mem->Name().Name();
diff --git a/src/tint/writer/glsl/generator_impl.h b/src/tint/writer/glsl/generator_impl.h
index 0698d4f..5d74e37 100644
--- a/src/tint/writer/glsl/generator_impl.h
+++ b/src/tint/writer/glsl/generator_impl.h
@@ -383,11 +383,11 @@
     /// this function will simply return `true` without emitting anything.
     /// @param buffer the text buffer that the type declaration will be written to
     /// @param ty the struct to generate
-    void EmitStructType(TextBuffer* buffer, const sem::Struct* ty);
+    void EmitStructType(TextBuffer* buffer, const type::Struct* ty);
     /// Handles generating the members of a structure
     /// @param buffer the text buffer that the struct members will be written to
     /// @param ty the struct to generate
-    void EmitStructMembers(TextBuffer* buffer, const sem::Struct* ty);
+    void EmitStructMembers(TextBuffer* buffer, const type::Struct* ty);
     /// Handles a unary op expression
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
@@ -399,7 +399,7 @@
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
     void EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     void EmitLet(const ast::Let* let);
     /// Handles generating a module-scope 'let' declaration
@@ -460,7 +460,7 @@
     std::unordered_map<const type::Vector*, std::string> dynamic_vector_write_;
     std::unordered_map<const type::Vector*, std::string> int_dot_funcs_;
     std::unordered_map<BinaryOperandType, std::string> float_modulo_funcs_;
-    std::unordered_set<const sem::Struct*> emitted_structs_;
+    std::unordered_set<const type::Struct*> emitted_structs_;
     bool requires_oes_sample_variables_ = false;
     bool requires_default_precision_qualifier_ = false;
     bool requires_f16_extension_ = false;
diff --git a/src/tint/writer/glsl/generator_impl_type_test.cc b/src/tint/writer/glsl/generator_impl_type_test.cc
index 0e85155..5cec150 100644
--- a/src/tint/writer/glsl/generator_impl_type_test.cc
+++ b/src/tint/writer/glsl/generator_impl_type_test.cc
@@ -170,8 +170,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    gen.EmitStructType(&buf, sem_s);
+    auto* str = program->TypeOf(s)->As<type::Struct>();
+    gen.EmitStructType(&buf, str);
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
@@ -190,9 +190,9 @@
 
     GeneratorImpl& gen = Build();
 
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
+    auto* str = program->TypeOf(s)->As<type::Struct>();
     utils::StringStream out;
-    gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    gen.EmitType(out, str, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "S");
 }
@@ -224,8 +224,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    gen.EmitStructType(&buf, sem_s);
+    auto* str = program->TypeOf(s)->As<type::Struct>();
+    gen.EmitStructType(&buf, str);
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 5cce6a1..ed86e9e 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -1094,7 +1094,7 @@
         }
     }
 
-    bool brackets = type->IsAnyOf<type::Array, sem::Struct>();
+    bool brackets = type->IsAnyOf<type::Array, type::Struct>();
 
     // For single-value vector initializers, swizzle the scalar to the right
     // vector dimension using .x
@@ -1887,7 +1887,7 @@
             return true;
         }
         case builtin::Function::kAtomicCompareExchangeWeak: {
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                 return false;
             }
 
@@ -2004,7 +2004,7 @@
 
             // Emit the builtin return type unique to this overload. This does not
             // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                 return false;
             }
 
@@ -2037,7 +2037,7 @@
 
             // Emit the builtin return type unique to this overload. This does not
             // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                 return false;
             }
 
@@ -3287,7 +3287,7 @@
         for (auto* var : func->params) {
             auto* sem = builder_.Sem().Get(var);
             auto* type = sem->Type();
-            if (TINT_UNLIKELY(!type->Is<sem::Struct>())) {
+            if (TINT_UNLIKELY(!type->Is<type::Struct>())) {
                 // ICE likely indicates that the CanonicalizeEntryPointIO transform was
                 // not run, or a builtin parameter was added after it was run.
                 TINT_ICE(Writer, diagnostics_) << "Unsupported non-struct entry point parameter";
@@ -3437,7 +3437,7 @@
 
             return true;
         },
-        [&](const sem::Struct* s) {
+        [&](const type::Struct* s) {
             if (!EmitStructType(&helpers_, s)) {
                 return false;
             }
@@ -3580,7 +3580,7 @@
             }
             return true;
         },
-        [&](const sem::Struct*) {
+        [&](const type::Struct*) {
             out << "(";
             TINT_DEFER(out << ")" << value);
             return EmitType(out, type, builtin::AddressSpace::kUndefined,
@@ -4077,7 +4077,7 @@
             out << "State";
             return true;
         },
-        [&](const sem::Struct* str) {
+        [&](const type::Struct* str) {
             out << StructName(str);
             return true;
         },
@@ -4202,7 +4202,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitStructType(TextBuffer* b, const sem::Struct* str) {
+bool GeneratorImpl::EmitStructType(TextBuffer* b, const type::Struct* str) {
     auto it = emitted_structs_.emplace(str);
     if (!it.second) {
         return true;
@@ -4216,73 +4216,48 @@
             auto* ty = mem->Type();
             auto out = line(b);
             std::string pre, post;
-            if (auto* decl = mem->Declaration()) {
-                for (auto* attr : decl->attributes) {
-                    if (attr->Is<ast::LocationAttribute>()) {
-                        auto& pipeline_stage_uses = str->PipelineStageUses();
-                        if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                            TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
-                        }
 
-                        auto loc = mem->Location().value();
-                        if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kVertexOutput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentInput)) {
-                            post += " : TEXCOORD" + std::to_string(loc);
-                        } else if (TINT_LIKELY(pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentOutput))) {
-                            post += " : SV_Target" + std::to_string(loc);
-                        } else {
-                            TINT_ICE(Writer, diagnostics_) << "invalid use of location attribute";
-                        }
-                    } else if (auto* builtin_attr = attr->As<ast::BuiltinAttribute>()) {
-                        auto builtin = program_->Sem().Get(builtin_attr)->Value();
-                        auto name = builtin_to_attribute(builtin);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer, "unsupported builtin");
-                            return false;
-                        }
-                        post += " : " + name;
-                    } else if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
-                        auto& sem = program_->Sem();
-                        auto i_type =
-                            sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationType>>(
-                                   interpolate->type)
-                                ->Value();
+            auto& attributes = mem->Attributes();
 
-                        auto i_smpl = builtin::InterpolationSampling::kUndefined;
-                        if (interpolate->sampling) {
-                            i_smpl =
-                                sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationSampling>>(
-                                       interpolate->sampling)
-                                    ->Value();
-                        }
-
-                        auto mod = interpolation_to_modifiers(i_type, i_smpl);
-                        if (mod.empty()) {
-                            diagnostics_.add_error(diag::System::Writer,
-                                                   "unsupported interpolation");
-                            return false;
-                        }
-                        pre += mod;
-
-                    } else if (attr->Is<ast::InvariantAttribute>()) {
-                        // Note: `precise` is not exactly the same as `invariant`, but is
-                        // stricter and therefore provides the necessary guarantees.
-                        // See discussion here: https://github.com/gpuweb/gpuweb/issues/893
-                        pre += "precise ";
-                    } else if (TINT_UNLIKELY((!attr->IsAnyOf<ast::StructMemberAlignAttribute,
-                                                             ast::StructMemberOffsetAttribute,
-                                                             ast::StructMemberSizeAttribute>()))) {
-                        TINT_ICE(Writer, diagnostics_)
-                            << "unhandled struct member attribute: " << attr->Name();
-                        return false;
-                    }
+            if (auto location = attributes.location) {
+                auto& pipeline_stage_uses = str->PipelineStageUses();
+                if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                    TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
                 }
+                if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexOutput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentInput)) {
+                    post += " : TEXCOORD" + std::to_string(location.value());
+                } else if (TINT_LIKELY(pipeline_stage_uses.count(
+                               type::PipelineStageUsage::kFragmentOutput))) {
+                    post += " : SV_Target" + std::to_string(location.value());
+                } else {
+                    TINT_ICE(Writer, diagnostics_) << "invalid use of location attribute";
+                }
+            }
+            if (auto builtin = attributes.builtin) {
+                auto name = builtin_to_attribute(builtin.value());
+                if (name.empty()) {
+                    diagnostics_.add_error(diag::System::Writer, "unsupported builtin");
+                    return false;
+                }
+                post += " : " + name;
+            }
+            if (auto interpolation = attributes.interpolation) {
+                auto mod = interpolation_to_modifiers(interpolation->type, interpolation->sampling);
+                if (mod.empty()) {
+                    diagnostics_.add_error(diag::System::Writer, "unsupported interpolation");
+                    return false;
+                }
+                pre += mod;
+            }
+            if (attributes.invariant) {
+                // Note: `precise` is not exactly the same as `invariant`, but is
+                // stricter and therefore provides the necessary guarantees.
+                // See discussion here: https://github.com/gpuweb/gpuweb/issues/893
+                pre += "precise ";
             }
 
             out << pre;
diff --git a/src/tint/writer/hlsl/generator_impl.h b/src/tint/writer/hlsl/generator_impl.h
index 103e355..f52d58f 100644
--- a/src/tint/writer/hlsl/generator_impl.h
+++ b/src/tint/writer/hlsl/generator_impl.h
@@ -449,7 +449,7 @@
     /// @param buffer the text buffer that the type declaration will be written to
     /// @param ty the struct to generate
     /// @returns true if the struct is emitted
-    bool EmitStructType(TextBuffer* buffer, const sem::Struct* ty);
+    bool EmitStructType(TextBuffer* buffer, const type::Struct* ty);
     /// Handles a unary op expression
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
@@ -470,7 +470,7 @@
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
     bool EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     /// @returns true if the variable was emitted
     bool EmitLet(const ast::Let* let);
@@ -568,7 +568,7 @@
     std::unordered_map<const type::Matrix*, std::string> dynamic_matrix_vector_write_;
     std::unordered_map<const type::Matrix*, std::string> dynamic_matrix_scalar_write_;
     std::unordered_map<const type::Type*, std::string> value_or_one_if_zero_;
-    std::unordered_set<const sem::Struct*> emitted_structs_;
+    std::unordered_set<const type::Struct*> emitted_structs_;
 };
 
 }  // namespace tint::writer::hlsl
diff --git a/src/tint/writer/hlsl/generator_impl_type_test.cc b/src/tint/writer/hlsl/generator_impl_type_test.cc
index d93d8e3..337b26b 100644
--- a/src/tint/writer/hlsl/generator_impl_type_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_type_test.cc
@@ -171,8 +171,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(s)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
   float b;
@@ -203,10 +203,10 @@
 
     GeneratorImpl& gen = Build();
 
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
+    auto* str = program->TypeOf(s)->As<type::Struct>();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
+    ASSERT_TRUE(
+        gen.EmitType(out, str, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.Diagnostics();
     EXPECT_EQ(out.str(), "S");
 }
@@ -238,8 +238,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(s)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
   float b;
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 0435918..14288ee 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -363,7 +363,7 @@
 }
 
 bool GeneratorImpl::EmitTypeDecl(const type::Type* ty) {
-    if (auto* str = ty->As<sem::Struct>()) {
+    if (auto* str = ty->As<type::Struct>()) {
         if (!EmitStructType(current_buffer_, str)) {
             return false;
         }
@@ -826,7 +826,7 @@
             terminator = "}";
             return true;
         },
-        [&](const sem::Struct*) {
+        [&](const type::Struct*) {
             out << "{";
             terminator = "}";
             return true;
@@ -848,10 +848,9 @@
             out << ", ";
         }
 
-        if (auto* struct_ty = type->As<sem::Struct>()) {
+        if (auto* struct_ty = type->As<type::Struct>()) {
             // Emit field designators for structures to account for padding members.
-            auto* member = struct_ty->Members()[i]->Declaration();
-            auto name = member->name->symbol.Name();
+            auto name = struct_ty->Members()[i]->Name().Name();
             out << "." << name << "=";
         }
 
@@ -922,11 +921,11 @@
         case builtin::Function::kAtomicCompareExchangeWeak: {
             auto* ptr_ty = TypeOf(expr->args[0])->UnwrapRef()->As<type::Pointer>();
             auto sc = ptr_ty->AddressSpace();
-            auto* str = builtin->ReturnType()->As<sem::Struct>();
+            auto* str = builtin->ReturnType()->As<type::Struct>();
 
             auto func = utils::GetOrCreate(
                 atomicCompareExchangeWeak_, ACEWKeyType{{sc, str}}, [&]() -> std::string {
-                    if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+                    if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                         return "";
                     }
 
@@ -937,7 +936,7 @@
 
                     {
                         auto f = line(&buf);
-                        auto str_name = StructName(builtin->ReturnType()->As<sem::Struct>());
+                        auto str_name = StructName(builtin->ReturnType()->As<type::Struct>());
                         f << str_name << " " << name << "(";
                         if (!EmitTypeAndName(f, atomic_ty, "atomic")) {
                             return "";
@@ -1361,11 +1360,11 @@
 
             // Emit the builtin return type unique to this overload. This does not
             // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                 return false;
             }
 
-            line(b) << StructName(builtin->ReturnType()->As<sem::Struct>()) << " result;";
+            line(b) << StructName(builtin->ReturnType()->As<type::Struct>()) << " result;";
             line(b) << "result.fract = modf(" << in << ", result.whole);";
             line(b) << "return result;";
             return true;
@@ -1387,11 +1386,11 @@
 
             // Emit the builtin return type unique to this overload. This does not
             // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
+            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<type::Struct>())) {
                 return false;
             }
 
-            line(b) << StructName(builtin->ReturnType()->As<sem::Struct>()) << " result;";
+            line(b) << StructName(builtin->ReturnType()->As<type::Struct>()) << " result;";
             line(b) << "result.fract = frexp(" << in << ", result.exp);";
             line(b) << "return result;";
             return true;
@@ -1658,7 +1657,7 @@
             out << "{}";
             return true;
         },
-        [&](const sem::Struct*) {
+        [&](const type::Struct*) {
             out << "{}";
             return true;
         },
@@ -1763,7 +1762,7 @@
 
             return true;
         },
-        [&](const sem::Struct* s) {
+        [&](const type::Struct* s) {
             if (!EmitStructType(&helpers_, s)) {
                 return false;
             }
@@ -2636,7 +2635,7 @@
             out << "sampler";
             return true;
         },
-        [&](const sem::Struct* str) {
+        [&](const type::Struct* str) {
             // The struct type emits as just the name. The declaration would be
             // emitted as part of emitting the declared types.
             out << StructName(str);
@@ -2791,7 +2790,7 @@
     return false;
 }
 
-bool GeneratorImpl::EmitStructType(TextBuffer* b, const sem::Struct* str) {
+bool GeneratorImpl::EmitStructType(TextBuffer* b, const type::Struct* str) {
     auto it = emitted_structs_.emplace(str);
     if (!it.second) {
         return true;
@@ -2852,88 +2851,51 @@
 
         out << " " << mem_name;
         // Emit attributes
-        if (auto* decl = mem->Declaration()) {
-            for (auto* attr : decl->attributes) {
-                bool ok = Switch(
-                    attr,
-                    [&](const ast::BuiltinAttribute* builtin_attr) {
-                        auto builtin = program_->Sem().Get(builtin_attr)->Value();
-                        auto name = builtin_to_attribute(builtin);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer, "unknown builtin");
-                            return false;
-                        }
-                        out << " [[" << name << "]]";
-                        return true;
-                    },
-                    [&](const ast::LocationAttribute*) {
-                        auto& pipeline_stage_uses = str->PipelineStageUses();
-                        if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                            TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
-                            return false;
-                        }
+        auto& attributes = mem->Attributes();
 
-                        uint32_t loc = mem->Location().value();
-                        if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
-                            out << " [[attribute(" + std::to_string(loc) + ")]]";
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kVertexOutput)) {
-                            out << " [[user(locn" + std::to_string(loc) + ")]]";
-                        } else if (pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentInput)) {
-                            out << " [[user(locn" + std::to_string(loc) + ")]]";
-                        } else if (TINT_LIKELY(pipeline_stage_uses.count(
-                                       type::PipelineStageUsage::kFragmentOutput))) {
-                            out << " [[color(" + std::to_string(loc) + ")]]";
-                        } else {
-                            TINT_ICE(Writer, diagnostics_) << "invalid use of location decoration";
-                            return false;
-                        }
-                        return true;
-                    },
-                    [&](const ast::InterpolateAttribute* interpolate) {
-                        auto& sem = program_->Sem();
-                        auto i_type =
-                            sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationType>>(
-                                   interpolate->type)
-                                ->Value();
-
-                        auto i_smpl = builtin::InterpolationSampling::kUndefined;
-                        if (interpolate->sampling) {
-                            i_smpl =
-                                sem.Get<sem::BuiltinEnumExpression<builtin::InterpolationSampling>>(
-                                       interpolate->sampling)
-                                    ->Value();
-                        }
-
-                        auto name = interpolation_to_attribute(i_type, i_smpl);
-                        if (name.empty()) {
-                            diagnostics_.add_error(diag::System::Writer,
-                                                   "unknown interpolation attribute");
-                            return false;
-                        }
-                        out << " [[" << name << "]]";
-                        return true;
-                    },
-                    [&](const ast::InvariantAttribute*) {
-                        if (invariant_define_name_.empty()) {
-                            invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
-                        }
-                        out << " " << invariant_define_name_;
-                        return true;
-                    },
-                    [&](const ast::StructMemberOffsetAttribute*) { return true; },
-                    [&](const ast::StructMemberAlignAttribute*) { return true; },
-                    [&](const ast::StructMemberSizeAttribute*) { return true; },
-                    [&](Default) {
-                        TINT_ICE(Writer, diagnostics_)
-                            << "unhandled struct member attribute: " << attr->Name();
-                        return false;
-                    });
-                if (!ok) {
-                    return false;
-                }
+        if (auto builtin = attributes.builtin) {
+            auto name = builtin_to_attribute(builtin.value());
+            if (name.empty()) {
+                diagnostics_.add_error(diag::System::Writer, "unknown builtin");
+                return false;
             }
+            out << " [[" << name << "]]";
+        }
+
+        if (auto location = attributes.location) {
+            auto& pipeline_stage_uses = str->PipelineStageUses();
+            if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                TINT_ICE(Writer, diagnostics_) << "invalid entry point IO struct uses";
+                return false;
+            }
+
+            if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexInput)) {
+                out << " [[attribute(" + std::to_string(location.value()) + ")]]";
+            } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kVertexOutput)) {
+                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+            } else if (pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentInput)) {
+                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+            } else if (TINT_LIKELY(
+                           pipeline_stage_uses.count(type::PipelineStageUsage::kFragmentOutput))) {
+                out << " [[color(" + std::to_string(location.value()) + ")]]";
+            } else {
+                TINT_ICE(Writer, diagnostics_) << "invalid use of location decoration";
+                return false;
+            }
+        }
+
+        if (auto interpolation = attributes.interpolation) {
+            auto name = interpolation_to_attribute(interpolation->type, interpolation->sampling);
+            if (name.empty()) {
+                diagnostics_.add_error(diag::System::Writer, "unknown interpolation attribute");
+                return false;
+            }
+            out << " [[" << name << "]]";
+        }
+
+        if (attributes.invariant) {
+            invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
+            out << " " << invariant_define_name_;
         }
 
         out << ";";
@@ -3223,7 +3185,7 @@
             return SizeAndAlign{};
         },
 
-        [&](const sem::Struct* str) {
+        [&](const type::Struct* str) {
             // TODO(crbug.com/tint/650): There's an assumption here that MSL's
             // default structure size and alignment matches WGSL's. We need to
             // confirm this.
diff --git a/src/tint/writer/msl/generator_impl.h b/src/tint/writer/msl/generator_impl.h
index 5699242..0be80be 100644
--- a/src/tint/writer/msl/generator_impl.h
+++ b/src/tint/writer/msl/generator_impl.h
@@ -340,7 +340,7 @@
     /// @param buffer the text buffer that the type declaration will be written to
     /// @param str the struct to generate
     /// @returns true if the struct is emitted
-    bool EmitStructType(TextBuffer* buffer, const sem::Struct* str);
+    bool EmitStructType(TextBuffer* buffer, const type::Struct* str);
     /// Handles a unary op expression
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
@@ -350,7 +350,7 @@
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
     bool EmitVar(const ast::Var* var);
-    /// Handles generating a function-scope 'let' declaration
+    /// Handles generating a 'let' declaration
     /// @param let the variable to generate
     /// @returns true if the variable was emitted
     bool EmitLet(const ast::Let* let);
@@ -418,7 +418,7 @@
     /// Name of atomicCompareExchangeWeak() helper for the given pointer storage
     /// class and struct return type
     using ACEWKeyType =
-        utils::UnorderedKeyWrapper<std::tuple<builtin::AddressSpace, const sem::Struct*>>;
+        utils::UnorderedKeyWrapper<std::tuple<builtin::AddressSpace, const type::Struct*>>;
     std::unordered_map<ACEWKeyType, std::string> atomicCompareExchangeWeak_;
 
     /// Unique name of the 'TINT_INVARIANT' preprocessor define.
@@ -440,7 +440,7 @@
     std::unordered_map<const sem::Builtin*, std::string> builtins_;
     std::unordered_map<const type::Type*, std::string> unary_minus_funcs_;
     std::unordered_map<uint32_t, std::string> int_dot_funcs_;
-    std::unordered_set<const sem::Struct*> emitted_structs_;
+    std::unordered_set<const type::Struct*> emitted_structs_;
 };
 
 }  // namespace tint::writer::msl
diff --git a/src/tint/writer/msl/generator_impl_type_test.cc b/src/tint/writer/msl/generator_impl_type_test.cc
index 3208708..3b9b652 100644
--- a/src/tint/writer/msl/generator_impl_type_test.cc
+++ b/src/tint/writer/msl/generator_impl_type_test.cc
@@ -245,8 +245,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(s)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
   float b;
@@ -292,8 +292,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
 
     // ALL_FIELDS() calls the macro FIELD(ADDR, TYPE, ARRAY_COUNT, NAME)
     // for each field of the structure s.
@@ -401,8 +401,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
 
     // ALL_FIELDS() calls the macro FIELD(ADDR, TYPE, ARRAY_COUNT, NAME)
     // for each field of the structure s.
@@ -493,8 +493,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
 
     // ALL_FIELDS() calls the macro FIELD(ADDR, TYPE, ARRAY_COUNT, NAME)
     // for each field of the structure s.
@@ -577,8 +577,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
 
     // ALL_FIELDS() calls the macro FIELD(ADDR, TYPE, ARRAY_COUNT, NAME)
     // for each field of the structure s.
@@ -639,8 +639,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
     EXPECT_EQ(buf.String(), R"(struct S {
   /* 0x0000 */ int tint_pad_2;
   /* 0x0004 */ tint_array<int8_t, 124> tint_pad_10;
@@ -698,8 +698,8 @@
     GeneratorImpl& gen = Build();
 
     TextGenerator::TextBuffer buf;
-    auto* sem_s = program->TypeOf(type)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    auto* str = program->TypeOf(type)->As<type::Struct>();
+    ASSERT_TRUE(gen.EmitStructType(&buf, str)) << gen.Diagnostics();
     EXPECT_EQ(buf.String(), R"(struct S {
   /* 0x0000 */ int a;
   /* 0x0004 */ float b;
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 4a5c90b..972ba7c 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -790,8 +790,8 @@
         ops.push_back(Operand(init_id));
     } else {
         auto* st = type->As<type::StorageTexture>();
-        if (st || type->Is<sem::Struct>()) {
-            // type is a sem::Struct or a type::StorageTexture
+        if (st || type->Is<type::Struct>()) {
+            // type is a type::Struct or a type::StorageTexture
             auto access = st ? st->access() : sem->Access();
             switch (access) {
                 case builtin::Access::kWrite:
@@ -1353,7 +1353,7 @@
         // If the result is not a vector then we should have validated that the
         // value type is a correctly sized vector so we can just use it directly.
         if (result_type == value_type || result_type->Is<type::Matrix>() ||
-            result_type->Is<type::Array>() || result_type->Is<sem::Struct>()) {
+            result_type->Is<type::Array>() || result_type->Is<type::Struct>()) {
             ops.push_back(Operand(id));
             continue;
         }
@@ -1715,7 +1715,7 @@
             }
             return composite(count.value());
         },
-        [&](const sem::Struct* s) { return composite(s->Members().Length()); },
+        [&](const type::Struct* s) { return composite(s->Members().Length()); },
         [&](Default) {
             error_ = "unhandled constant type: " + builder_.FriendlyName(ty);
             return 0;
@@ -2391,13 +2391,12 @@
             params.push_back(Operand(struct_id));
 
             auto* type = TypeOf(accessor->object)->UnwrapRef();
-            if (!type->Is<sem::Struct>()) {
+            if (!type->Is<type::Struct>()) {
                 error_ = "invalid type (" + type->FriendlyName() + ") for runtime array length";
                 return 0;
             }
             // Runtime array must be the last member in the structure
-            params.push_back(
-                Operand(uint32_t(type->As<sem::Struct>()->Declaration()->members.Length() - 1)));
+            params.push_back(Operand(uint32_t(type->As<type::Struct>()->Members().Length() - 1)));
 
             if (!push_function_inst(spv::Op::OpArrayLength, params)) {
                 return 0;
@@ -3702,7 +3701,7 @@
             [&](const type::Reference* ref) {  //
                 return GenerateReferenceType(ref, result);
             },
-            [&](const sem::Struct* str) {  //
+            [&](const type::Struct* str) {  //
                 return GenerateStructType(str, result);
             },
             [&](const type::U32*) {
@@ -3914,7 +3913,7 @@
     return true;
 }
 
-bool Builder::GenerateStructType(const sem::Struct* struct_type, const Operand& result) {
+bool Builder::GenerateStructType(const type::Struct* struct_type, const Operand& result) {
     auto struct_id = std::get<uint32_t>(result);
 
     if (struct_type->Name().IsValid()) {
@@ -3924,9 +3923,11 @@
     OperandList ops;
     ops.push_back(result);
 
-    auto* decl = struct_type->Declaration();
-    if (decl && ast::HasAttribute<transform::AddBlockAttribute::BlockAttribute>(decl->attributes)) {
-        push_annot(spv::Op::OpDecorate, {Operand(struct_id), U32Operand(SpvDecorationBlock)});
+    if (auto* sem_str = struct_type->As<sem::Struct>()) {
+        auto* decl = sem_str->Declaration();
+        if (ast::HasAttribute<transform::AddBlockAttribute::BlockAttribute>(decl->attributes)) {
+            push_annot(spv::Op::OpDecorate, {Operand(struct_id), U32Operand(SpvDecorationBlock)});
+        }
     }
 
     for (uint32_t i = 0; i < struct_type->Members().Length(); ++i) {
@@ -3944,7 +3945,7 @@
 
 uint32_t Builder::GenerateStructMember(uint32_t struct_id,
                                        uint32_t idx,
-                                       const sem::StructMember* member) {
+                                       const type::StructMember* member) {
     push_debug(spv::Op::OpMemberName,
                {Operand(struct_id), Operand(idx), Operand(member->Name().Name())});
 
diff --git a/src/tint/writer/spirv/builder.h b/src/tint/writer/spirv/builder.h
index 53fb108..1bbffc8 100644
--- a/src/tint/writer/spirv/builder.h
+++ b/src/tint/writer/spirv/builder.h
@@ -496,7 +496,7 @@
     /// @param struct_type the vector to generate
     /// @param result the result operand
     /// @returns true if the vector was successfully generated
-    bool GenerateStructType(const sem::Struct* struct_type, const Operand& result);
+    bool GenerateStructType(const type::Struct* struct_type, const Operand& result);
     /// Generates a struct member
     /// @param struct_id the id of the parent structure
     /// @param idx the index of the member
@@ -504,7 +504,7 @@
     /// @returns the id of the struct member or 0 on error.
     uint32_t GenerateStructMember(uint32_t struct_id,
                                   uint32_t idx,
-                                  const sem::StructMember* member);
+                                  const type::StructMember* member);
     /// Generates a variable declaration statement
     /// @param stmt the statement to generate
     /// @returns true on successfull generation
diff --git a/src/tint/writer/text_generator.cc b/src/tint/writer/text_generator.cc
index 8d18b9a..13a75d5 100644
--- a/src/tint/writer/text_generator.cc
+++ b/src/tint/writer/text_generator.cc
@@ -30,7 +30,7 @@
     return builder_.Symbols().New(prefix).Name();
 }
 
-std::string TextGenerator::StructName(const sem::Struct* s) {
+std::string TextGenerator::StructName(const type::Struct* s) {
     auto name = s->Name().Name();
     if (name.size() > 1 && name[0] == '_' && name[1] == '_') {
         name = utils::GetOrCreate(builtin_struct_names_, s,
diff --git a/src/tint/writer/text_generator.h b/src/tint/writer/text_generator.h
index 34b93d4..04fe785 100644
--- a/src/tint/writer/text_generator.h
+++ b/src/tint/writer/text_generator.h
@@ -112,7 +112,7 @@
     /// structures that start with double underscores. If the structure is a
     /// builtin, then the returned name will be a unique name without the leading
     /// underscores.
-    std::string StructName(const sem::Struct* s);
+    std::string StructName(const type::Struct* s);
 
     /// @param str the string
     /// @param suffix the suffix to remove
@@ -221,7 +221,7 @@
     /// The primary text buffer that the generator will emit
     TextBuffer main_buffer_;
     /// Map of builtin structure to unique generated name
-    std::unordered_map<const sem::Struct*, std::string> builtin_struct_names_;
+    std::unordered_map<const type::Struct*, std::string> builtin_struct_names_;
 };
 
 }  // namespace tint::writer
diff --git a/tint_overrides_with_defaults.gni b/tint_overrides_with_defaults.gni
index c54915d..977e433 100644
--- a/tint_overrides_with_defaults.gni
+++ b/tint_overrides_with_defaults.gni
@@ -77,6 +77,11 @@
     tint_build_syntax_tree_writer = false
   }
 
+  # Build the Tint IR
+  if (!defined(tint_build_ir)) {
+    tint_build_ir = false
+  }
+
   # Build unittests
   if (!defined(tint_build_unittests)) {
     tint_build_unittests = true