Import Tint changes from Dawn

Changes:
  - 2e11963cafd801f8749609cd5c00250b664312a5 D3D12: Skip robustness transform on textures by Jiawei Shao <jiawei.shao@intel.com>
  - 3ef08efc818b130e5aa46bb0c23fcbcec3b4d2c3 Add Partial Tint Dual Source Blending Extension by Brandon Jones <brandon1.jones@intel.com>
  - 4dc97b404b088d3094f04ed08ec002e53d29f220 [ir] Add a helper for creating struct types by James Price <jrprice@google.com>
  - 9397714f67830d5745c3bb871e51e5bb3f857470 [tint][ir] Rename Case::start -> Case::block by Ben Clayton <bclayton@google.com>
  - c8f07088e47520a6d20efe9a7929c35c2c1c2c52 [tint][ir] Make ControlInstructions regular instructions by Ben Clayton <bclayton@google.com>
  - 2649bd008951a70fd0472dede0df517ab5d5eed8 Vulkan: Skip image robustness transform with VK_EXT_robus... by Jiawei Shao <jiawei.shao@intel.com>
  - 5702df1d368de93eddebb815b482567bd48f39c1 [spirv-reader] Detect constant vector splats by James Price <jrprice@google.com>
  - 4915adcccaf98049d9e03e7306d03eec1c598310 [tint][ir] Add Unreachable instruction by Ben Clayton <bclayton@google.com>
  - 1104ad1c9b9108a0e25fee0ad53b0f6b9cbe489a [spirv-reader] Emit zero value constructors by James Price <jrprice@google.com>
  - e38419aac1628f5a012fe47dadb3e3d1f70490ea [spirv-reader] Detect vector splats by James Price <jrprice@google.com>
  - 80f0a172352efeadfdaaa7e4da303c5101579a76 [spirv-reader] Emit multi-component swizzles by James Price <jrprice@google.com>
  - b62d00861d4823e9f072f998239343ae7f31239b [tint][ir] Add Instruction::Destroy() by Ben Clayton <bclayton@google.com>
  - ab0cbed26bb0cb086734dcca58db9aace020d5b7 [tint][ir] Add Destroy(), Alive(), ReplaceAllUsesWith() t... by Ben Clayton <bclayton@google.com>
  - a059e59780d140490a337377bcd4f00b28923d50 [tint][ir] Add Builder::Exit() helper by Ben Clayton <bclayton@google.com>
  - 27c6d9801ab38f1b6ef7b21003ac1cc52641325d Add static assert: const-eval depends on inf and NaN supp... by David Neto <dneto@google.com>
  - c801286132eff43707a13fad9867b163beef8cb8 [ir][spirv-writer] Enable capabilities for F16 by James Price <jrprice@google.com>
  - 4187a53d41dc8e5d8bd8344a07e70554990e097f [ir] Validate transform test input and output by James Price <jrprice@google.com>
  - b13184bb85a1a4b2d5047b6c751b1b0c3ce0aa35 [tint][utils] Add Any(), All() and list ctor to Hashset by Ben Clayton <bclayton@google.com>
  - afa5617f6fd288a16b7755aa03dbafe330216ec4 [ir][spirv-writer] Add MergeReturn transform by James Price <jrprice@google.com>
  - aabfc4d85bc39c3ff28670d22cfada276b94860a [ir][spirv-writer] Emit construct instructions by James Price <jrprice@google.com>
  - 0d80c3d66152a0211cd64c08929335cc520d23cd [ir] Sever instruction and value. by dan sinclair <dsinclair@chromium.org>
  - 367fad852a6c48f9b0691d756c2fe7150c056ead [ir] Spit `instruction` and `value` allocators. by dan sinclair <dsinclair@chromium.org>
  - 525b6f5f76e2ddad10c4aa6e3b261d58bfa05a4b [ir] Add Result objects into the instructions by dan sinclair <dsinclair@chromium.org>
  - 4934dc0544f8d39db30f791337fbcf94b8c9301c [ir] Update `Value` parameters. by dan sinclair <dsinclair@chromium.org>
  - 138d5b7b9f6f2e394b95b040ffd7b041cbba7831 [tint] Move 'using namespace' into anonymous namespace by Ben Clayton <bclayton@google.com>
  - b839f673ac7a118aac551959d118aa61b0d77fb6 [ir] Make function an operand of return by James Price <jrprice@google.com>
  - 66805b0c0e45eaf4ef95d60d26c32b16ff6ae639 [tint] Migrate to 'fluent types' by Ben Clayton <bclayton@google.com>
  - a3390da8f5a22a23bf490e3d0fcfc147e73ba905 [ir] Add Return::Value() to get the return value by James Price <jrprice@google.com>
  - 19f92f9cdeab4e4f888d78ad7a3fd5819fdc2a7d [ir] Convert tests to block builder. by dan sinclair <dsinclair@chromium.org>
  - 2ef231183fbaf6e2315891257eb72b0a2071739e [ir] Allow builder to auto-append instructions. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 2e11963cafd801f8749609cd5c00250b664312a5
Change-Id: I51fe8a11246dfafc61a9551dc7f6d23870ace1a0
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/137400
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 2b450fb..1d71177 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -201,8 +201,6 @@
 
 libtint_source_set("libtint_symbols_src") {
   sources = [
-    "number.cc",
-    "number.h",
     "program_id.cc",
     "program_id.h",
     "reflection.h",
@@ -523,6 +521,8 @@
       "ir/transform/add_empty_entry_point.h",
       "ir/transform/block_decorated_structs.cc",
       "ir/transform/block_decorated_structs.h",
+      "ir/transform/merge_return.cc",
+      "ir/transform/merge_return.h",
       "ir/transform/var_for_dynamic_index.cc",
       "ir/transform/var_for_dynamic_index.h",
     ]
@@ -577,6 +577,7 @@
     "ast/if_statement.h",
     "ast/increment_decrement_statement.h",
     "ast/index_accessor_expression.h",
+    "ast/index_attribute.h",
     "ast/int_literal_expression.h",
     "ast/internal_attribute.h",
     "ast/interpolate_attribute.h",
@@ -666,6 +667,7 @@
     "ast/if_statement.cc",
     "ast/increment_decrement_statement.cc",
     "ast/index_accessor_expression.cc",
+    "ast/index_attribute.cc",
     "ast/int_literal_expression.cc",
     "ast/internal_attribute.cc",
     "ast/interpolate_attribute.cc",
@@ -812,6 +814,7 @@
     "builtin/diagnostic_severity.h",
     "builtin/extension.cc",
     "builtin/extension.h",
+    "builtin/fluent_types.h",
     "builtin/function.cc",
     "builtin/function.h",
     "builtin/interpolation.h",
@@ -819,6 +822,8 @@
     "builtin/interpolation_sampling.h",
     "builtin/interpolation_type.cc",
     "builtin/interpolation_type.h",
+    "builtin/number.cc",
+    "builtin/number.h",
     "builtin/texel_format.cc",
     "builtin/texel_format.h",
   ]
@@ -920,6 +925,7 @@
     "constant/value.h",
   ]
   deps = [
+    ":libtint_builtins_src",
     ":libtint_symbols_src",
     ":libtint_type_src",
     ":libtint_utils_src",
@@ -1252,6 +1258,8 @@
       "ir/disassembler.h",
       "ir/discard.cc",
       "ir/discard.h",
+      "ir/exit.cc",
+      "ir/exit.h",
       "ir/exit_if.cc",
       "ir/exit_if.h",
       "ir/exit_loop.cc",
@@ -1266,6 +1274,8 @@
       "ir/if.h",
       "ir/instruction.cc",
       "ir/instruction.h",
+      "ir/instruction_result.cc",
+      "ir/instruction_result.h",
       "ir/load.cc",
       "ir/load.h",
       "ir/location.h",
@@ -1291,6 +1301,8 @@
       "ir/transform/transform.h",
       "ir/unary.cc",
       "ir/unary.h",
+      "ir/unreachable.cc",
+      "ir/unreachable.h",
       "ir/user_call.cc",
       "ir/user_call.h",
       "ir/validate.cc",
@@ -1554,6 +1566,7 @@
       "ast/if_statement_test.cc",
       "ast/increment_decrement_statement_test.cc",
       "ast/index_accessor_expression_test.cc",
+      "ast/index_attribute_test.cc",
       "ast/int_literal_expression_test.cc",
       "ast/interpolate_attribute_test.cc",
       "ast/location_attribute_test.cc",
@@ -1604,6 +1617,7 @@
       "builtin/extension_test.cc",
       "builtin/interpolation_sampling_test.cc",
       "builtin/interpolation_type_test.cc",
+      "builtin/number_test.cc",
       "builtin/texel_format_test.cc",
     ]
     deps = [ ":libtint_builtins_src" ]
@@ -1662,6 +1676,7 @@
       "resolver/control_block_validation_test.cc",
       "resolver/dependency_graph_test.cc",
       "resolver/diagnostic_control_test.cc",
+      "resolver/dual_source_blending_extension_test.cc",
       "resolver/entry_point_validation_test.cc",
       "resolver/evaluation_stage_test.cc",
       "resolver/expression_kind_test.cc",
@@ -1825,6 +1840,7 @@
       sources = [
         "ir/transform/add_empty_entry_point_test.cc",
         "ir/transform/block_decorated_structs_test.cc",
+        "ir/transform/merge_return_test.cc",
         "ir/transform/test_helper.h",
         "ir/transform/var_for_dynamic_index_test.cc",
       ]
@@ -1998,6 +2014,7 @@
         "writer/spirv/ir/generator_impl_ir_binary_test.cc",
         "writer/spirv/ir/generator_impl_ir_builtin_test.cc",
         "writer/spirv/ir/generator_impl_ir_constant_test.cc",
+        "writer/spirv/ir/generator_impl_ir_construct_test.cc",
         "writer/spirv/ir/generator_impl_ir_function_test.cc",
         "writer/spirv/ir/generator_impl_ir_if_test.cc",
         "writer/spirv/ir/generator_impl_ir_loop_test.cc",
@@ -2256,6 +2273,7 @@
 
     deps = [
       ":libtint_ast_transform_src",
+      ":libtint_builtins_src",
       ":libtint_glsl_writer_src",
       ":libtint_symbols_src",
       ":libtint_transform_manager_src",
@@ -2266,7 +2284,6 @@
 
   tint_unittests_source_set("tint_unittests_symbols_src") {
     sources = [
-      "number_test.cc",
       "reflection_test.cc",
       "scope_stack_test.cc",
       "symbol_table_test.cc",
@@ -2331,6 +2348,7 @@
         "ir/function_param_test.cc",
         "ir/function_test.cc",
         "ir/if_test.cc",
+        "ir/instruction_result_test.cc",
         "ir/instruction_test.cc",
         "ir/ir_test_helper.h",
         "ir/load_test.cc",
@@ -2338,6 +2356,7 @@
         "ir/module_test.cc",
         "ir/multi_in_block_test.cc",
         "ir/next_iteration_test.cc",
+        "ir/operand_instruction_test.cc",
         "ir/program_test_helper.h",
         "ir/return_test.cc",
         "ir/store_test.cc",
@@ -2347,6 +2366,7 @@
         "ir/unary_test.cc",
         "ir/user_call_test.cc",
         "ir/validate_test.cc",
+        "ir/value_test.cc",
         "ir/var_test.cc",
       ]
 
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 6e6d760..5c0bf6f 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -58,14 +58,14 @@
 add_library(tint_diagnostic_utils
   debug.cc
   debug.h
-  source.h
-  source.cc
   diagnostic/diagnostic.cc
   diagnostic/diagnostic.h
   diagnostic/formatter.cc
   diagnostic/formatter.h
   diagnostic/printer.cc
   diagnostic/printer.h
+  source.cc
+  source.h
   utils/debugger.cc
   utils/debugger.h
   utils/unicode.cc
@@ -158,6 +158,8 @@
   ast/increment_decrement_statement.h
   ast/index_accessor_expression.cc
   ast/index_accessor_expression.h
+  ast/index_attribute.cc
+  ast/index_attribute.h
   ast/int_literal_expression.cc
   ast/int_literal_expression.h
   ast/internal_attribute.cc
@@ -232,6 +234,9 @@
   ast/while_statement.h
   ast/workgroup_attribute.cc
   ast/workgroup_attribute.h
+  builtin/fluent_types.h
+  builtin/number.cc
+  builtin/number.h
   clone_context.cc
   clone_context.h
   constant/clone_context.h
@@ -255,8 +260,6 @@
   inspector/resource_binding.h
   inspector/scalar.cc
   inspector/scalar.h
-  number.cc
-  number.h
   program_builder.cc
   program_builder.h
   program_id.cc
@@ -761,6 +764,8 @@
     ir/exit_loop.h
     ir/exit_switch.cc
     ir/exit_switch.h
+    ir/exit.cc
+    ir/exit.h
     ir/from_program.cc
     ir/from_program.h
     ir/function.cc
@@ -771,6 +776,8 @@
     ir/if.h
     ir/instruction.cc
     ir/instruction.h
+    ir/instruction_result.cc
+    ir/instruction_result.h
     ir/load.cc
     ir/load.h
     ir/location.h
@@ -796,6 +803,8 @@
     ir/to_program.h
     ir/unary.cc
     ir/unary.h
+    ir/unreachable.cc
+    ir/unreachable.h
     ir/user_call.cc
     ir/user_call.h
     ir/validate.cc
@@ -808,6 +817,8 @@
     ir/transform/add_empty_entry_point.h
     ir/transform/block_decorated_structs.cc
     ir/transform/block_decorated_structs.h
+    ir/transform/merge_return.cc
+    ir/transform/merge_return.h
     ir/transform/transform.cc
     ir/transform/transform.h
     ir/transform/var_for_dynamic_index.cc
@@ -930,6 +941,7 @@
     ast/if_statement_test.cc
     ast/increment_decrement_statement_test.cc
     ast/index_accessor_expression_test.cc
+    ast/index_attribute_test.cc
     ast/int_literal_expression_test.cc
     ast/interpolate_attribute_test.cc
     ast/location_attribute_test.cc
@@ -955,6 +967,7 @@
     ast/variable_test.cc
     ast/while_statement_test.cc
     ast/workgroup_attribute_test.cc
+    builtin/number_test.cc
     clone_context_test.cc
     constant/composite_test.cc
     constant/manager_test.cc
@@ -965,7 +978,6 @@
     diagnostic/diagnostic_test.cc
     diagnostic/formatter_test.cc
     diagnostic/printer_test.cc
-    number_test.cc
     program_builder_test.cc
     program_test.cc
     reflection_test.cc
@@ -999,6 +1011,7 @@
     resolver/control_block_validation_test.cc
     resolver/dependency_graph_test.cc
     resolver/diagnostic_control_test.cc
+    resolver/dual_source_blending_extension_test.cc
     resolver/entry_point_validation_test.cc
     resolver/evaluation_stage_test.cc
     resolver/expression_kind_test.cc
@@ -1285,6 +1298,7 @@
         writer/spirv/ir/generator_impl_ir_binary_test.cc
         writer/spirv/ir/generator_impl_ir_builtin_test.cc
         writer/spirv/ir/generator_impl_ir_constant_test.cc
+        writer/spirv/ir/generator_impl_ir_construct_test.cc
         writer/spirv/ir/generator_impl_ir_function_test.cc
         writer/spirv/ir/generator_impl_ir_if_test.cc
         writer/spirv/ir/generator_impl_ir_loop_test.cc
@@ -1532,6 +1546,7 @@
       ir/function_param_test.cc
       ir/function_test.cc
       ir/if_test.cc
+      ir/instruction_result_test.cc
       ir/instruction_test.cc
       ir/ir_test_helper.h
       ir/load_test.cc
@@ -1539,6 +1554,7 @@
       ir/module_test.cc
       ir/multi_in_block_test.cc
       ir/next_iteration_test.cc
+      ir/operand_instruction_test.cc
       ir/program_test_helper.h
       ir/return_test.cc
       ir/store_test.cc
@@ -1546,10 +1562,12 @@
       ir/swizzle_test.cc
       ir/transform/add_empty_entry_point_test.cc
       ir/transform/block_decorated_structs_test.cc
+      ir/transform/merge_return_test.cc
       ir/transform/var_for_dynamic_index_test.cc
       ir/unary_test.cc
       ir/user_call_test.cc
       ir/validate_test.cc
+      ir/value_test.cc
       ir/var_test.cc
     )
   endif()
diff --git a/src/tint/ast/builtin_texture_helper_test.cc b/src/tint/ast/builtin_texture_helper_test.cc
index 3231081..5c548db 100644
--- a/src/tint/ast/builtin_texture_helper_test.cc
+++ b/src/tint/ast/builtin_texture_helper_test.cc
@@ -20,11 +20,12 @@
 #include "src/tint/type/sampled_texture.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::test {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 utils::StringStream& operator<<(utils::StringStream& out, const TextureKind& kind) {
     switch (kind) {
         case TextureKind::kRegular:
@@ -492,10 +493,10 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_i,                      // component
-                                   kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f));  // coords
+                return b->ExprList(0_i,                            // component
+                                   kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f));  // coords
             },
             /* returns value */ true,
         },
@@ -512,11 +513,11 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_u,                      // component
-                                   kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<i32>(3_i, 4_i));  // offset
+                return b->ExprList(0_u,                            // component
+                                   kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<i32>>(3_i, 4_i));  // offset
             },
             /* returns value */ true,
         },
@@ -533,11 +534,11 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_i,                     // component
-                                   kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i);                    // array index
+                return b->ExprList(0_i,                           // component
+                                   kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i);                          // array index
             },
             /* returns value */ true,
         },
@@ -555,12 +556,12 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_u,                      // component
-                                   kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_u,                      // array_index
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(0_u,                            // component
+                                   kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_u,                            // array_index
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -576,10 +577,10 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_i,                           // component
-                                   kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f));  // coords
+                return b->ExprList(0_i,                                 // component
+                                   kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f));  // coords
             },
             /* returns value */ true,
         },
@@ -596,11 +597,11 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(0_u,                          // component
-                                   kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_u);                         // array_index
+                return b->ExprList(0_u,                                // component
+                                   kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_u);                               // array_index
             },
             /* returns value */ true,
         },
@@ -615,9 +616,9 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f));  // coords
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f));  // coords
             },
             /* returns value */ true,
         },
@@ -633,10 +634,10 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<i32>(3_i, 4_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<i32>>(3_i, 4_i));  // offset
             },
             /* returns value */ true,
         },
@@ -652,10 +653,10 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_u);                    // array_index
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_u);                          // array_index
             },
             /* returns value */ true,
         },
@@ -672,11 +673,11 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -691,9 +692,9 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f));  // coords
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f));  // coords
             },
             /* returns value */ true,
         },
@@ -709,10 +710,10 @@
             TextureDataType::kF32,
             "textureGather",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_u);                         // array_index
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_u);                               // array_index
             },
             /* returns value */ true,
         },
@@ -728,10 +729,10 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -748,11 +749,11 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_f,                      // depth_ref
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -769,11 +770,11 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i,                     // array_index
-                                   4_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i,                           // array_index
+                                   4_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -791,12 +792,12 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   4_f,                      // depth_ref
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   4_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -812,10 +813,10 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -832,11 +833,11 @@
             TextureDataType::kF32,
             "textureGatherCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_u,                          // array_index
-                                   5_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_u,                                // array_index
+                                   5_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -1044,9 +1045,9 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f));  // coords
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f));  // coords
             },
             /* returns value */ true,
         },
@@ -1062,10 +1063,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<i32>(3_i, 4_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<i32>>(3_i, 4_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1081,10 +1082,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i);                    // array_index
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i);                          // array_index
             },
             /* returns value */ true,
         },
@@ -1101,11 +1102,11 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_u,                      // array_index
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_u,                            // array_index
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1120,9 +1121,9 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f));  // coords
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f));  // coords
             },
             /* returns value */ true,
         },
@@ -1138,10 +1139,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   b->vec3<i32>(4_i, 5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   b->Call<vec3<i32>>(4_i, 5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1156,9 +1157,9 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f));  // coords
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f));  // coords
             },
             /* returns value */ true,
         },
@@ -1174,10 +1175,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i);                         // array_index
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i);                               // array_index
             },
             /* returns value */ true,
         },
@@ -1192,9 +1193,9 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f));  // coords
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f));  // coords
             },
             /* returns value */ true,
         },
@@ -1210,10 +1211,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<i32>(3_i, 4_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<i32>>(3_i, 4_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1229,10 +1230,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i);                    // array_index
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i);                          // array_index
             },
             /* returns value */ true,
         },
@@ -1249,11 +1250,11 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1268,9 +1269,9 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f));  // coords
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f));  // coords
             },
             /* returns value */ true,
         },
@@ -1286,10 +1287,10 @@
             TextureDataType::kF32,
             "textureSample",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_u);                         // array_index
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_u);                               // array_index
             },
             /* returns value */ true,
         },
@@ -1305,10 +1306,10 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_f);                    // bias
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_f);                          // bias
             },
             /* returns value */ true,
         },
@@ -1325,11 +1326,11 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_f,                      // bias
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_f,                            // bias
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1346,11 +1347,11 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   4_u,                     // array_index
-                                   3_f);                    // bias
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   4_u,                           // array_index
+                                   3_f);                          // bias
             },
             /* returns value */ true,
         },
@@ -1368,12 +1369,12 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   4_f,                      // bias
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   4_f,                            // bias
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1389,10 +1390,10 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // bias
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // bias
             },
             /* returns value */ true,
         },
@@ -1409,11 +1410,11 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   4_f,                           // bias
-                                   b->vec3<i32>(5_i, 6_i, 7_i));  // offset
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   4_f,                                 // bias
+                                   b->Call<vec3<i32>>(5_i, 6_i, 7_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1429,10 +1430,10 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // bias
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // bias
             },
             /* returns value */ true,
         },
@@ -1449,11 +1450,11 @@
             TextureDataType::kF32,
             "textureSampleBias",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   3_i,                          // array_index
-                                   4_f);                         // bias
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   3_i,                                // array_index
+                                   4_f);                               // bias
             },
             /* returns value */ true,
         },
@@ -1469,10 +1470,10 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_f);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_f);                          // level
             },
             /* returns value */ true,
         },
@@ -1489,11 +1490,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_f,                      // level
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_f,                            // level
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1510,11 +1511,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i,                     // array_index
-                                   4_f);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i,                           // array_index
+                                   4_f);                          // level
             },
             /* returns value */ true,
         },
@@ -1532,12 +1533,12 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   4_f,                      // level
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   4_f,                            // level
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1553,10 +1554,10 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // level
             },
             /* returns value */ true,
         },
@@ -1573,11 +1574,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   4_f,                           // level
-                                   b->vec3<i32>(5_i, 6_i, 7_i));  // offset
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   4_f,                                 // level
+                                   b->Call<vec3<i32>>(5_i, 6_i, 7_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1593,10 +1594,10 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // level
             },
             /* returns value */ true,
         },
@@ -1613,11 +1614,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i,                          // array_index
-                                   5_f);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i,                                // array_index
+                                   5_f);                               // level
             },
             /* returns value */ true,
         },
@@ -1633,10 +1634,10 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_u);                          // level
             },
             /* returns value */ true,
         },
@@ -1653,11 +1654,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // level
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // level
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1674,11 +1675,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_u,                     // array_index
-                                   4_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_u,                           // array_index
+                                   4_u);                          // level
             },
             /* returns value */ true,
         },
@@ -1696,12 +1697,12 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_u,                      // array_index
-                                   4_u,                      // level
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_u,                            // array_index
+                                   4_u,                            // level
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1717,10 +1718,10 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i);                               // level
             },
             /* returns value */ true,
         },
@@ -1737,11 +1738,11 @@
             TextureDataType::kF32,
             "textureSampleLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i,                          // array_index
-                                   5_i);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i,                                // array_index
+                                   5_i);                               // level
             },
             /* returns value */ true,
         },
@@ -1758,11 +1759,11 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<f32>(3_f, 4_f),   // ddx
-                                   b->vec2<f32>(5_f, 6_f));  // ddy
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<f32>>(3_f, 4_f),   // ddx
+                                   b->Call<vec2<f32>>(5_f, 6_f));  // ddy
             },
             /* returns value */ true,
         },
@@ -1780,12 +1781,12 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   b->vec2<f32>(3_f, 4_f),   // ddx
-                                   b->vec2<f32>(5_f, 6_f),   // ddy
-                                   b->vec2<i32>(7_i, 7_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   b->Call<vec2<f32>>(3_f, 4_f),   // ddx
+                                   b->Call<vec2<f32>>(5_f, 6_f),   // ddy
+                                   b->Call<vec2<i32>>(7_i, 7_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1803,12 +1804,12 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   b->vec2<f32>(4_f, 5_f),   // ddx
-                                   b->vec2<f32>(6_f, 7_f));  // ddy
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   b->Call<vec2<f32>>(4_f, 5_f),   // ddx
+                                   b->Call<vec2<f32>>(6_f, 7_f));  // ddy
             },
             /* returns value */ true,
         },
@@ -1827,13 +1828,13 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_u,                      // array_index
-                                   b->vec2<f32>(4_f, 5_f),   // ddx
-                                   b->vec2<f32>(6_f, 7_f),   // ddy
-                                   b->vec2<i32>(6_i, 7_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_u,                            // array_index
+                                   b->Call<vec2<f32>>(4_f, 5_f),   // ddx
+                                   b->Call<vec2<f32>>(6_f, 7_f),   // ddy
+                                   b->Call<vec2<i32>>(6_i, 7_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1850,11 +1851,11 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   b->vec3<f32>(4_f, 5_f, 6_f),   // ddx
-                                   b->vec3<f32>(7_f, 8_f, 9_f));  // ddy
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   b->Call<vec3<f32>>(4_f, 5_f, 6_f),   // ddx
+                                   b->Call<vec3<f32>>(7_f, 8_f, 9_f));  // ddy
             },
             /* returns value */ true,
         },
@@ -1872,12 +1873,12 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   b->vec3<f32>(4_f, 5_f, 6_f),   // ddx
-                                   b->vec3<f32>(7_f, 8_f, 9_f),   // ddy
-                                   b->vec3<i32>(0_i, 1_i, 2_i));  // offset
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   b->Call<vec3<f32>>(4_f, 5_f, 6_f),   // ddx
+                                   b->Call<vec3<f32>>(7_f, 8_f, 9_f),   // ddy
+                                   b->Call<vec3<i32>>(0_i, 1_i, 2_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1894,11 +1895,11 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                  // t
-                                   kSamplerName,                  // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),   // coords
-                                   b->vec3<f32>(4_f, 5_f, 6_f),   // ddx
-                                   b->vec3<f32>(7_f, 8_f, 9_f));  // ddy
+                return b->ExprList(kTextureName,                        // t
+                                   kSamplerName,                        // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),   // coords
+                                   b->Call<vec3<f32>>(4_f, 5_f, 6_f),   // ddx
+                                   b->Call<vec3<f32>>(7_f, 8_f, 9_f));  // ddy
             },
             /* returns value */ true,
         },
@@ -1916,12 +1917,12 @@
             TextureDataType::kF32,
             "textureSampleGrad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                   // t
-                                   kSamplerName,                   // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),    // coords
-                                   4_u,                            // array_index
-                                   b->vec3<f32>(5_f, 6_f, 7_f),    // ddx
-                                   b->vec3<f32>(8_f, 9_f, 10_f));  // ddy
+                return b->ExprList(kTextureName,                         // t
+                                   kSamplerName,                         // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),    // coords
+                                   4_u,                                  // array_index
+                                   b->Call<vec3<f32>>(5_f, 6_f, 7_f),    // ddx
+                                   b->Call<vec3<f32>>(8_f, 9_f, 10_f));  // ddy
             },
             /* returns value */ true,
         },
@@ -1937,10 +1938,10 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -1957,11 +1958,11 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_f,                      // depth_ref
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -1978,11 +1979,11 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   4_i,                     // array_index
-                                   3_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   4_i,                           // array_index
+                                   3_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -2000,12 +2001,12 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   4_u,                      // array_index
-                                   3_f,                      // depth_ref
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   4_u,                            // array_index
+                                   3_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -2021,10 +2022,10 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -2041,11 +2042,11 @@
             TextureDataType::kF32,
             "textureSampleCompare",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i,                          // array_index
-                                   5_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i,                                // array_index
+                                   5_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -2061,10 +2062,10 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -2081,11 +2082,11 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_f,                      // depth_ref
-                                   b->vec2<i32>(4_i, 5_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(4_i, 5_i));  // offset
             },
             /* returns value */ true,
         },
@@ -2102,11 +2103,11 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   kSamplerName,            // s
-                                   b->vec2<f32>(1_f, 2_f),  // coords
-                                   3_i,                     // array_index
-                                   4_f);                    // depth_ref
+                return b->ExprList(kTextureName,                  // t
+                                   kSamplerName,                  // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),  // coords
+                                   3_i,                           // array_index
+                                   4_f);                          // depth_ref
             },
             /* returns value */ true,
         },
@@ -2124,12 +2125,12 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,             // t
-                                   kSamplerName,             // s
-                                   b->vec2<f32>(1_f, 2_f),   // coords
-                                   3_i,                      // array_index
-                                   4_f,                      // depth_ref
-                                   b->vec2<i32>(5_i, 6_i));  // offset
+                return b->ExprList(kTextureName,                   // t
+                                   kSamplerName,                   // s
+                                   b->Call<vec2<f32>>(1_f, 2_f),   // coords
+                                   3_i,                            // array_index
+                                   4_f,                            // depth_ref
+                                   b->Call<vec2<i32>>(5_i, 6_i));  // offset
             },
             /* returns value */ true,
         },
@@ -2145,10 +2146,10 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -2165,11 +2166,11 @@
             TextureDataType::kF32,
             "textureSampleCompareLevel",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   kSamplerName,                 // s
-                                   b->vec3<f32>(1_f, 2_f, 3_f),  // coords
-                                   4_i,                          // array_index
-                                   5_f);                         // depth_ref
+                return b->ExprList(kTextureName,                       // t
+                                   kSamplerName,                       // s
+                                   b->Call<vec3<f32>>(1_f, 2_f, 3_f),  // coords
+                                   4_i,                                // array_index
+                                   5_f);                               // depth_ref
             },
             /* returns value */ true,
         },
@@ -2231,9 +2232,9 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u);                          // level
             },
             /* returns value */ true,
         },
@@ -2247,9 +2248,9 @@
             TextureDataType::kU32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i);                          // level
             },
             /* returns value */ true,
         },
@@ -2263,9 +2264,9 @@
             TextureDataType::kI32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u);                          // level
             },
             /* returns value */ true,
         },
@@ -2280,10 +2281,10 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i,                     // array_index
-                                   4_i);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i,                           // array_index
+                                   4_i);                          // level
             },
             /* returns value */ true,
         },
@@ -2298,10 +2299,10 @@
             TextureDataType::kU32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i,                     // array_index
-                                   4_i);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i,                           // array_index
+                                   4_i);                          // level
             },
             /* returns value */ true,
         },
@@ -2316,10 +2317,10 @@
             TextureDataType::kI32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u,                     // array_index
-                                   4_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u,                           // array_index
+                                   4_u);                          // level
             },
             /* returns value */ true,
         },
@@ -2333,9 +2334,9 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   b->vec3<i32>(1_i, 2_i, 3_i),  // coords
-                                   4_i);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   b->Call<vec3<i32>>(1_i, 2_i, 3_i),  // coords
+                                   4_i);                               // level
             },
             /* returns value */ true,
         },
@@ -2349,9 +2350,9 @@
             TextureDataType::kU32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   b->vec3<i32>(1_i, 2_i, 3_i),  // coords
-                                   4_i);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   b->Call<vec3<i32>>(1_i, 2_i, 3_i),  // coords
+                                   4_i);                               // level
             },
             /* returns value */ true,
         },
@@ -2365,9 +2366,9 @@
             TextureDataType::kI32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                 // t
-                                   b->vec3<u32>(1_u, 2_u, 3_u),  // coords
-                                   4_u);                         // level
+                return b->ExprList(kTextureName,                       // t
+                                   b->Call<vec3<u32>>(1_u, 2_u, 3_u),  // coords
+                                   4_u);                               // level
             },
             /* returns value */ true,
         },
@@ -2381,9 +2382,9 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i);                    // sample_index
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i);                          // sample_index
             },
             /* returns value */ true,
         },
@@ -2397,9 +2398,9 @@
             TextureDataType::kU32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i);                    // sample_index
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i);                          // sample_index
             },
             /* returns value */ true,
         },
@@ -2413,9 +2414,9 @@
             TextureDataType::kI32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u);                    // sample_index
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u);                          // sample_index
             },
             /* returns value */ true,
         },
@@ -2429,9 +2430,9 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<i32>(1_i, 2_i),  // coords
-                                   3_i);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),  // coords
+                                   3_i);                          // level
             },
             /* returns value */ true,
         },
@@ -2446,10 +2447,10 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u,                     // array_index
-                                   4_u);                    // level
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u,                           // array_index
+                                   4_u);                          // level
             },
             /* returns value */ true,
         },
@@ -2463,9 +2464,9 @@
             TextureDataType::kF32,
             "textureLoad",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,            // t
-                                   b->vec2<u32>(1_u, 2_u),  // coords
-                                   3_u);                    // sample_index
+                return b->ExprList(kTextureName,                  // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),  // coords
+                                   3_u);                          // sample_index
             },
             /* returns value */ true,
         },
@@ -2480,9 +2481,9 @@
             TextureDataType::kF32,
             "textureStore",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                       // t
-                                   1_i,                                // coords
-                                   b->vec4<f32>(2_f, 3_f, 4_f, 5_f));  // value
+                return b->ExprList(kTextureName,                             // t
+                                   1_i,                                      // coords
+                                   b->Call<vec4<f32>>(2_f, 3_f, 4_f, 5_f));  // value
             },
             /* returns value */ false,
         },
@@ -2497,9 +2498,9 @@
             TextureDataType::kF32,
             "textureStore",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                       // t
-                                   b->vec2<i32>(1_i, 2_i),             // coords
-                                   b->vec4<f32>(3_f, 4_f, 5_f, 6_f));  // value
+                return b->ExprList(kTextureName,                             // t
+                                   b->Call<vec2<i32>>(1_i, 2_i),             // coords
+                                   b->Call<vec4<f32>>(3_f, 4_f, 5_f, 6_f));  // value
             },
             /* returns value */ false,
         },
@@ -2515,10 +2516,10 @@
             TextureDataType::kF32,
             "textureStore",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                       // t
-                                   b->vec2<u32>(1_u, 2_u),             // coords
-                                   3_u,                                // array_index
-                                   b->vec4<f32>(4_f, 5_f, 6_f, 7_f));  // value
+                return b->ExprList(kTextureName,                             // t
+                                   b->Call<vec2<u32>>(1_u, 2_u),             // coords
+                                   3_u,                                      // array_index
+                                   b->Call<vec4<f32>>(4_f, 5_f, 6_f, 7_f));  // value
             },
             /* returns value */ false,
         },
@@ -2533,9 +2534,9 @@
             TextureDataType::kF32,
             "textureStore",
             [](ProgramBuilder* b) {
-                return b->ExprList(kTextureName,                       // t
-                                   b->vec3<u32>(1_u, 2_u, 3_u),        // coords
-                                   b->vec4<f32>(4_f, 5_f, 6_f, 7_f));  // value
+                return b->ExprList(kTextureName,                             // t
+                                   b->Call<vec3<u32>>(1_u, 2_u, 3_u),        // coords
+                                   b->Call<vec4<f32>>(4_f, 5_f, 6_f, 7_f));  // value
             },
             /* returns value */ false,
         },
diff --git a/src/tint/ast/index_attribute.cc b/src/tint/ast/index_attribute.cc
new file mode 100644
index 0000000..7916226
--- /dev/null
+++ b/src/tint/ast/index_attribute.cc
@@ -0,0 +1,41 @@
+// 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/ast/index_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAttribute);
+
+namespace tint::ast {
+
+IndexAttribute::IndexAttribute(ProgramID pid, NodeID nid, const Source& src, const Expression* exp)
+    : Base(pid, nid, src), expr(exp) {}
+
+IndexAttribute::~IndexAttribute() = default;
+
+std::string IndexAttribute::Name() const {
+    return "index";
+}
+
+const IndexAttribute* IndexAttribute::Clone(CloneContext* ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx->Clone(source);
+    auto* expr_ = ctx->Clone(expr);
+    return ctx->dst->create<IndexAttribute>(src, expr_);
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/index_attribute.h b/src/tint/ast/index_attribute.h
new file mode 100644
index 0000000..e241ba6
--- /dev/null
+++ b/src/tint/ast/index_attribute.h
@@ -0,0 +1,51 @@
+// 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_AST_INDEX_ATTRIBUTE_H_
+#define SRC_TINT_AST_INDEX_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/expression.h"
+
+namespace tint::ast {
+
+/// An id attribute for pipeline-overridable constants
+class IndexAttribute final : public utils::Castable<IndexAttribute, Attribute> {
+  public:
+    /// Create an index attribute.
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param expr the numeric id expression
+    IndexAttribute(ProgramID pid, NodeID nid, const Source& src, const Expression* expr);
+    ~IndexAttribute() override;
+
+    /// @returns the WGSL name for the attribute
+    std::string Name() const override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext`
+    /// `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const IndexAttribute* Clone(CloneContext* ctx) const override;
+
+    /// The id expression
+    const Expression* const expr;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_INDEX_ATTRIBUTE_H_
diff --git a/src/tint/ast/index_attribute_test.cc b/src/tint/ast/index_attribute_test.cc
new file mode 100644
index 0000000..bad37c6
--- /dev/null
+++ b/src/tint/ast/index_attribute_test.cc
@@ -0,0 +1,31 @@
+// 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/ast/id_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint::ast {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+using IndexAttributeTest = TestHelper;
+
+TEST_F(IndexAttributeTest, Creation) {
+    auto* d = Index(1_a);
+    EXPECT_TRUE(d->expr->Is<IntLiteralExpression>());
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/ast/transform/builtin_polyfill.cc b/src/tint/ast/transform/builtin_polyfill.cc
index 05f5da9..75d91b3 100644
--- a/src/tint/ast/transform/builtin_polyfill.cc
+++ b/src/tint/ast/transform/builtin_polyfill.cc
@@ -29,7 +29,8 @@
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/map.h"
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::BuiltinPolyfill);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::BuiltinPolyfill::Config);
@@ -767,7 +768,7 @@
         auto name = b.Symbols().New("tint_textureSampleBaseClampToEdge");
         auto body = utils::Vector{
             b.Decl(b.Let("dims", b.Call(b.ty.vec2<f32>(), b.Call("textureDimensions", "t", 0_a)))),
-            b.Decl(b.Let("half_texel", b.Div(b.vec2<f32>(0.5_a), "dims"))),
+            b.Decl(b.Let("half_texel", b.Div(b.Call<vec2<f32>>(0.5_a), "dims"))),
             b.Decl(
                 b.Let("clamped", b.Call("clamp", "coord", "half_texel", b.Sub(1_a, "half_texel")))),
             b.Return(b.Call("textureSampleLevel", "t", "s", "clamped", 0_a)),
@@ -814,7 +815,7 @@
         auto name = b.Symbols().New("tint_workgroupUniformLoad");
         b.Func(name,
                utils::Vector{
-                   b.Param("p", b.ty.ptr(builtin::AddressSpace::kWorkgroup, T(type))),
+                   b.Param("p", b.ty.ptr<workgroup>(T(type))),
                },
                T(type),
                utils::Vector{
diff --git a/src/tint/ast/transform/calculate_array_length.cc b/src/tint/ast/transform/calculate_array_length.cc
index 0e11ad4..9ce16da 100644
--- a/src/tint/ast/transform/calculate_array_length.cc
+++ b/src/tint/ast/transform/calculate_array_length.cc
@@ -35,12 +35,12 @@
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CalculateArrayLength);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CalculateArrayLength::BufferSizeIntrinsic);
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
-
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 bool ShouldRun(const Program* program) {
     for (auto* fn : program->AST().Functions()) {
         if (auto* sem_fn = program->Sem().Get(fn)) {
@@ -112,7 +112,7 @@
                        b.Param("buffer",
                                b.ty.ptr(buffer_type->AddressSpace(), type, buffer_type->Access()),
                                utils::Vector{disable_validation}),
-                       b.Param("result", b.ty.ptr(builtin::AddressSpace::kFunction, b.ty.u32())),
+                       b.Param("result", b.ty.ptr<function, u32>()),
                    },
                    b.ty.void_(), nullptr,
                    utils::Vector{
diff --git a/src/tint/ast/transform/decompose_strided_matrix_test.cc b/src/tint/ast/transform/decompose_strided_matrix_test.cc
index 4fa8017..f1646ff 100644
--- a/src/tint/ast/transform/decompose_strided_matrix_test.cc
+++ b/src/tint/ast/transform/decompose_strided_matrix_test.cc
@@ -24,11 +24,12 @@
 #include "src/tint/ast/transform/unshadow.h"
 #include "src/tint/program_builder.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using DecomposeStridedMatrixTest = TransformTest;
 
 TEST_F(DecomposeStridedMatrixTest, ShouldRunEmptyModule) {
@@ -356,15 +357,16 @@
                          });
     b.GlobalVar("s", b.ty.Of(S), builtin::AddressSpace::kStorage, builtin::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
-    b.Func("f", utils::Empty, b.ty.void_(),
-           utils::Vector{
-               b.Assign(b.MemberAccessor("s", "m"),
-                        b.mat2x2<f32>(b.vec2<f32>(1_f, 2_f), b.vec2<f32>(3_f, 4_f))),
-           },
-           utils::Vector{
-               b.Stage(PipelineStage::kCompute),
-               b.WorkgroupSize(1_i),
-           });
+    b.Func(
+        "f", utils::Empty, b.ty.void_(),
+        utils::Vector{
+            b.Assign(b.MemberAccessor("s", "m"),
+                     b.Call<mat2x2<f32>>(b.Call<vec2<f32>>(1_f, 2_f), b.Call<vec2<f32>>(3_f, 4_f))),
+        },
+        utils::Vector{
+            b.Stage(PipelineStage::kCompute),
+            b.WorkgroupSize(1_i),
+        });
 
     auto* expect = R"(
 struct S {
@@ -415,14 +417,15 @@
                          });
     b.GlobalVar("s", b.ty.Of(S), builtin::AddressSpace::kStorage, builtin::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
-    b.Func("f", utils::Empty, b.ty.void_(),
-           utils::Vector{
-               b.Assign(b.IndexAccessor(b.MemberAccessor("s", "m"), 1_i), b.vec2<f32>(1_f, 2_f)),
-           },
-           utils::Vector{
-               b.Stage(PipelineStage::kCompute),
-               b.WorkgroupSize(1_i),
-           });
+    b.Func(
+        "f", utils::Empty, b.ty.void_(),
+        utils::Vector{
+            b.Assign(b.IndexAccessor(b.MemberAccessor("s", "m"), 1_i), b.Call<vec2<f32>>(1_f, 2_f)),
+        },
+        utils::Vector{
+            b.Stage(PipelineStage::kCompute),
+            b.WorkgroupSize(1_i),
+        });
 
     auto* expect = R"(
 struct S {
@@ -482,8 +485,9 @@
                b.Decl(b.Let("x", b.Deref("b"))),
                b.Decl(b.Let("y", b.IndexAccessor(b.Deref("b"), 1_i))),
                b.Decl(b.Let("z", b.IndexAccessor("x", 1_i))),
-               b.Assign(b.Deref("b"), b.mat2x2<f32>(b.vec2<f32>(1_f, 2_f), b.vec2<f32>(3_f, 4_f))),
-               b.Assign(b.IndexAccessor(b.Deref("b"), 1_i), b.vec2<f32>(5_f, 6_f)),
+               b.Assign(b.Deref("b"), b.Call<mat2x2<f32>>(b.Call<vec2<f32>>(1_f, 2_f),
+                                                          b.Call<vec2<f32>>(3_f, 4_f))),
+               b.Assign(b.IndexAccessor(b.Deref("b"), 1_i), b.Call<vec2<f32>>(5_f, 6_f)),
            },
            utils::Vector{
                b.Stage(PipelineStage::kCompute),
@@ -600,15 +604,16 @@
                                       }),
                          });
     b.GlobalVar("s", b.ty.Of(S), builtin::AddressSpace::kPrivate);
-    b.Func("f", utils::Empty, b.ty.void_(),
-           utils::Vector{
-               b.Assign(b.MemberAccessor("s", "m"),
-                        b.mat2x2<f32>(b.vec2<f32>(1_f, 2_f), b.vec2<f32>(3_f, 4_f))),
-           },
-           utils::Vector{
-               b.Stage(PipelineStage::kCompute),
-               b.WorkgroupSize(1_i),
-           });
+    b.Func(
+        "f", utils::Empty, b.ty.void_(),
+        utils::Vector{
+            b.Assign(b.MemberAccessor("s", "m"),
+                     b.Call<mat2x2<f32>>(b.Call<vec2<f32>>(1_f, 2_f), b.Call<vec2<f32>>(3_f, 4_f))),
+        },
+        utils::Vector{
+            b.Stage(PipelineStage::kCompute),
+            b.WorkgroupSize(1_i),
+        });
 
     auto* expect = R"(
 struct S {
diff --git a/src/tint/ast/transform/module_scope_var_to_entry_point_param.cc b/src/tint/ast/transform/module_scope_var_to_entry_point_param.cc
index db06414..14112f0 100644
--- a/src/tint/ast/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/ast/transform/module_scope_var_to_entry_point_param.cc
@@ -30,6 +30,8 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ModuleScopeVarToEntryPointParam);
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+
 namespace tint::ast::transform {
 namespace {
 
@@ -183,9 +185,7 @@
                     auto* member_ptr = ctx.dst->AddressOf(
                         ctx.dst->MemberAccessor(ctx.dst->Deref(workgroup_param()), member));
                     auto* local_var = ctx.dst->Let(
-                        new_var_symbol,
-                        ctx.dst->ty.ptr(builtin::AddressSpace::kWorkgroup, store_type()),
-                        member_ptr);
+                        new_var_symbol, ctx.dst->ty.ptr<workgroup>(store_type()), member_ptr);
                     ctx.InsertFront(func->body->statements, ctx.dst->Decl(local_var));
                     is_pointer = true;
                 } else {
@@ -427,8 +427,7 @@
                     }
                 } else {
                     // Create a parameter that is a pointer to the private variable struct.
-                    auto ptr = ctx.dst->ty.ptr(builtin::AddressSpace::kPrivate,
-                                               ctx.dst->ty(PrivateStructName()));
+                    auto ptr = ctx.dst->ty.ptr<private_>(ctx.dst->ty(PrivateStructName()));
                     auto* param = ctx.dst->Param(PrivateStructVariableName(), ptr);
                     ctx.InsertBack(func_ast->params, param);
                 }
@@ -489,8 +488,7 @@
                 // The parameter is a struct that contains members for each workgroup variable.
                 auto* str =
                     ctx.dst->Structure(ctx.dst->Sym(), std::move(workgroup_parameter_members));
-                auto param_type =
-                    ctx.dst->ty.ptr(builtin::AddressSpace::kWorkgroup, ctx.dst->ty.Of(str));
+                auto param_type = ctx.dst->ty.ptr(workgroup, ctx.dst->ty.Of(str));
                 auto* param =
                     ctx.dst->Param(workgroup_param(), param_type,
                                    utils::Vector{
diff --git a/src/tint/ast/transform/multiplanar_external_texture.cc b/src/tint/ast/transform/multiplanar_external_texture.cc
index 87844f8..db1ae5a 100644
--- a/src/tint/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/ast/transform/multiplanar_external_texture.cc
@@ -27,11 +27,12 @@
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::MultiplanarExternalTexture);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::MultiplanarExternalTexture::NewBindingPoints);
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 bool ShouldRun(const Program* program) {
     auto ext = program->Types().Find<type::ExternalTexture>();
     return ext != nullptr;
@@ -270,34 +271,35 @@
     void createGammaCorrectionFn() {
         gamma_correction_sym = b.Symbols().New("gammaCorrection");
 
-        b.Func(
-            gamma_correction_sym,
-            utils::Vector{
-                b.Param("v", b.ty.vec3<f32>()),
-                b.Param("params", b.ty(gamma_transfer_struct_sym)),
-            },
-            b.ty.vec3<f32>(),
-            utils::Vector{
-                // let cond = abs(v) < vec3(params.D);
-                b.Decl(b.Let("cond", b.LessThan(b.Call("abs", "v"),
-                                                b.vec3<f32>(b.MemberAccessor("params", "D"))))),
-                // let t = sign(v) * ((params.C * abs(v)) + params.F);
-                b.Decl(b.Let("t",
-                             b.Mul(b.Call("sign", "v"),
-                                   b.Add(b.Mul(b.MemberAccessor("params", "C"), b.Call("abs", "v")),
-                                         b.MemberAccessor("params", "F"))))),
-                // let f = (sign(v) * pow(((params.A * abs(v)) + params.B),
-                // vec3(params.G))) + params.E;
-                b.Decl(b.Let("f", b.Mul(b.Call("sign", "v"),
-                                        b.Add(b.Call("pow",
-                                                     b.Add(b.Mul(b.MemberAccessor("params", "A"),
-                                                                 b.Call("abs", "v")),
-                                                           b.MemberAccessor("params", "B")),
-                                                     b.vec3<f32>(b.MemberAccessor("params", "G"))),
-                                              b.MemberAccessor("params", "E"))))),
-                // return select(f, t, cond);
-                b.Return(b.Call("select", "f", "t", "cond")),
-            });
+        b.Func(gamma_correction_sym,
+               utils::Vector{
+                   b.Param("v", b.ty.vec3<f32>()),
+                   b.Param("params", b.ty(gamma_transfer_struct_sym)),
+               },
+               b.ty.vec3<f32>(),
+               utils::Vector{
+                   // let cond = abs(v) < vec3(params.D);
+                   b.Decl(b.Let("cond",
+                                b.LessThan(b.Call("abs", "v"),
+                                           b.Call<vec3<f32>>(b.MemberAccessor("params", "D"))))),
+                   // let t = sign(v) * ((params.C * abs(v)) + params.F);
+                   b.Decl(b.Let(
+                       "t", b.Mul(b.Call("sign", "v"),
+                                  b.Add(b.Mul(b.MemberAccessor("params", "C"), b.Call("abs", "v")),
+                                        b.MemberAccessor("params", "F"))))),
+                   // let f = (sign(v) * pow(((params.A * abs(v)) + params.B),
+                   // vec3(params.G))) + params.E;
+                   b.Decl(b.Let(
+                       "f", b.Mul(b.Call("sign", "v"),
+                                  b.Add(b.Call("pow",
+                                               b.Add(b.Mul(b.MemberAccessor("params", "A"),
+                                                           b.Call("abs", "v")),
+                                                     b.MemberAccessor("params", "B")),
+                                               b.Call<vec3<f32>>(b.MemberAccessor("params", "G"))),
+                                        b.MemberAccessor("params", "E"))))),
+                   // return select(f, t, cond);
+                   b.Return(b.Call("select", "f", "t", "cond")),
+               });
     }
 
     /// Constructs a StatementList containing all the statements making up the body of the texture
@@ -313,21 +315,21 @@
             case builtin::Function::kTextureSampleBaseClampToEdge:
                 stmts.Push(b.Decl(b.Let(
                     "modifiedCoords", b.Mul(b.MemberAccessor("params", "coordTransformationMatrix"),
-                                            b.vec3<f32>("coord", 1_a)))));
+                                            b.Call<vec3<f32>>("coord", 1_a)))));
 
                 stmts.Push(b.Decl(
                     b.Let("plane0_dims",
                           b.Call(b.ty.vec2<f32>(), b.Call("textureDimensions", "plane0", 0_a)))));
-                stmts.Push(
-                    b.Decl(b.Let("plane0_half_texel", b.Div(b.vec2<f32>(0.5_a), "plane0_dims"))));
+                stmts.Push(b.Decl(
+                    b.Let("plane0_half_texel", b.Div(b.Call<vec2<f32>>(0.5_a), "plane0_dims"))));
                 stmts.Push(b.Decl(
                     b.Let("plane0_clamped", b.Call("clamp", "modifiedCoords", "plane0_half_texel",
                                                    b.Sub(1_a, "plane0_half_texel")))));
                 stmts.Push(b.Decl(
                     b.Let("plane1_dims",
                           b.Call(b.ty.vec2<f32>(), b.Call("textureDimensions", "plane1", 0_a)))));
-                stmts.Push(
-                    b.Decl(b.Let("plane1_half_texel", b.Div(b.vec2<f32>(0.5_a), "plane1_dims"))));
+                stmts.Push(b.Decl(
+                    b.Let("plane1_half_texel", b.Div(b.Call<vec2<f32>>(0.5_a), "plane1_dims"))));
                 stmts.Push(b.Decl(
                     b.Let("plane1_clamped", b.Call("clamp", "modifiedCoords", "plane1_half_texel",
                                                    b.Sub(1_a, "plane1_half_texel")))));
@@ -346,7 +348,7 @@
                 // textureLoad(plane0, coord, 0);
                 plane_0_call = b.Call("textureLoad", "plane0", "coord", 0_a);
                 // let coord1 = coord >> 1;
-                stmts.Push(b.Decl(b.Let("coord1", b.Shr("coord", b.vec2<u32>(1_a)))));
+                stmts.Push(b.Decl(b.Let("coord1", b.Shr("coord", b.Call<vec2<u32>>(1_a)))));
                 // textureLoad(plane1, coord1, 0);
                 plane_1_call = b.Call("textureLoad", "plane1", "coord1", 0_a);
                 break;
@@ -367,8 +369,8 @@
                      // color = vec4<f32>(plane_0_call.r, plane_1_call.rg, 1.0) *
                      //         params.yuvToRgbConversionMatrix;
                      b.Assign("color",
-                              b.Mul(b.vec4<f32>(b.MemberAccessor(plane_0_call, "r"),
-                                                b.MemberAccessor(plane_1_call, "rg"), 1_a),
+                              b.Mul(b.Call<vec4<f32>>(b.MemberAccessor(plane_0_call, "r"),
+                                                      b.MemberAccessor(plane_1_call, "rg"), 1_a),
                                     b.MemberAccessor("params", "yuvToRgbConversionMatrix")))))));
 
         // if (params.doYuvToRgbConversionOnly == 0u)
@@ -386,7 +388,7 @@
                                               b.MemberAccessor("params", "gammaEncodeParams"))))));
 
         // return vec4<f32>(color, 1.f);
-        stmts.Push(b.Return(b.vec4<f32>("color", 1_a)));
+        stmts.Push(b.Return(b.Call<vec4<f32>>("color", 1_a)));
 
         return stmts;
     }
diff --git a/src/tint/ast/transform/preserve_padding.cc b/src/tint/ast/transform/preserve_padding.cc
index 7d9143c..3f51fa4 100644
--- a/src/tint/ast/transform/preserve_padding.cc
+++ b/src/tint/ast/transform/preserve_padding.cc
@@ -26,10 +26,11 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::PreservePadding);
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 PreservePadding::PreservePadding() = default;
 
 PreservePadding::~PreservePadding() = default;
@@ -122,8 +123,7 @@
                 auto helper_name = b.Symbols().New("assign_and_preserve_padding");
                 utils::Vector<const Parameter*, 2> params = {
                     b.Param(kDestParamName,
-                            b.ty.ptr(builtin::AddressSpace::kStorage, CreateASTTypeFor(ctx, ty),
-                                     builtin::Access::kReadWrite)),
+                            b.ty.ptr<storage, read_write>(CreateASTTypeFor(ctx, ty))),
                     b.Param(kValueParamName, CreateASTTypeFor(ctx, ty)),
                 };
                 b.Func(helper_name, params, b.ty.void_(), body());
diff --git a/src/tint/ast/transform/robustness.cc b/src/tint/ast/transform/robustness.cc
index aa94748..e0cb752 100644
--- a/src/tint/ast/transform/robustness.cc
+++ b/src/tint/ast/transform/robustness.cc
@@ -35,10 +35,11 @@
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Robustness);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Robustness::Config);
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 /// PIMPL state for the transform
 struct Robustness::State {
     /// Constructor
diff --git a/src/tint/ast/transform/vertex_pulling.cc b/src/tint/ast/transform/vertex_pulling.cc
index c2a543e..54cb8c0 100644
--- a/src/tint/ast/transform/vertex_pulling.cc
+++ b/src/tint/ast/transform/vertex_pulling.cc
@@ -32,10 +32,11 @@
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VertexPulling);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VertexPulling::Config);
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::ast::transform {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 namespace {
 
 /// The base WGSL type of a component.
@@ -576,71 +577,71 @@
 
             case VertexFormat::kUint8x2: {
                 // yyxx0000, yyxx0000
-                auto* u16s = b.vec2<u32>(load_u16_h());
+                auto* u16s = b.Call<vec2<u32>>(load_u16_h());
                 // xx000000, yyxx0000
-                auto* shl = b.Shl(u16s, b.vec2<u32>(8_u, 0_u));
+                auto* shl = b.Shl(u16s, b.Call<vec2<u32>>(8_u, 0_u));
                 // 000000xx, 000000yy
-                return b.Shr(shl, b.vec2<u32>(24_u));
+                return b.Shr(shl, b.Call<vec2<u32>>(24_u));
             }
             case VertexFormat::kUint8x4: {
                 // wwzzyyxx, wwzzyyxx, wwzzyyxx, wwzzyyxx
-                auto* u32s = b.vec4<u32>(load_u32());
+                auto* u32s = b.Call<vec4<u32>>(load_u32());
                 // xx000000, yyxx0000, zzyyxx00, wwzzyyxx
-                auto* shl = b.Shl(u32s, b.vec4<u32>(24_u, 16_u, 8_u, 0_u));
+                auto* shl = b.Shl(u32s, b.Call<vec4<u32>>(24_u, 16_u, 8_u, 0_u));
                 // 000000xx, 000000yy, 000000zz, 000000ww
-                return b.Shr(shl, b.vec4<u32>(24_u));
+                return b.Shr(shl, b.Call<vec4<u32>>(24_u));
             }
             case VertexFormat::kUint16x2: {
                 // yyyyxxxx, yyyyxxxx
-                auto* u32s = b.vec2<u32>(load_u32());
+                auto* u32s = b.Call<vec2<u32>>(load_u32());
                 // xxxx0000, yyyyxxxx
-                auto* shl = b.Shl(u32s, b.vec2<u32>(16_u, 0_u));
+                auto* shl = b.Shl(u32s, b.Call<vec2<u32>>(16_u, 0_u));
                 // 0000xxxx, 0000yyyy
-                return b.Shr(shl, b.vec2<u32>(16_u));
+                return b.Shr(shl, b.Call<vec2<u32>>(16_u));
             }
             case VertexFormat::kUint16x4: {
                 // yyyyxxxx, wwwwzzzz
-                auto* u32s = b.vec2<u32>(load_u32(), load_next_u32());
+                auto* u32s = b.Call<vec2<u32>>(load_u32(), load_next_u32());
                 // yyyyxxxx, yyyyxxxx, wwwwzzzz, wwwwzzzz
                 auto* xxyy = b.MemberAccessor(u32s, "xxyy");
                 // xxxx0000, yyyyxxxx, zzzz0000, wwwwzzzz
-                auto* shl = b.Shl(xxyy, b.vec4<u32>(16_u, 0_u, 16_u, 0_u));
+                auto* shl = b.Shl(xxyy, b.Call<vec4<u32>>(16_u, 0_u, 16_u, 0_u));
                 // 0000xxxx, 0000yyyy, 0000zzzz, 0000wwww
-                return b.Shr(shl, b.vec4<u32>(16_u));
+                return b.Shr(shl, b.Call<vec4<u32>>(16_u));
             }
             case VertexFormat::kSint8x2: {
                 // yyxx0000, yyxx0000
-                auto* i16s = b.vec2<i32>(load_i16_h());
+                auto* i16s = b.Call<vec2<i32>>(load_i16_h());
                 // xx000000, yyxx0000
-                auto* shl = b.Shl(i16s, b.vec2<u32>(8_u, 0_u));
+                auto* shl = b.Shl(i16s, b.Call<vec2<u32>>(8_u, 0_u));
                 // ssssssxx, ssssssyy
-                return b.Shr(shl, b.vec2<u32>(24_u));
+                return b.Shr(shl, b.Call<vec2<u32>>(24_u));
             }
             case VertexFormat::kSint8x4: {
                 // wwzzyyxx, wwzzyyxx, wwzzyyxx, wwzzyyxx
-                auto* i32s = b.vec4<i32>(load_i32());
+                auto* i32s = b.Call<vec4<i32>>(load_i32());
                 // xx000000, yyxx0000, zzyyxx00, wwzzyyxx
-                auto* shl = b.Shl(i32s, b.vec4<u32>(24_u, 16_u, 8_u, 0_u));
+                auto* shl = b.Shl(i32s, b.Call<vec4<u32>>(24_u, 16_u, 8_u, 0_u));
                 // ssssssxx, ssssssyy, sssssszz, ssssssww
-                return b.Shr(shl, b.vec4<u32>(24_u));
+                return b.Shr(shl, b.Call<vec4<u32>>(24_u));
             }
             case VertexFormat::kSint16x2: {
                 // yyyyxxxx, yyyyxxxx
-                auto* i32s = b.vec2<i32>(load_i32());
+                auto* i32s = b.Call<vec2<i32>>(load_i32());
                 // xxxx0000, yyyyxxxx
-                auto* shl = b.Shl(i32s, b.vec2<u32>(16_u, 0_u));
+                auto* shl = b.Shl(i32s, b.Call<vec2<u32>>(16_u, 0_u));
                 // ssssxxxx, ssssyyyy
-                return b.Shr(shl, b.vec2<u32>(16_u));
+                return b.Shr(shl, b.Call<vec2<u32>>(16_u));
             }
             case VertexFormat::kSint16x4: {
                 // yyyyxxxx, wwwwzzzz
-                auto* i32s = b.vec2<i32>(load_i32(), load_next_i32());
+                auto* i32s = b.Call<vec2<i32>>(load_i32(), load_next_i32());
                 // yyyyxxxx, yyyyxxxx, wwwwzzzz, wwwwzzzz
                 auto* xxyy = b.MemberAccessor(i32s, "xxyy");
                 // xxxx0000, yyyyxxxx, zzzz0000, wwwwzzzz
-                auto* shl = b.Shl(xxyy, b.vec4<u32>(16_u, 0_u, 16_u, 0_u));
+                auto* shl = b.Shl(xxyy, b.Call<vec4<u32>>(16_u, 0_u, 16_u, 0_u));
                 // ssssxxxx, ssssyyyy, sssszzzz, sssswwww
-                return b.Shr(shl, b.vec4<u32>(16_u));
+                return b.Shr(shl, b.Call<vec4<u32>>(16_u));
             }
             case VertexFormat::kUnorm8x2:
                 return b.MemberAccessor(b.Call("unpack4x8unorm", load_u16_l()), "xy");
@@ -657,14 +658,14 @@
             case VertexFormat::kFloat16x2:
                 return b.Call("unpack2x16float", load_u32());
             case VertexFormat::kUnorm16x4:
-                return b.vec4<f32>(b.Call("unpack2x16unorm", load_u32()),
-                                   b.Call("unpack2x16unorm", load_next_u32()));
+                return b.Call<vec4<f32>>(b.Call("unpack2x16unorm", load_u32()),
+                                         b.Call("unpack2x16unorm", load_next_u32()));
             case VertexFormat::kSnorm16x4:
-                return b.vec4<f32>(b.Call("unpack2x16snorm", load_u32()),
-                                   b.Call("unpack2x16snorm", load_next_u32()));
+                return b.Call<vec4<f32>>(b.Call("unpack2x16snorm", load_u32()),
+                                         b.Call("unpack2x16snorm", load_next_u32()));
             case VertexFormat::kFloat16x4:
-                return b.vec4<f32>(b.Call("unpack2x16float", load_u32()),
-                                   b.Call("unpack2x16float", load_next_u32()));
+                return b.Call<vec4<f32>>(b.Call("unpack2x16float", load_u32()),
+                                         b.Call("unpack2x16float", load_next_u32()));
         }
 
         TINT_UNREACHABLE(Transform, b.Diagnostics()) << "format " << static_cast<int>(format);
diff --git a/src/tint/builtin/attribute.cc b/src/tint/builtin/attribute.cc
index 33a5171..0647825 100644
--- a/src/tint/builtin/attribute.cc
+++ b/src/tint/builtin/attribute.cc
@@ -52,6 +52,9 @@
     if (str == "id") {
         return Attribute::kId;
     }
+    if (str == "index") {
+        return Attribute::kIndex;
+    }
     if (str == "interpolate") {
         return Attribute::kInterpolate;
     }
@@ -96,6 +99,8 @@
             return out << "group";
         case Attribute::kId:
             return out << "id";
+        case Attribute::kIndex:
+            return out << "index";
         case Attribute::kInterpolate:
             return out << "interpolate";
         case Attribute::kInvariant:
diff --git a/src/tint/builtin/attribute.h b/src/tint/builtin/attribute.h
index c975267..f9a1868 100644
--- a/src/tint/builtin/attribute.h
+++ b/src/tint/builtin/attribute.h
@@ -41,6 +41,7 @@
     kFragment,
     kGroup,
     kId,
+    kIndex,
     kInterpolate,
     kInvariant,
     kLocation,
@@ -61,9 +62,9 @@
 Attribute ParseAttribute(std::string_view str);
 
 constexpr const char* kAttributeStrings[] = {
-    "align",    "binding",  "builtin", "compute",     "diagnostic",
-    "fragment", "group",    "id",      "interpolate", "invariant",
-    "location", "must_use", "size",    "vertex",      "workgroup_size",
+    "align",    "binding", "builtin", "compute",        "diagnostic", "fragment",
+    "group",    "id",      "index",   "interpolate",    "invariant",  "location",
+    "must_use", "size",    "vertex",  "workgroup_size",
 };
 
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute_bench.cc b/src/tint/builtin/attribute_bench.cc
index a25fbf1..267a905 100644
--- a/src/tint/builtin/attribute_bench.cc
+++ b/src/tint/builtin/attribute_bench.cc
@@ -87,55 +87,62 @@
         "i",
         "rrd",
         "iG",
-        "FFnterpolate",
-        "iEtrplat",
-        "inerporrate",
+        "inFFex",
+        "iE",
+        "inrrx",
+        "index",
+        "inx",
+        "inJJD",
+        "ie",
+        "inerpklae",
+        "intrpolate",
+        "inJerpolae",
         "interpolate",
-        "inteplate",
-        "XterJJoDate",
-        "inepol8t",
-        "nvark1n",
-        "invriant",
-        "Jnvarant",
+        "interpocate",
+        "interpolaOe",
+        "__nttevvpoKKate",
+        "xnvari5n8",
+        "inFq__ant",
+        "iqqariant",
         "invariant",
-        "invaricnt",
-        "invariaOt",
-        "invttKK_ianvv",
-        "lxxcati8",
-        "Focqq__o",
-        "locaiqqn",
+        "invar6a33O",
+        "i96arQttanoo",
+        "inari66nt",
+        "lOxati6zz",
+        "locyytion",
+        "lHHtion",
         "location",
-        "loc33tio6",
-        "ltto6at9QQn",
-        "loc66tio",
-        "mOxt_u6zz",
-        "musyy_use",
-        "mHH_use",
+        "qWW4caton",
+        "locOOton",
+        "ocatiYn",
+        "m_use",
+        "mutFuse",
+        "wust_us",
         "must_use",
-        "qWW4st_se",
-        "musOO_se",
-        "ust_uYe",
-        "i",
-        "Fie",
-        "siw",
+        "Kst_sff",
+        "qusKK_use",
+        "mFsmm_3se",
+        "ize",
+        "sze",
+        "sbbb",
         "size",
-        "zff",
-        "sizqK",
-        "s3zmm",
-        "ertex",
-        "vereq",
-        "vbtbbx",
+        "iie",
+        "siqe",
+        "svvTTe",
+        "vertFFx",
+        "vrQ00P",
+        "vePtex",
         "vertex",
-        "irtex",
-        "vOOteq",
-        "vertTvvx",
-        "woFFkgroup_size",
-        "wfr00grPupsiQe",
-        "workgrouP_size",
+        "vsste77",
+        "veCtRRbb",
+        "verteXX",
+        "workgqou_siCCOOO",
+        "worsgroupsuzL",
+        "wXrkgroup_size",
         "workgroup_size",
-        "workgroup77sise",
-        "RRobbkgroupCsize",
-        "wXXrkgroup_size",
+        "workgroup_sze",
+        "wqqrOgoupize",
+        "workg22oup_size",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/attribute_test.cc b/src/tint/builtin/attribute_test.cc
index 9919467..33f557e 100644
--- a/src/tint/builtin/attribute_test.cc
+++ b/src/tint/builtin/attribute_test.cc
@@ -43,21 +43,14 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"align", Attribute::kAlign},
-    {"binding", Attribute::kBinding},
-    {"builtin", Attribute::kBuiltin},
-    {"compute", Attribute::kCompute},
-    {"diagnostic", Attribute::kDiagnostic},
-    {"fragment", Attribute::kFragment},
-    {"group", Attribute::kGroup},
-    {"id", Attribute::kId},
-    {"interpolate", Attribute::kInterpolate},
-    {"invariant", Attribute::kInvariant},
-    {"location", Attribute::kLocation},
-    {"must_use", Attribute::kMustUse},
-    {"size", Attribute::kSize},
-    {"vertex", Attribute::kVertex},
-    {"workgroup_size", Attribute::kWorkgroupSize},
+    {"align", Attribute::kAlign},           {"binding", Attribute::kBinding},
+    {"builtin", Attribute::kBuiltin},       {"compute", Attribute::kCompute},
+    {"diagnostic", Attribute::kDiagnostic}, {"fragment", Attribute::kFragment},
+    {"group", Attribute::kGroup},           {"id", Attribute::kId},
+    {"index", Attribute::kIndex},           {"interpolate", Attribute::kInterpolate},
+    {"invariant", Attribute::kInvariant},   {"location", Attribute::kLocation},
+    {"must_use", Attribute::kMustUse},      {"size", Attribute::kSize},
+    {"vertex", Attribute::kVertex},         {"workgroup_size", Attribute::kWorkgroupSize},
 };
 
 static constexpr Case kInvalidCases[] = {
@@ -85,27 +78,30 @@
     {"d", Attribute::kUndefined},
     {"i", Attribute::kUndefined},
     {"OVd", Attribute::kUndefined},
-    {"inyerpolae", Attribute::kUndefined},
-    {"rrnterpolll77Ge", Attribute::kUndefined},
-    {"inte4pol00te", Attribute::kUndefined},
-    {"inoornt", Attribute::kUndefined},
-    {"inzzriat", Attribute::kUndefined},
-    {"n11pariiin", Attribute::kUndefined},
-    {"XXocation", Attribute::kUndefined},
-    {"lIIc9955nnon", Attribute::kUndefined},
-    {"aaoHHatioYSS", Attribute::kUndefined},
-    {"mkksue", Attribute::kUndefined},
-    {"gjs_RRs", Attribute::kUndefined},
-    {"msb_se", Attribute::kUndefined},
-    {"jize", Attribute::kUndefined},
-    {"sze", Attribute::kUndefined},
-    {"qz", Attribute::kUndefined},
-    {"vNNtex", Attribute::kUndefined},
-    {"vevvx", Attribute::kUndefined},
-    {"veQQex", Attribute::kUndefined},
-    {"workgrrupffie", Attribute::kUndefined},
-    {"workgroup_sije", Attribute::kUndefined},
-    {"workgoupNNwsiz8", Attribute::kUndefined},
+    {"ndyx", Attribute::kUndefined},
+    {"n77rrldGx", Attribute::kUndefined},
+    {"inde40", Attribute::kUndefined},
+    {"itooolate", Attribute::kUndefined},
+    {"intezplate", Attribute::kUndefined},
+    {"ppnerii1olat", Attribute::kUndefined},
+    {"invarianXX", Attribute::kUndefined},
+    {"inv55ria99nII", Attribute::kUndefined},
+    {"irrvariaSSaHH", Attribute::kUndefined},
+    {"lkkcin", Attribute::kUndefined},
+    {"gjctRRo", Attribute::kUndefined},
+    {"lcbton", Attribute::kUndefined},
+    {"mustjuse", Attribute::kUndefined},
+    {"must_se", Attribute::kUndefined},
+    {"muquse", Attribute::kUndefined},
+    {"szNN", Attribute::kUndefined},
+    {"zvv", Attribute::kUndefined},
+    {"QQze", Attribute::kUndefined},
+    {"eterf", Attribute::kUndefined},
+    {"vertjx", Attribute::kUndefined},
+    {"v82wNNx", Attribute::kUndefined},
+    {"worgroup_size", Attribute::kUndefined},
+    {"workgrourr_size", Attribute::kUndefined},
+    {"workgroGp_size", Attribute::kUndefined},
 };
 
 using AttributeParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/builtin/extension.cc b/src/tint/builtin/extension.cc
index aca6220..a8d4567 100644
--- a/src/tint/builtin/extension.cc
+++ b/src/tint/builtin/extension.cc
@@ -40,6 +40,9 @@
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
+    if (str == "chromium_internal_dual_source_blending") {
+        return Extension::kChromiumInternalDualSourceBlending;
+    }
     if (str == "chromium_internal_relaxed_uniform_layout") {
         return Extension::kChromiumInternalRelaxedUniformLayout;
     }
@@ -61,6 +64,8 @@
             return out << "chromium_experimental_full_ptr_parameters";
         case Extension::kChromiumExperimentalPushConstant:
             return out << "chromium_experimental_push_constant";
+        case Extension::kChromiumInternalDualSourceBlending:
+            return out << "chromium_internal_dual_source_blending";
         case Extension::kChromiumInternalRelaxedUniformLayout:
             return out << "chromium_internal_relaxed_uniform_layout";
         case Extension::kF16:
diff --git a/src/tint/builtin/extension.h b/src/tint/builtin/extension.h
index 6beeda3..6883f4b 100644
--- a/src/tint/builtin/extension.h
+++ b/src/tint/builtin/extension.h
@@ -36,6 +36,7 @@
     kChromiumExperimentalDp4A,
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPushConstant,
+    kChromiumInternalDualSourceBlending,
     kChromiumInternalRelaxedUniformLayout,
     kF16,
 };
@@ -51,9 +52,13 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr const char* kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",      "chromium_experimental_dp4a",
-    "chromium_experimental_full_ptr_parameters", "chromium_experimental_push_constant",
-    "chromium_internal_relaxed_uniform_layout",  "f16",
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_dp4a",
+    "chromium_experimental_full_ptr_parameters",
+    "chromium_experimental_push_constant",
+    "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout",
+    "f16",
 };
 
 // A unique vector of extensions
diff --git a/src/tint/builtin/extension_bench.cc b/src/tint/builtin/extension_bench.cc
index 7a50281..447bf55 100644
--- a/src/tint/builtin/extension_bench.cc
+++ b/src/tint/builtin/extension_bench.cc
@@ -59,20 +59,27 @@
         "chromium_exp9rimFntal_ush_constant",
         "chrmium_experimental_push_constant",
         "cOOromium_experiVeHtal_puh_conRRtant",
-        "chromium_internl_relaxyd_uniform_layout",
-        "chromnnum_internrr77_Gelaxell_uniform_layout",
-        "chromium_intern4l_relaxe00_uniform_layout",
+        "chromium_internay_dual_sorce_blending",
+        "chrnnmium_internal_duGrr_source_bllend77ng",
+        "chromiu4_inter00al_dual_source_blending",
+        "chromium_internal_dual_source_blending",
+        "chromium_intoornal_dua_sorce_bleding",
+        "cromium_nternal_dual_sourcezzblending",
+        "chiiomiu_internal_dppal_source11blendig",
+        "XXhromium_internal_relaxed_uniform_layout",
+        "chromium_99nternal_reIInaxed_un55form_layout",
+        "chromYuaaSSinternrrl_rHHlaxed_uniform_layout",
         "chromium_internal_relaxed_uniform_layout",
-        "chrmoom_internal_relaxed_uniform_lyout",
-        "chroium_internal_rlaxed_uniform_layzzut",
-        "chromium_internaii_r11axed_uppifor_layout",
-        "f1XX",
-        "55199II",
-        "frSSHHa",
+        "chroiHm_internal_rkklaxd_uniform_layou",
+        "chromium_internl_relaxedguniRRorm_lajou",
+        "chromium_inernal_relaxedbuniorm_layout",
+        "fj6",
+        "f6",
+        "q",
         "f16",
-        "U",
-        "jV3",
-        "",
+        "fNN",
+        "fv",
+        "Q16",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/extension_test.cc b/src/tint/builtin/extension_test.cc
index 9ef8683..cc13b13 100644
--- a/src/tint/builtin/extension_test.cc
+++ b/src/tint/builtin/extension_test.cc
@@ -48,6 +48,7 @@
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
+    {"chromium_internal_dual_source_blending", Extension::kChromiumInternalDualSourceBlending},
     {"chromium_internal_relaxed_uniform_layout", Extension::kChromiumInternalRelaxedUniformLayout},
     {"f16", Extension::kF16},
 };
@@ -65,12 +66,15 @@
     {"chvomium_experimental_push_constiint", Extension::kUndefined},
     {"chromiu8WWexperimental_push_constant", Extension::kUndefined},
     {"chromium_experiMental_push_costanxx", Extension::kUndefined},
-    {"chromium_internal_relaxed_unXform_layugg", Extension::kUndefined},
-    {"chromiuu_iVterna_relxed_unifXrm_layout", Extension::kUndefined},
-    {"chromium_internal_relaxed_uni3orm_layout", Extension::kUndefined},
-    {"fE6", Extension::kUndefined},
-    {"fPTT", Extension::kUndefined},
-    {"dxx6", Extension::kUndefined},
+    {"Xhromium_ggnternal_dual_sourceblending", Extension::kUndefined},
+    {"chromium_internludual_sorce_bVenXing", Extension::kUndefined},
+    {"chromium_internal_dual_source_b3ending", Extension::kUndefined},
+    {"chromium_internal_rElaxed_uniform_layout", Extension::kUndefined},
+    {"chromium_internalPPrTTlaed_uniform_layout", Extension::kUndefined},
+    {"chroddium_internxxl_relaxed_unform_layout", Extension::kUndefined},
+    {"4416", Extension::kUndefined},
+    {"fSVV6", Extension::kUndefined},
+    {"RR2", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/builtin/fluent_types.h b/src/tint/builtin/fluent_types.h
new file mode 100644
index 0000000..925b54e
--- /dev/null
+++ b/src/tint/builtin/fluent_types.h
@@ -0,0 +1,237 @@
+// 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_FLUENT_TYPES_H_
+#define SRC_TINT_BUILTIN_FLUENT_TYPES_H_
+
+#include <stdint.h>
+
+#include "src/tint/builtin/access.h"
+#include "src/tint/builtin/address_space.h"
+#include "src/tint/builtin/number.h"
+
+namespace tint::builtin::fluent_types {
+
+// A sentinel type used by some template arguments to signal that the a type should be inferred.
+struct Infer {};
+
+/// A 'fluent' type helper used to construct an ast::Array or type::Array.
+/// @tparam T the array element type
+/// @tparam N the array length. 0 represents a runtime-sized array.
+/// @see https://www.w3.org/TR/WGSL/#array-types
+template <typename T = Infer, uint32_t N = 0>
+struct array {
+    /// the array element type
+    using type = T;
+    /// the array length. 0 represents a runtime-sized array.
+    static constexpr uint32_t length = N;
+};
+
+/// A 'fluent' type helper used to construct an ast::Atomic or type::Atomic.
+/// @tparam T the atomic element type
+/// @see https://www.w3.org/TR/WGSL/#atomic-types
+template <typename T>
+struct atomic {
+    /// the atomic element type
+    using type = T;
+};
+
+/// A 'fluent' type helper used to construct an ast::Vector or type::Vector.
+/// @tparam N the vector width
+/// @tparam T the vector element type
+template <uint32_t N, typename T = Infer>
+struct vec {
+    /// the vector width
+    static constexpr uint32_t width = N;
+    /// the vector element type
+    using type = T;
+};
+
+/// A 'fluent' type helper used to construct an ast::Matrix or type::Matrix.
+/// @tparam C the number of columns of the matrix
+/// @tparam R the number of rows of the matrix
+/// @tparam T the matrix element type
+/// @see https://www.w3.org/TR/WGSL/#matrix-types
+template <uint32_t C, uint32_t R, typename T = Infer>
+struct mat {
+    /// the number of columns of the matrix
+    static constexpr uint32_t columns = C;
+    /// the number of rows of the matrix
+    static constexpr uint32_t rows = R;
+    /// the matrix element type
+    using type = T;
+    /// the column vector type
+    using column = vec<R, T>;
+};
+
+/// A 'fluent' type helper used to construct an ast::Pointer or type::Pointer.
+/// @tparam ADDRESS the pointer address space
+/// @tparam T the pointer storage type
+/// @tparam ACCESS the pointer access control
+template <builtin::AddressSpace ADDRESS,
+          typename T,
+          builtin::Access ACCESS = builtin::Access::kUndefined>
+struct ptr {
+    /// the pointer address space
+    static constexpr builtin::AddressSpace address = ADDRESS;
+    /// the pointer storage type
+    using type = T;
+    /// the pointer access control
+    static constexpr builtin::Access access = ACCESS;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Aliases
+//
+// Shorthand aliases for the types declared above
+////////////////////////////////////////////////////////////////////////////////
+
+//! @cond Doxygen_Suppress
+template <typename T>
+using mat2x2 = mat<2, 2, T>;
+
+template <typename T>
+using mat2x3 = mat<2, 3, T>;
+
+template <typename T>
+using mat2x4 = mat<2, 4, T>;
+
+template <typename T>
+using mat3x2 = mat<3, 2, T>;
+
+template <typename T>
+using mat3x3 = mat<3, 3, T>;
+
+template <typename T>
+using mat3x4 = mat<3, 4, T>;
+
+template <typename T>
+using mat4x2 = mat<4, 2, T>;
+
+template <typename T>
+using mat4x3 = mat<4, 3, T>;
+
+template <typename T>
+using mat4x4 = mat<4, 4, T>;
+
+template <typename T>
+using vec2 = vec<2, T>;
+
+template <typename T>
+using vec3 = vec<3, T>;
+
+template <typename T>
+using vec4 = vec<4, T>;
+
+//! @endcond
+
+////////////////////////////////////////////////////////////////////////////////
+// Address space aliases
+////////////////////////////////////////////////////////////////////////////////
+static constexpr builtin::AddressSpace function = builtin::AddressSpace::kFunction;
+static constexpr builtin::AddressSpace private_ = builtin::AddressSpace::kPrivate;
+static constexpr builtin::AddressSpace push_constant = builtin::AddressSpace::kPushConstant;
+static constexpr builtin::AddressSpace storage = builtin::AddressSpace::kStorage;
+static constexpr builtin::AddressSpace uniform = builtin::AddressSpace::kUniform;
+static constexpr builtin::AddressSpace workgroup = builtin::AddressSpace::kWorkgroup;
+
+////////////////////////////////////////////////////////////////////////////////
+// Access control aliases
+////////////////////////////////////////////////////////////////////////////////
+static constexpr builtin::Access read = builtin::Access::kRead;
+static constexpr builtin::Access read_write = builtin::Access::kReadWrite;
+static constexpr builtin::Access write = builtin::Access::kWrite;
+
+////////////////////////////////////////////////////////////////////////////////
+// Traits
+////////////////////////////////////////////////////////////////////////////////
+namespace detail {
+
+//! @cond Doxygen_Suppress
+template <typename T>
+struct IsArray {
+    static constexpr bool value = false;
+};
+
+template <typename T, uint32_t N>
+struct IsArray<array<T, N>> {
+    static constexpr bool value = true;
+};
+
+template <typename T>
+struct IsAtomic {
+    static constexpr bool value = false;
+};
+
+template <typename T>
+struct IsAtomic<atomic<T>> {
+    static constexpr bool value = true;
+};
+
+template <typename T>
+struct IsMatrix {
+    static constexpr bool value = false;
+};
+
+template <uint32_t C, uint32_t R, typename T>
+struct IsMatrix<mat<C, R, T>> {
+    static constexpr bool value = true;
+};
+
+template <typename T>
+struct IsVector {
+    static constexpr bool value = false;
+};
+
+template <uint32_t N, typename T>
+struct IsVector<vec<N, T>> {
+    static constexpr bool value = true;
+};
+
+template <typename T>
+struct IsPointer {
+    static constexpr bool value = false;
+};
+
+template <builtin::AddressSpace ADDRESS, typename T, builtin::Access ACCESS>
+struct IsPointer<ptr<ADDRESS, T, ACCESS>> {
+    static constexpr bool value = true;
+};
+//! @endcond
+
+}  // namespace detail
+
+/// Evaluates to true if `T` is a array
+template <typename T>
+static constexpr bool IsArray = fluent_types::detail::IsArray<T>::value;
+
+/// Evaluates to true if `T` is a atomic
+template <typename T>
+static constexpr bool IsAtomic = fluent_types::detail::IsAtomic<T>::value;
+
+/// Evaluates to true if `T` is a mat
+template <typename T>
+static constexpr bool IsMatrix = fluent_types::detail::IsMatrix<T>::value;
+
+/// Evaluates to true if `T` is a vec
+template <typename T>
+static constexpr bool IsVector = fluent_types::detail::IsVector<T>::value;
+
+/// Evaluates to true if `T` is a ptr
+template <typename T>
+static constexpr bool IsPointer = fluent_types::detail::IsPointer<T>::value;
+
+}  // namespace tint::builtin::fluent_types
+
+#endif  // SRC_TINT_BUILTIN_FLUENT_TYPES_H_
diff --git a/src/tint/number.cc b/src/tint/builtin/number.cc
similarity index 99%
rename from src/tint/number.cc
rename to src/tint/builtin/number.cc
index 8b85670..bf15fef 100644
--- a/src/tint/number.cc
+++ b/src/tint/builtin/number.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/number.h"
+#include "src/tint/builtin/number.h"
 
 #include <algorithm>
 #include <cmath>
@@ -345,7 +345,7 @@
 }
 
 // static
-Number<detail::NumberKindF16> f16::FromBits(uint16_t bits) {
+Number<tint::detail::NumberKindF16> f16::FromBits(uint16_t bits) {
     // Assert we use binary32 (i.e. float) as underlying type, which has 4 bytes.
     static_assert(std::is_same<f16::type, float>());
 
diff --git a/src/tint/number.h b/src/tint/builtin/number.h
similarity index 95%
rename from src/tint/number.h
rename to src/tint/builtin/number.h
index 06ab7fd..23b9d0e 100644
--- a/src/tint/number.h
+++ b/src/tint/builtin/number.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_NUMBER_H_
-#define SRC_TINT_NUMBER_H_
+#ifndef SRC_TINT_BUILTIN_NUMBER_H_
+#define SRC_TINT_BUILTIN_NUMBER_H_
 
 #include <stdint.h>
 #include <cmath>
@@ -66,16 +66,16 @@
 
 /// Evaluates to true iff T is a Number
 template <typename T>
-constexpr bool IsNumber = detail::IsNumber<T>::value;
+constexpr bool IsNumber = tint::detail::IsNumber<T>::value;
 
 /// Resolves to the underlying type for a Number.
 template <typename T>
-using UnwrapNumber = typename detail::NumberUnwrapper<T>::type;
+using UnwrapNumber = typename tint::detail::NumberUnwrapper<T>::type;
 
 /// Evaluates to true iff T or Number<T> is a floating-point type or is NumberKindF16.
 template <typename T>
 constexpr bool IsFloatingPoint = std::is_floating_point_v<UnwrapNumber<T>> ||
-                                 std::is_same_v<UnwrapNumber<T>, detail::NumberKindF16>;
+                                 std::is_same_v<UnwrapNumber<T>, tint::detail::NumberKindF16>;
 
 /// Evaluates to true iff T or Number<T> is an integral type.
 template <typename T>
@@ -186,7 +186,7 @@
 /// The partial specification of Number for f16 type, storing the f16 value as float,
 /// and enforcing proper explicit casting.
 template <>
-struct Number<detail::NumberKindF16> : NumberBase<Number<detail::NumberKindF16>> {
+struct Number<tint::detail::NumberKindF16> : NumberBase<Number<tint::detail::NumberKindF16>> {
     /// C++ does not have a native float16 type, so we use a 32-bit float instead.
     using type = float;
 
@@ -226,7 +226,7 @@
 
     /// Negation operator
     /// @returns the negative value of the number
-    Number operator-() const { return Number<detail::NumberKindF16>(-value); }
+    Number operator-() const { return Number<tint::detail::NumberKindF16>(-value); }
 
     /// Assignment operator with parameter as native floating point type
     /// @param v the new value
@@ -246,7 +246,7 @@
     /// Creates an f16 value from the uint16_t bit representation.
     /// @param bits the bits to convert from
     /// @returns the binary16 value based off the provided bit pattern.
-    static Number<detail::NumberKindF16> FromBits(uint16_t bits);
+    static Number<tint::detail::NumberKindF16> FromBits(uint16_t bits);
 
     /// @param value the input float32 value
     /// @returns the float32 value quantized to the smaller float16 value, through truncation of the
@@ -272,7 +272,14 @@
 using f32 = Number<float>;
 /// `f16` is a type alias to `Number<detail::NumberKindF16>`, which should be IEEE 754 binary16.
 /// However since C++ don't have native binary16 type, the value is stored as float.
-using f16 = Number<detail::NumberKindF16>;
+using f16 = Number<tint::detail::NumberKindF16>;
+
+/// The algorithms in this module require support for infinity and quiet NaNs on
+/// floating point types.
+static_assert(std::numeric_limits<float>::has_infinity);
+static_assert(std::numeric_limits<float>::has_quiet_NaN);
+static_assert(std::numeric_limits<double>::has_infinity);
+static_assert(std::numeric_limits<double>::has_quiet_NaN);
 
 template <typename T, utils::traits::EnableIf<IsFloatingPoint<T>>* = nullptr>
 inline const auto kPi = T(UnwrapNumber<T>(3.14159265358979323846));
@@ -576,7 +583,7 @@
         return {};
     }
 
-    return AInt{detail::Mod(a.value, b.value)};
+    return AInt{tint::detail::Mod(a.value, b.value)};
 }
 
 /// @returns the remainder of a / b, or an empty optional if the resulting value overflowed the
@@ -587,7 +594,7 @@
     if (b == FloatingPointT{0.0} || b == FloatingPointT{-0.0}) {
         return {};
     }
-    auto result = FloatingPointT{detail::Mod(a.value, b.value)};
+    auto result = FloatingPointT{tint::detail::Mod(a.value, b.value)};
     if (!std::isfinite(result.value)) {
         return {};
     }
@@ -681,4 +688,4 @@
 
 }  // namespace std
 
-#endif  // SRC_TINT_NUMBER_H_
+#endif  // SRC_TINT_BUILTIN_NUMBER_H_
diff --git a/src/tint/number_test.cc b/src/tint/builtin/number_test.cc
similarity index 100%
rename from src/tint/number_test.cc
rename to src/tint/builtin/number_test.cc
diff --git a/src/tint/constant/composite.h b/src/tint/constant/composite.h
index 23367a5..c2f264c 100644
--- a/src/tint/constant/composite.h
+++ b/src/tint/constant/composite.h
@@ -15,8 +15,8 @@
 #ifndef SRC_TINT_CONSTANT_COMPOSITE_H_
 #define SRC_TINT_CONSTANT_COMPOSITE_H_
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/value.h"
-#include "src/tint/number.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/hash.h"
diff --git a/src/tint/constant/manager.h b/src/tint/constant/manager.h
index d356988..43d5049 100644
--- a/src/tint/constant/manager.h
+++ b/src/tint/constant/manager.h
@@ -17,8 +17,8 @@
 
 #include <utility>
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/value.h"
-#include "src/tint/number.h"
 #include "src/tint/type/manager.h"
 #include "src/tint/utils/hash.h"
 #include "src/tint/utils/unique_allocator.h"
diff --git a/src/tint/constant/scalar.h b/src/tint/constant/scalar.h
index 23684ec..442bc59 100644
--- a/src/tint/constant/scalar.h
+++ b/src/tint/constant/scalar.h
@@ -15,9 +15,9 @@
 #ifndef SRC_TINT_CONSTANT_SCALAR_H_
 #define SRC_TINT_CONSTANT_SCALAR_H_
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/manager.h"
 #include "src/tint/constant/value.h"
-#include "src/tint/number.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/hash.h"
diff --git a/src/tint/constant/value.h b/src/tint/constant/value.h
index baf1171..34ca335 100644
--- a/src/tint/constant/value.h
+++ b/src/tint/constant/value.h
@@ -17,9 +17,9 @@
 
 #include <variant>
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/clone_context.h"
 #include "src/tint/constant/node.h"
-#include "src/tint/number.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 0d667ab..4943c1c 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -76,6 +76,8 @@
   chromium_experimental_full_ptr_parameters
   // A Chromium-specific extension that relaxes memory layout requirements for uniform storage.
   chromium_internal_relaxed_uniform_layout
+  // A Chromium-specific extension that enables dual source blending.
+  chromium_internal_dual_source_blending
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#storage-class
@@ -260,6 +262,7 @@
   fragment
   group
   id
+  index
   interpolate
   invariant
   location
diff --git a/src/tint/ir/access.cc b/src/tint/ir/access.cc
index 8f2a793..a040991 100644
--- a/src/tint/ir/access.cc
+++ b/src/tint/ir/access.cc
@@ -23,14 +23,10 @@
 namespace tint::ir {
 
 //! @cond Doxygen_Suppress
-Access::Access(const type::Type* ty, Value* object, utils::VectorRef<Value*> indices)
-    : result_type_(ty) {
-    TINT_ASSERT(IR, object);
-    TINT_ASSERT(IR, result_type_);
-    TINT_ASSERT(IR, !indices.IsEmpty());
-
-    AddOperand(object);
-    AddOperands(std::move(indices));
+Access::Access(InstructionResult* result, Value* object, utils::VectorRef<Value*> indices) {
+    AddOperand(Access::kObjectOperandOffset, object);
+    AddOperands(Access::kIndicesOperandOffset, std::move(indices));
+    AddResult(result);
 }
 
 Access::~Access() = default;
diff --git a/src/tint/ir/access.h b/src/tint/ir/access.h
index 04467e5..7c0c917 100644
--- a/src/tint/ir/access.h
+++ b/src/tint/ir/access.h
@@ -21,32 +21,26 @@
 namespace tint::ir {
 
 /// An access instruction in the IR.
-class Access : public utils::Castable<Access, OperandInstruction<3>> {
+class Access : public utils::Castable<Access, OperandInstruction<3, 1>> {
   public:
-    /// The base offset in Operands() for the object being accessed
+    /// The offset in Operands() for the object being accessed
     static constexpr size_t kObjectOperandOffset = 0;
 
     /// The base offset in Operands() for the access indices
     static constexpr size_t kIndicesOperandOffset = 1;
 
     /// Constructor
-    /// @param result_type the result type
+    /// @param result the result value
     /// @param object the accessor object
     /// @param indices the indices to access
-    Access(const type::Type* result_type, Value* object, utils::VectorRef<Value*> indices);
+    Access(InstructionResult* result, Value* object, utils::VectorRef<Value*> indices);
     ~Access() override;
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the object used for the access
     Value* Object() { return operands_[kObjectOperandOffset]; }
 
     /// @returns the accessor indices
     utils::Slice<Value*> Indices() { return operands_.Slice().Offset(kIndicesOperandOffset); }
-
-  private:
-    const type::Type* result_type_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/access_test.cc b/src/tint/ir/access_test.cc
index 1f0e947..7840cc3 100644
--- a/src/tint/ir/access_test.cc
+++ b/src/tint/ir/access_test.cc
@@ -18,6 +18,8 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/ir/ir_test_helper.h"
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+
 namespace tint::ir {
 namespace {
 
@@ -29,10 +31,23 @@
     auto* idx = b.Constant(u32(1));
     auto* a = b.Access(ty.i32(), var, idx);
 
-    EXPECT_THAT(var->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
+    EXPECT_THAT(var->Result()->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
     EXPECT_THAT(idx->Usages(), testing::UnorderedElementsAre(Usage{a, 1u}));
 }
 
+TEST_F(IR_AccessTest, Result) {
+    auto* type = ty.ptr<function, i32>();
+    auto* var = b.Var(type);
+    auto* idx = b.Constant(u32(1));
+    auto* a = b.Access(ty.i32(), var, idx);
+
+    EXPECT_TRUE(a->HasResults());
+    EXPECT_FALSE(a->HasMultiResults());
+
+    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result()->Source());
+}
+
 TEST_F(IR_AccessTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
@@ -45,39 +60,5 @@
         "");
 }
 
-TEST_F(IR_AccessTest, Fail_NullObject) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Access(mod.Types().i32(), nullptr, u32(1));
-        },
-        "");
-}
-
-TEST_F(IR_AccessTest, Fail_EmptyIndices) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            auto* ty = (mod.Types().ptr<function, i32>());
-            auto* var = b.Var(ty);
-            b.Access(mod.Types().i32(), var, utils::Empty);
-        },
-        "");
-}
-
-TEST_F(IR_AccessTest, Fail_NullIndex) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            auto* ty = (mod.Types().ptr<function, i32>());
-            auto* var = b.Var(ty);
-            b.Access(mod.Types().i32(), var, nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/binary.cc b/src/tint/ir/binary.cc
index 6287715..f4de0e6 100644
--- a/src/tint/ir/binary.cc
+++ b/src/tint/ir/binary.cc
@@ -19,14 +19,10 @@
 
 namespace tint::ir {
 
-Binary::Binary(enum Kind kind, const type::Type* res_ty, Value* lhs, Value* rhs)
-    : kind_(kind), result_type_(res_ty) {
-    TINT_ASSERT(IR, result_type_);
-    TINT_ASSERT(IR, lhs);
-    TINT_ASSERT(IR, rhs);
-
-    AddOperand(lhs);
-    AddOperand(rhs);
+Binary::Binary(InstructionResult* result, enum Kind kind, Value* lhs, Value* rhs) : kind_(kind) {
+    AddOperand(Binary::kLhsOperandOffset, lhs);
+    AddOperand(Binary::kRhsOperandOffset, rhs);
+    AddResult(result);
 }
 
 Binary::~Binary() = default;
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index 6c8e6d4..e70ab0f 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -21,8 +21,14 @@
 namespace tint::ir {
 
 /// A binary instruction in the IR.
-class Binary : public utils::Castable<Binary, OperandInstruction<2>> {
+class Binary : public utils::Castable<Binary, OperandInstruction<2, 1>> {
   public:
+    /// The offset in Operands() for the LHS
+    static constexpr size_t kLhsOperandOffset = 0;
+
+    /// The offset in Operands() for the RHS
+    static constexpr size_t kRhsOperandOffset = 1;
+
     /// The kind of instruction.
     enum class Kind {
         kAdd,
@@ -47,28 +53,24 @@
     };
 
     /// Constructor
+    /// @param result the result value
     /// @param kind the kind of binary instruction
-    /// @param type the result type
     /// @param lhs the lhs of the instruction
     /// @param rhs the rhs of the instruction
-    Binary(enum Kind kind, const type::Type* type, Value* lhs, Value* rhs);
+    Binary(InstructionResult* result, enum Kind kind, Value* lhs, Value* rhs);
     ~Binary() override;
 
     /// @returns the kind of the binary instruction
     enum Kind Kind() { return kind_; }
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the left-hand-side value for the instruction
-    Value* LHS() { return operands_[0]; }
+    Value* LHS() { return operands_[kLhsOperandOffset]; }
 
     /// @returns the right-hand-side value for the instruction
-    Value* RHS() { return operands_[1]; }
+    Value* RHS() { return operands_[kRhsOperandOffset]; }
 
   private:
     enum Kind kind_;
-    const type::Type* result_type_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 8b3a90d..0d3c151 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -35,24 +35,13 @@
         "");
 }
 
-TEST_F(IR_BinaryTest, Fail_NullLHS) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Add(mod.Types().u32(), nullptr, u32(2));
-        },
-        "");
-}
+TEST_F(IR_BinaryTest, Result) {
+    auto* a = b.Add(mod.Types().i32(), 4_i, 2_i);
 
-TEST_F(IR_BinaryTest, Fail_NullRHS) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Add(mod.Types().u32(), u32(1), nullptr);
-        },
-        "");
+    EXPECT_TRUE(a->HasResults());
+    EXPECT_FALSE(a->HasMultiResults());
+    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result()->Source());
 }
 
 TEST_F(IR_BinaryTest, CreateAnd) {
@@ -60,7 +49,7 @@
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->Kind(), Binary::Kind::kAnd);
-    ASSERT_NE(inst->Type(), nullptr);
+    ASSERT_NE(inst->Results()[0]->Type(), nullptr);
 
     ASSERT_TRUE(inst->LHS()->Is<Constant>());
     auto lhs = inst->LHS()->As<Constant>()->Value();
diff --git a/src/tint/ir/bitcast.cc b/src/tint/ir/bitcast.cc
index a219749..c65e8bc 100644
--- a/src/tint/ir/bitcast.cc
+++ b/src/tint/ir/bitcast.cc
@@ -19,10 +19,9 @@
 
 namespace tint::ir {
 
-Bitcast::Bitcast(const type::Type* ty, Value* val) : Base(ty) {
-    TINT_ASSERT(IR, val);
-
-    AddOperand(val);
+Bitcast::Bitcast(InstructionResult* result, Value* val) {
+    AddOperand(Bitcast::kValueOperandOffset, val);
+    AddResult(result);
 }
 
 Bitcast::~Bitcast() = default;
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index ddf74b9..4e437ae 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -23,10 +23,13 @@
 /// A bitcast instruction in the IR.
 class Bitcast : public utils::Castable<Bitcast, Call> {
   public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
     /// Constructor
-    /// @param type the result type
+    /// @param result the result value
     /// @param val the value being bitcast
-    Bitcast(const type::Type* type, Value* val);
+    Bitcast(InstructionResult* result, Value* val);
     ~Bitcast() override;
 };
 
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index e925349..d9a975e 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -30,7 +30,7 @@
     auto* inst = b.Bitcast(mod.Types().i32(), 4_i);
 
     ASSERT_TRUE(inst->Is<ir::Bitcast>());
-    ASSERT_NE(inst->Type(), nullptr);
+    ASSERT_NE(inst->Result()->Type(), nullptr);
 
     auto args = inst->Args();
     ASSERT_EQ(args.Length(), 1u);
@@ -40,6 +40,15 @@
     EXPECT_EQ(4_i, val->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
+TEST_F(IR_BitcastTest, Result) {
+    auto* a = b.Bitcast(mod.Types().i32(), 4_i);
+
+    EXPECT_TRUE(a->HasResults());
+    EXPECT_FALSE(a->HasMultiResults());
+    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result()->Source());
+}
+
 TEST_F(IR_BitcastTest, Bitcast_Usage) {
     auto* inst = b.Bitcast(mod.Types().i32(), 4_i);
 
@@ -49,16 +58,6 @@
     EXPECT_THAT(args[0]->Usages(), testing::UnorderedElementsAre(Usage{inst, 0u}));
 }
 
-TEST_F(IR_BitcastTest, Fail_NullValue) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Bitcast(mod.Types().i32(), nullptr);
-        },
-        "");
-}
-
 TEST_F(IR_BitcastTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
diff --git a/src/tint/ir/block.cc b/src/tint/ir/block.cc
index 4dad6c2..c743065 100644
--- a/src/tint/ir/block.cc
+++ b/src/tint/ir/block.cc
@@ -157,16 +157,4 @@
     inst->next = nullptr;
 }
 
-void Block::SetInstructions(utils::VectorRef<Instruction*> instructions) {
-    for (auto* i : instructions) {
-        Append(i);
-    }
-}
-
-void Block::SetInstructions(std::initializer_list<Instruction*> instructions) {
-    for (auto* i : instructions) {
-        Append(i);
-    }
-}
-
 }  // namespace tint::ir
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 9b9a4ca..60f6b31 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -50,14 +50,6 @@
         return instructions_.last->As<ir::Branch>();
     }
 
-    /// Sets the instructions in the block
-    /// @param instructions the instructions to set
-    void SetInstructions(utils::VectorRef<Instruction*> instructions);
-
-    /// Sets the instructions in the block
-    /// @param instructions the instructions to set
-    void SetInstructions(std::initializer_list<Instruction*> instructions);
-
     /// @returns the instructions in the block
     Instruction* Instructions() { return instructions_.first; }
 
diff --git a/src/tint/ir/block_test.cc b/src/tint/ir/block_test.cc
index 3549a05..e9523c5 100644
--- a/src/tint/ir/block_test.cc
+++ b/src/tint/ir/block_test.cc
@@ -83,35 +83,6 @@
     EXPECT_TRUE(blk->HasBranchTarget());
 }
 
-TEST_F(IR_BlockTest, SetInstructions) {
-    auto* inst1 = b.Loop();
-    auto* inst2 = b.Loop();
-    auto* inst3 = b.Loop();
-
-    auto* blk = b.Block();
-    blk->SetInstructions({inst1, inst2, inst3});
-
-    ASSERT_EQ(inst1->Block(), blk);
-    ASSERT_EQ(inst2->Block(), blk);
-    ASSERT_EQ(inst3->Block(), blk);
-
-    EXPECT_FALSE(blk->IsEmpty());
-    EXPECT_EQ(3u, blk->Length());
-
-    auto* inst = blk->Instructions();
-    ASSERT_EQ(inst, inst1);
-    ASSERT_EQ(inst->prev, nullptr);
-    inst = inst->next;
-
-    ASSERT_EQ(inst, inst2);
-    ASSERT_EQ(inst->prev, inst1);
-    inst = inst->next;
-
-    ASSERT_EQ(inst, inst3);
-    ASSERT_EQ(inst->prev, inst2);
-    ASSERT_EQ(inst->next, nullptr);
-}
-
 TEST_F(IR_BlockTest, Append) {
     auto* inst1 = b.Loop();
     auto* inst2 = b.Loop();
@@ -285,13 +256,12 @@
 }
 
 TEST_F(IR_BlockTest, Replace_Middle) {
-    auto* inst1 = b.Loop();
-    auto* inst2 = b.Loop();
-    auto* inst3 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst1, inst4, inst3});
+    auto* inst1 = blk->Append(b.Loop());
+    auto* inst4 = blk->Append(b.Loop());
+    auto* inst3 = blk->Append(b.Loop());
+
+    auto* inst2 = b.Loop();
     blk->Replace(inst4, inst2);
 
     ASSERT_EQ(inst1->Block(), blk);
@@ -317,12 +287,11 @@
 }
 
 TEST_F(IR_BlockTest, Replace_Start) {
-    auto* inst1 = b.Loop();
-    auto* inst2 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst4, inst2});
+    auto* inst4 = blk->Append(b.Loop());
+    auto* inst2 = blk->Append(b.Loop());
+
+    auto* inst1 = b.Loop();
     blk->Replace(inst4, inst1);
 
     ASSERT_EQ(inst1->Block(), blk);
@@ -343,12 +312,11 @@
 }
 
 TEST_F(IR_BlockTest, Replace_End) {
-    auto* inst1 = b.Loop();
-    auto* inst2 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst1, inst4});
+    auto* inst1 = blk->Append(b.Loop());
+    auto* inst4 = blk->Append(b.Loop());
+
+    auto* inst2 = b.Loop();
     blk->Replace(inst4, inst2);
 
     ASSERT_EQ(inst1->Block(), blk);
@@ -369,11 +337,10 @@
 }
 
 TEST_F(IR_BlockTest, Replace_OnlyNode) {
-    auto* inst1 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst4});
+    auto* inst4 = blk->Append(b.Loop());
+
+    auto* inst1 = b.Loop();
     blk->Replace(inst4, inst1);
 
     ASSERT_EQ(inst1->Block(), blk);
@@ -389,12 +356,10 @@
 }
 
 TEST_F(IR_BlockTest, Remove_Middle) {
-    auto* inst1 = b.Loop();
-    auto* inst2 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst1, inst4, inst2});
+    auto* inst1 = blk->Append(b.Loop());
+    auto* inst4 = blk->Append(b.Loop());
+    auto* inst2 = blk->Append(b.Loop());
     blk->Remove(inst4);
 
     ASSERT_EQ(inst4->Block(), nullptr);
@@ -413,11 +378,9 @@
 }
 
 TEST_F(IR_BlockTest, Remove_Start) {
-    auto* inst1 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst4, inst1});
+    auto* inst4 = blk->Append(b.Loop());
+    auto* inst1 = blk->Append(b.Loop());
     blk->Remove(inst4);
 
     ASSERT_EQ(inst4->Block(), nullptr);
@@ -432,11 +395,9 @@
 }
 
 TEST_F(IR_BlockTest, Remove_End) {
-    auto* inst1 = b.Loop();
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst1, inst4});
+    auto* inst1 = blk->Append(b.Loop());
+    auto* inst4 = blk->Append(b.Loop());
     blk->Remove(inst4);
 
     ASSERT_EQ(inst4->Block(), nullptr);
@@ -451,10 +412,8 @@
 }
 
 TEST_F(IR_BlockTest, Remove_OnlyNode) {
-    auto* inst4 = b.Loop();
-
     auto* blk = b.Block();
-    blk->SetInstructions({inst4});
+    auto* inst4 = blk->Append(b.Loop());
     blk->Remove(inst4);
 
     ASSERT_EQ(inst4->Block(), nullptr);
diff --git a/src/tint/ir/branch.h b/src/tint/ir/branch.h
index 15a7d42..1069529 100644
--- a/src/tint/ir/branch.h
+++ b/src/tint/ir/branch.h
@@ -27,7 +27,7 @@
 namespace tint::ir {
 
 /// A branch instruction.
-class Branch : public utils::Castable<Branch, OperandInstruction<1>> {
+class Branch : public utils::Castable<Branch, OperandInstruction<1, 0>> {
   public:
     ~Branch() override;
 
diff --git a/src/tint/ir/break_if.cc b/src/tint/ir/break_if.cc
index 4a18fb8..3b34c28 100644
--- a/src/tint/ir/break_if.cc
+++ b/src/tint/ir/break_if.cc
@@ -24,19 +24,15 @@
 
 namespace tint::ir {
 
-BreakIf::BreakIf(Value* condition,
-                 ir::Loop* loop,
-                 utils::VectorRef<Value*> args /* = utils::Empty */)
-    : loop_(loop) {
-    TINT_ASSERT(IR, condition);
+BreakIf::BreakIf(Value* condition, ir::Loop* loop, utils::VectorRef<Value*> args) : loop_(loop) {
     TINT_ASSERT(IR, loop_);
 
-    AddOperand(condition);
+    AddOperand(BreakIf::kConditionOperandOffset, condition);
+    AddOperands(BreakIf::kArgsOperandOffset, std::move(args));
+
     if (loop_) {
         loop_->Body()->AddInboundSiblingBranch(this);
-        loop_->Merge()->AddInboundSiblingBranch(this);
     }
-    AddOperands(std::move(args));
 }
 
 BreakIf::~BreakIf() = default;
diff --git a/src/tint/ir/break_if.h b/src/tint/ir/break_if.h
index 1eba580..59046b2 100644
--- a/src/tint/ir/break_if.h
+++ b/src/tint/ir/break_if.h
@@ -29,6 +29,12 @@
 /// A break-if iteration instruction.
 class BreakIf : public utils::Castable<BreakIf, Branch> {
   public:
+    /// The offset in Operands() for the condition
+    static constexpr size_t kConditionOperandOffset = 0;
+
+    /// The base offset in Operands() for the arguments
+    static constexpr size_t kArgsOperandOffset = 1;
+
     /// Constructor
     /// @param condition the break condition
     /// @param loop the loop containing the break-if
@@ -37,10 +43,12 @@
     ~BreakIf() override;
 
     /// @returns the branch arguments
-    utils::Slice<Value* const> Args() override { return operands_.Slice().Offset(1); }
+    utils::Slice<Value* const> Args() override {
+        return operands_.Slice().Offset(kArgsOperandOffset);
+    }
 
     /// @returns the break condition
-    Value* Condition() { return operands_[0]; }
+    Value* Condition() { return operands_[kConditionOperandOffset]; }
 
     /// @returns the loop containing the break-if
     ir::Loop* Loop() { return loop_; }
diff --git a/src/tint/ir/break_if_test.cc b/src/tint/ir/break_if_test.cc
index 9dd095a..2ce17ae 100644
--- a/src/tint/ir/break_if_test.cc
+++ b/src/tint/ir/break_if_test.cc
@@ -37,14 +37,15 @@
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{brk, 2u}));
 }
 
-TEST_F(IR_BreakIfTest, Fail_NullCondition) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.BreakIf(nullptr, b.Loop());
-        },
-        "");
+TEST_F(IR_BreakIfTest, Results) {
+    auto* loop = b.Loop();
+    auto* cond = b.Constant(true);
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+
+    auto* brk = b.BreakIf(cond, loop, arg1, arg2);
+    EXPECT_FALSE(brk->HasResults());
+    EXPECT_FALSE(brk->HasMultiResults());
 }
 
 TEST_F(IR_BreakIfTest, Fail_NullLoop) {
@@ -57,15 +58,5 @@
         "");
 }
 
-TEST_F(IR_BreakIfTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.BreakIf(true, b.Loop(), nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index e2d2b83..2bb295b 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -24,6 +24,8 @@
 
 Builder::Builder(Module& mod) : ir(mod) {}
 
+Builder::Builder(Module& mod, ir::Block* block) : current_block_(block), ir(mod) {}
+
 Builder::~Builder() = default;
 
 ir::Block* Builder::RootBlock() {
@@ -52,7 +54,7 @@
 }
 
 ir::Loop* Builder::Loop() {
-    return ir.values.Create<ir::Loop>(Block(), MultiInBlock(), MultiInBlock(), MultiInBlock());
+    return Append(ir.instructions.Create<ir::Loop>(Block(), MultiInBlock(), MultiInBlock()));
 }
 
 Block* Builder::Case(ir::Switch* s, utils::VectorRef<Switch::CaseSelector> selectors) {
@@ -67,11 +69,11 @@
 }
 
 ir::Discard* Builder::Discard() {
-    return ir.values.Create<ir::Discard>(ir.Types().void_());
+    return Append(ir.instructions.Create<ir::Discard>());
 }
 
 ir::Var* Builder::Var(const type::Pointer* type) {
-    return ir.values.Create<ir::Var>(type);
+    return Append(ir.instructions.Create<ir::Var>(InstructionResult(type)));
 }
 
 ir::BlockParam* Builder::BlockParam(const type::Type* type) {
@@ -82,16 +84,8 @@
     return ir.values.Create<ir::FunctionParam>(type);
 }
 
-ir::Swizzle* Builder::Swizzle(const type::Type* type,
-                              ir::Value* object,
-                              utils::VectorRef<uint32_t> indices) {
-    return ir.values.Create<ir::Swizzle>(type, object, std::move(indices));
-}
-
-ir::Swizzle* Builder::Swizzle(const type::Type* type,
-                              ir::Value* object,
-                              std::initializer_list<uint32_t> indices) {
-    return ir.values.Create<ir::Swizzle>(type, object, utils::Vector<uint32_t, 4>(indices));
+ir::Unreachable* Builder::Unreachable() {
+    return Append(ir.instructions.Create<ir::Unreachable>());
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 1bb2973..9a2c817 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -35,6 +35,7 @@
 #include "src/tint/ir/function.h"
 #include "src/tint/ir/function_param.h"
 #include "src/tint/ir/if.h"
+#include "src/tint/ir/instruction_result.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
@@ -45,9 +46,11 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/swizzle.h"
 #include "src/tint/ir/unary.h"
+#include "src/tint/ir/unreachable.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/ir/var.h"
+#include "src/tint/switch.h"
 #include "src/tint/type/bool.h"
 #include "src/tint/type/f16.h"
 #include "src/tint/type/f32.h"
@@ -73,13 +76,33 @@
     using DisableIfVectorLike = utils::traits::EnableIf<
         !utils::IsVectorLike<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
+    template <typename T>
+    T* Append(T* val) {
+        if (current_block_) {
+            current_block_->Append(val);
+        }
+        return val;
+    }
+
+    /// If set, any created instruction will be auto-appended to the block.
+    ir::Block* current_block_ = nullptr;
+
   public:
     /// Constructor
     /// @param mod the ir::Module to wrap with this builder
     explicit Builder(Module& mod);
+    /// Constructor
+    /// @param mod the ir::Module to wrap with this builder
+    /// @param block the block to insert too
+    Builder(Module& mod, ir::Block* block);
     /// Destructor
     ~Builder();
 
+    /// Creates a new builder wrapping the given block
+    /// @param b the block to set as the current block
+    /// @returns the builder
+    Builder With(Block* b) { return Builder(ir, b); }
+
     /// @returns a new block
     ir::Block* Block();
 
@@ -102,8 +125,8 @@
     /// @returns the flow node
     template <typename T>
     ir::If* If(T&& condition) {
-        return ir.values.Create<ir::If>(Value(std::forward<T>(condition)), Block(), Block(),
-                                        MultiInBlock());
+        return Append(
+            ir.instructions.Create<ir::If>(Value(std::forward<T>(condition)), Block(), Block()));
     }
 
     /// Creates a loop flow node
@@ -115,7 +138,7 @@
     /// @returns the flow node
     template <typename T>
     ir::Switch* Switch(T&& condition) {
-        return ir.values.Create<ir::Switch>(Value(std::forward<T>(condition)), MultiInBlock());
+        return Append(ir.instructions.Create<ir::Switch>(Value(std::forward<T>(condition))));
     }
 
     /// Creates a case flow node for the given case branch.
@@ -170,15 +193,30 @@
         return Constant(std::forward<T>(number));
     }
 
+    /// Pass-through overload for nullptr values
+    /// @returns nullptr
+    ir::Value* Value(std::nullptr_t) { return nullptr; }
+
     /// Pass-through overload for Value()
     /// @param v the ir::Value pointer
     /// @returns @p v
     ir::Value* Value(ir::Value* v) { return v; }
 
-    /// Creates a ir::Constant for the given boolean
-    /// @param v the boolean value
-    /// @returns the new constant
-    ir::Constant* Value(bool v) { return Constant(v); }
+    /// Extract the first result from the instruction
+    /// @param inst the instruction
+    /// @returns the result value
+    ir::Value* Value(ir::Instruction* inst) {
+        TINT_ASSERT(IR, inst->HasResults() && !inst->HasMultiResults());
+        return inst->Result();
+    }
+
+    /// Creates a value from the given number
+    /// @param n the number
+    /// @returns the value
+    template <typename T>
+    ir::Value* Value(Number<T> n) {
+        return Constant(n);
+    }
 
     /// Pass-through overload for Values() with vector-like argument
     /// @param vec the vector of ir::Value*
@@ -211,8 +249,9 @@
     /// @returns the operation
     template <typename LHS, typename RHS>
     ir::Binary* Binary(enum Binary::Kind kind, const type::Type* type, LHS&& lhs, RHS&& rhs) {
-        return ir.values.Create<ir::Binary>(kind, type, Value(std::forward<LHS>(lhs)),
-                                            Value(std::forward<RHS>(rhs)));
+        return Append(ir.instructions.Create<ir::Binary>(InstructionResult(type), kind,
+                                                         Value(std::forward<LHS>(lhs)),
+                                                         Value(std::forward<RHS>(rhs))));
     }
 
     /// Creates an And operation
@@ -394,7 +433,8 @@
     /// @returns the operation
     template <typename VAL>
     ir::Unary* Unary(enum Unary::Kind kind, const type::Type* type, VAL&& val) {
-        return ir.values.Create<ir::Unary>(kind, type, Value(std::forward<VAL>(val)));
+        return Append(ir.instructions.Create<ir::Unary>(InstructionResult(type), kind,
+                                                        Value(std::forward<VAL>(val))));
     }
 
     /// Creates a Complement operation
@@ -430,7 +470,8 @@
     /// @returns the instruction
     template <typename VAL>
     ir::Bitcast* Bitcast(const type::Type* type, VAL&& val) {
-        return ir.values.Create<ir::Bitcast>(type, Value(std::forward<VAL>(val)));
+        return Append(ir.instructions.Create<ir::Bitcast>(InstructionResult(type),
+                                                          Value(std::forward<VAL>(val))));
     }
 
     /// Creates a discard instruction
@@ -444,7 +485,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::UserCall* Call(const type::Type* type, ir::Function* func, ARGS&&... args) {
-        return ir.values.Create<ir::UserCall>(type, func, Values(std::forward<ARGS>(args)...));
+        return Append(ir.instructions.Create<ir::UserCall>(InstructionResult(type), func,
+                                                           Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a builtin call instruction
@@ -454,7 +496,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::BuiltinCall* Call(const type::Type* type, builtin::Function func, ARGS&&... args) {
-        return ir.values.Create<ir::BuiltinCall>(type, func, Values(std::forward<ARGS>(args)...));
+        return Append(ir.instructions.Create<ir::BuiltinCall>(InstructionResult(type), func,
+                                                              Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a value conversion instruction
@@ -463,7 +506,8 @@
     /// @returns the instruction
     template <typename VAL>
     ir::Convert* Convert(const type::Type* to, VAL&& val) {
-        return ir.values.Create<ir::Convert>(to, Value(std::forward<VAL>(val)));
+        return Append(ir.instructions.Create<ir::Convert>(InstructionResult(to),
+                                                          Value(std::forward<VAL>(val))));
     }
 
     /// Creates a value constructor instruction
@@ -472,7 +516,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::Construct* Construct(const type::Type* type, ARGS&&... args) {
-        return ir.values.Create<ir::Construct>(type, Values(std::forward<ARGS>(args)...));
+        return Append(ir.instructions.Create<ir::Construct>(InstructionResult(type),
+                                                            Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a load instruction
@@ -480,16 +525,19 @@
     /// @returns the instruction
     template <typename VAL>
     ir::Load* Load(VAL&& from) {
-        return ir.values.Create<ir::Load>(Value(std::forward<VAL>(from)));
+        auto* val = Value(std::forward<VAL>(from));
+        return Append(
+            ir.instructions.Create<ir::Load>(InstructionResult(val->Type()->UnwrapPtr()), val));
     }
 
     /// Creates a store instruction
     /// @param to the expression being stored too
     /// @param from the expression being stored
     /// @returns the instruction
-    template <typename ARG>
-    ir::Store* Store(ir::Value* to, ARG&& from) {
-        return ir.values.Create<ir::Store>(to, Value(std::forward<ARG>(from)));
+    template <typename TO, typename ARG>
+    ir::Store* Store(TO&& to, ARG&& from) {
+        return Append(ir.instructions.Create<ir::Store>(Value(std::forward<TO>(to)),
+                                                        Value(std::forward<ARG>(from))));
     }
 
     /// Creates a new `var` declaration
@@ -500,7 +548,9 @@
     /// Creates a return instruction
     /// @param func the function being returned
     /// @returns the instruction
-    ir::Return* Return(ir::Function* func) { return ir.values.Create<ir::Return>(func); }
+    ir::Return* Return(ir::Function* func) {
+        return Append(ir.instructions.Create<ir::Return>(func));
+    }
 
     /// Creates a return instruction
     /// @param func the function being returned
@@ -508,7 +558,7 @@
     /// @returns the instruction
     template <typename ARG>
     ir::Return* Return(ir::Function* func, ARG&& value) {
-        return ir.values.Create<ir::Return>(func, Value(std::forward<ARG>(value)));
+        return Append(ir.instructions.Create<ir::Return>(func, Value(std::forward<ARG>(value))));
     }
 
     /// Creates a loop next iteration instruction
@@ -517,7 +567,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::NextIteration* NextIteration(ir::Loop* loop, ARGS&&... args) {
-        return ir.values.Create<ir::NextIteration>(loop, Values(std::forward<ARGS>(args)...));
+        return Append(
+            ir.instructions.Create<ir::NextIteration>(loop, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a loop break-if instruction
@@ -527,8 +578,8 @@
     /// @returns the instruction
     template <typename CONDITION, typename... ARGS>
     ir::BreakIf* BreakIf(CONDITION&& condition, ir::Loop* loop, ARGS&&... args) {
-        return ir.values.Create<ir::BreakIf>(Value(std::forward<CONDITION>(condition)), loop,
-                                             Values(std::forward<ARGS>(args)...));
+        return Append(ir.instructions.Create<ir::BreakIf>(
+            Value(std::forward<CONDITION>(condition)), loop, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a continue instruction
@@ -537,7 +588,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::Continue* Continue(ir::Loop* loop, ARGS&&... args) {
-        return ir.values.Create<ir::Continue>(loop, Values(std::forward<ARGS>(args)...));
+        return Append(
+            ir.instructions.Create<ir::Continue>(loop, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates an exit switch instruction
@@ -546,7 +598,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::ExitSwitch* ExitSwitch(ir::Switch* sw, ARGS&&... args) {
-        return ir.values.Create<ir::ExitSwitch>(sw, Values(std::forward<ARGS>(args)...));
+        return Append(
+            ir.instructions.Create<ir::ExitSwitch>(sw, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates an exit loop instruction
@@ -555,7 +608,8 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::ExitLoop* ExitLoop(ir::Loop* loop, ARGS&&... args) {
-        return ir.values.Create<ir::ExitLoop>(loop, Values(std::forward<ARGS>(args)...));
+        return Append(
+            ir.instructions.Create<ir::ExitLoop>(loop, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates an exit if instruction
@@ -564,7 +618,20 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::ExitIf* ExitIf(ir::If* i, ARGS&&... args) {
-        return ir.values.Create<ir::ExitIf>(i, Values(std::forward<ARGS>(args)...));
+        return Append(ir.instructions.Create<ir::ExitIf>(i, Values(std::forward<ARGS>(args)...)));
+    }
+
+    /// Creates an exit instruction for the given control instruction
+    /// @param inst the control instruction being exited
+    /// @param args the branch arguments
+    /// @returns the exit instruction, or nullptr if the control instruction is not supported.
+    template <typename... ARGS>
+    ir::Branch* Exit(ir::ControlInstruction* inst, ARGS&&... args) {
+        return tint::Switch(
+            inst,  //
+            [&](ir::If* i) { return ExitIf(i, std::forward<ARGS>(args)...); },
+            [&](ir::Loop* i) { return ExitLoop(i, std::forward<ARGS>(args)...); },
+            [&](ir::Switch* i) { return ExitSwitch(i, std::forward<ARGS>(args)...); });
     }
 
     /// Creates a new `BlockParam`
@@ -582,9 +649,11 @@
     /// @param object the object being accessed
     /// @param indices the access indices
     /// @returns the instruction
-    template <typename... ARGS>
-    ir::Access* Access(const type::Type* type, ir::Value* object, ARGS&&... indices) {
-        return ir.values.Create<ir::Access>(type, object, Values(std::forward<ARGS>(indices)...));
+    template <typename OBJ, typename... ARGS>
+    ir::Access* Access(const type::Type* type, OBJ&& object, ARGS&&... indices) {
+        return Append(ir.instructions.Create<ir::Access>(InstructionResult(type),
+                                                         Value(std::forward<OBJ>(object)),
+                                                         Values(std::forward<ARGS>(indices)...)));
     }
 
     /// Creates a new `Swizzle`
@@ -592,23 +661,41 @@
     /// @param object the object being swizzled
     /// @param indices the swizzle indices
     /// @returns the instruction
-    ir::Swizzle* Swizzle(const type::Type* type,
-                         ir::Value* object,
-                         utils::VectorRef<uint32_t> indices);
+    template <typename OBJ>
+    ir::Swizzle* Swizzle(const type::Type* type, OBJ&& object, utils::VectorRef<uint32_t> indices) {
+        return Append(ir.instructions.Create<ir::Swizzle>(
+            InstructionResult(type), Value(std::forward<OBJ>(object)), std::move(indices)));
+    }
 
     /// Creates a new `Swizzle`
     /// @param type the return type
     /// @param object the object being swizzled
     /// @param indices the swizzle indices
     /// @returns the instruction
+    template <typename OBJ>
     ir::Swizzle* Swizzle(const type::Type* type,
-                         ir::Value* object,
-                         std::initializer_list<uint32_t> indices);
+                         OBJ&& object,
+                         std::initializer_list<uint32_t> indices) {
+        return Append(ir.instructions.Create<ir::Swizzle>(InstructionResult(type),
+                                                          Value(std::forward<OBJ>(object)),
+                                                          utils::Vector<uint32_t, 4>(indices)));
+    }
+
+    /// Creates an unreachable instruction
+    /// @returns the instruction
+    ir::Unreachable* Unreachable();
 
     /// Retrieves the root block for the module, creating if necessary
     /// @returns the root block
     ir::Block* RootBlock();
 
+    /// Creates a new runtime value
+    /// @param type the return type
+    /// @returns the value
+    ir::InstructionResult* InstructionResult(const type::Type* type) {
+        return ir.values.Create<ir::InstructionResult>(type);
+    }
+
     /// The IR module.
     Module& ir;
 };
diff --git a/src/tint/ir/builtin_call.cc b/src/tint/ir/builtin_call.cc
index a230510..2726191 100644
--- a/src/tint/ir/builtin_call.cc
+++ b/src/tint/ir/builtin_call.cc
@@ -22,13 +22,15 @@
 
 namespace tint::ir {
 
-BuiltinCall::BuiltinCall(const type::Type* ty,
+BuiltinCall::BuiltinCall(InstructionResult* result,
                          builtin::Function func,
                          utils::VectorRef<Value*> arguments)
-    : Base(ty), func_(func) {
+    : func_(func) {
     TINT_ASSERT(IR, func != builtin::Function::kNone);
     TINT_ASSERT(IR, func != builtin::Function::kTintMaterialize);
-    AddOperands(std::move(arguments));
+
+    AddOperands(BuiltinCall::kArgsOperandOffset, std::move(arguments));
+    AddResult(result);
 }
 
 BuiltinCall::~BuiltinCall() = default;
diff --git a/src/tint/ir/builtin_call.h b/src/tint/ir/builtin_call.h
index 4aef1eb..3a142a5 100644
--- a/src/tint/ir/builtin_call.h
+++ b/src/tint/ir/builtin_call.h
@@ -24,11 +24,14 @@
 /// A builtin call instruction in the IR.
 class BuiltinCall : public utils::Castable<BuiltinCall, Call> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
-    /// @param res_type the result type
+    /// @param result the result value
     /// @param func the builtin function
     /// @param args the conversion arguments
-    BuiltinCall(const type::Type* res_type,
+    BuiltinCall(InstructionResult* result,
                 builtin::Function func,
                 utils::VectorRef<Value*> args = utils::Empty);
     ~BuiltinCall() override;
diff --git a/src/tint/ir/builtin_call_test.cc b/src/tint/ir/builtin_call_test.cc
index fb6580e..f0928a0 100644
--- a/src/tint/ir/builtin_call_test.cc
+++ b/src/tint/ir/builtin_call_test.cc
@@ -32,6 +32,17 @@
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{builtin, 1u}));
 }
 
+TEST_F(IR_BuiltinCallTest, Result) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* builtin = b.Call(mod.Types().f32(), builtin::Function::kAbs, arg1, arg2);
+
+    EXPECT_TRUE(builtin->HasResults());
+    EXPECT_FALSE(builtin->HasMultiResults());
+    EXPECT_TRUE(builtin->Result()->Is<InstructionResult>());
+    EXPECT_EQ(builtin->Result()->Source(), builtin);
+}
+
 TEST_F(IR_BuiltinCallTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
@@ -62,15 +73,5 @@
         "");
 }
 
-TEST_F(IR_BuiltinCallTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Call(mod.Types().f32(), builtin::Function::kAbs, nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/call.cc b/src/tint/ir/call.cc
index 30174d4..ead6124 100644
--- a/src/tint/ir/call.cc
+++ b/src/tint/ir/call.cc
@@ -20,9 +20,7 @@
 
 namespace tint::ir {
 
-Call::Call(const type::Type* res_ty) : result_type_(res_ty) {
-    TINT_ASSERT(IR, result_type_);
-}
+Call::Call() = default;
 
 Call::~Call() = default;
 
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
index c26aa5d..041c012 100644
--- a/src/tint/ir/call.h
+++ b/src/tint/ir/call.h
@@ -21,25 +21,16 @@
 namespace tint::ir {
 
 /// A Call instruction in the IR.
-class Call : public utils::Castable<Call, OperandInstruction<4>> {
+class Call : public utils::Castable<Call, OperandInstruction<4, 1>> {
   public:
     ~Call() override;
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the call arguments
     virtual utils::Slice<Value* const> Args() { return operands_.Slice(); }
 
   protected:
     /// Constructor
-    Call() = delete;
-    /// Constructor
-    /// @param result_type the result type
-    explicit Call(const type::Type* result_type);
-
-  private:
-    const type::Type* result_type_ = nullptr;
+    Call();
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/construct.cc b/src/tint/ir/construct.cc
index c4a330e..6dbbd45 100644
--- a/src/tint/ir/construct.cc
+++ b/src/tint/ir/construct.cc
@@ -22,8 +22,9 @@
 
 namespace tint::ir {
 
-Construct::Construct(const type::Type* ty, utils::VectorRef<Value*> arguments) : Base(ty) {
-    AddOperands(std::move(arguments));
+Construct::Construct(InstructionResult* result, utils::VectorRef<Value*> arguments) {
+    AddOperands(Construct::kArgsOperandOffset, std::move(arguments));
+    AddResult(result);
 }
 
 Construct::~Construct() = default;
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
index 4a15849..a2fe9b3 100644
--- a/src/tint/ir/construct.h
+++ b/src/tint/ir/construct.h
@@ -23,10 +23,13 @@
 /// A constructor instruction in the IR.
 class Construct : public utils::Castable<Construct, Call> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
-    /// @param type the result type
+    /// @param result the result value
     /// @param args the constructor arguments
-    explicit Construct(const type::Type* type, utils::VectorRef<Value*> args = utils::Empty);
+    explicit Construct(InstructionResult* result, utils::VectorRef<Value*> args = utils::Empty);
     ~Construct() override;
 };
 
diff --git a/src/tint/ir/construct_test.cc b/src/tint/ir/construct_test.cc
index fa31ff9..956aefd 100644
--- a/src/tint/ir/construct_test.cc
+++ b/src/tint/ir/construct_test.cc
@@ -33,6 +33,17 @@
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{c, 1u}));
 }
 
+TEST_F(IR_ConstructTest, Result) {
+    auto* arg1 = b.Constant(true);
+    auto* arg2 = b.Constant(false);
+    auto* c = b.Construct(mod.Types().f32(), arg1, arg2);
+
+    EXPECT_TRUE(c->HasResults());
+    EXPECT_FALSE(c->HasMultiResults());
+    EXPECT_TRUE(c->Result()->Is<InstructionResult>());
+    EXPECT_EQ(c, c->Result()->Source());
+}
+
 TEST_F(IR_ConstructTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
@@ -43,15 +54,5 @@
         "");
 }
 
-TEST_F(IR_ConstructTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Construct(mod.Types().f32(), nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/continue.cc b/src/tint/ir/continue.cc
index e1e8d84..5bb29ff 100644
--- a/src/tint/ir/continue.cc
+++ b/src/tint/ir/continue.cc
@@ -24,14 +24,14 @@
 
 namespace tint::ir {
 
-Continue::Continue(ir::Loop* loop, utils::VectorRef<Value*> args /* = utils::Empty */)
-    : loop_(loop) {
+Continue::Continue(ir::Loop* loop, utils::VectorRef<Value*> args) : loop_(loop) {
     TINT_ASSERT(IR, loop_);
 
+    AddOperands(Continue::kArgsOperandOffset, std::move(args));
+
     if (loop_) {
         loop_->Continuing()->AddInboundSiblingBranch(this);
     }
-    AddOperands(std::move(args));
 }
 
 Continue::~Continue() = default;
diff --git a/src/tint/ir/continue.h b/src/tint/ir/continue.h
index f9c5455..6beda97 100644
--- a/src/tint/ir/continue.h
+++ b/src/tint/ir/continue.h
@@ -28,6 +28,9 @@
 /// A continue instruction.
 class Continue : public utils::Castable<Continue, Branch> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
     /// @param loop the loop owning the continue block
     /// @param args the branch arguments
diff --git a/src/tint/ir/continue_test.cc b/src/tint/ir/continue_test.cc
index 29ea605..d16defb 100644
--- a/src/tint/ir/continue_test.cc
+++ b/src/tint/ir/continue_test.cc
@@ -35,6 +35,17 @@
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{brk, 1u}));
 }
 
+TEST_F(IR_ContinueTest, Results) {
+    auto* loop = b.Loop();
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+
+    auto* brk = b.Continue(loop, arg1, arg2);
+
+    EXPECT_FALSE(brk->HasResults());
+    EXPECT_FALSE(brk->HasMultiResults());
+}
+
 TEST_F(IR_ContinueTest, Fail_NullLoop) {
     EXPECT_FATAL_FAILURE(
         {
@@ -45,15 +56,5 @@
         "");
 }
 
-TEST_F(IR_ContinueTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Continue(b.Loop(), nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/control_instruction.cc b/src/tint/ir/control_instruction.cc
index efcac64..6451622 100644
--- a/src/tint/ir/control_instruction.cc
+++ b/src/tint/ir/control_instruction.cc
@@ -20,4 +20,12 @@
 
 ControlInstruction::~ControlInstruction() = default;
 
+void ControlInstruction::AddExit(Exit* exit) {
+    exits_.Add(exit);
+}
+
+void ControlInstruction::RemoveExit(Exit* exit) {
+    exits_.Remove(exit);
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/control_instruction.h b/src/tint/ir/control_instruction.h
index cd0e7d3..ebe5825 100644
--- a/src/tint/ir/control_instruction.h
+++ b/src/tint/ir/control_instruction.h
@@ -15,16 +15,65 @@
 #ifndef SRC_TINT_IR_CONTROL_INSTRUCTION_H_
 #define SRC_TINT_IR_CONTROL_INSTRUCTION_H_
 
+#include <utility>
+
 #include "src/tint/ir/branch.h"
+#include "src/tint/ir/operand_instruction.h"
+
+// Forward declarations
+namespace tint::ir {
+class Block;
+class Exit;
+}  // namespace tint::ir
 
 namespace tint::ir {
 
 /// Base class of instructions that perform branches to two or more blocks, owned by the
 /// ControlInstruction.
-class ControlInstruction : public utils::Castable<ControlInstruction, Branch> {
+class ControlInstruction : public utils::Castable<ControlInstruction, OperandInstruction<1, 1>> {
   public:
     /// Destructor
     ~ControlInstruction() override;
+
+    /// Calls @p cb for each block owned by this control instruction
+    /// @param cb the function to call once for each block
+    virtual void ForeachBlock(const std::function<void(ir::Block*)>& cb) = 0;
+
+    /// Sets the results of the control instruction
+    /// @param values the new result values
+    void SetResults(utils::VectorRef<InstructionResult*> values) {
+        for (auto* value : results_) {
+            value->SetSource(nullptr);
+        }
+        results_ = std::move(values);
+        for (auto* value : results_) {
+            value->SetSource(this);
+        }
+    }
+
+    /// Sets the results of the control instruction
+    /// @param values the new result values
+    template <typename... ARGS,
+              typename = std::enable_if_t<!utils::IsVectorLike<
+                  utils::traits::Decay<utils::traits::NthTypeOf<0, ARGS..., void>>>>>
+    void SetResults(ARGS&&... values) {
+        SetResults(utils::Vector{std::forward<ARGS>(values)...});
+    }
+
+    /// @return All the exit branches for the flow control instruction
+    const utils::Hashset<Exit*, 2>& Exits() const { return exits_; }
+
+    /// Adds the exit to the flow control instruction
+    /// @param exit the exit instruction
+    void AddExit(Exit* exit);
+
+    /// Removes the exit to the flow control instruction
+    /// @param exit the exit instruction
+    void RemoveExit(Exit* exit);
+
+  protected:
+    /// The flow control exits
+    utils::Hashset<Exit*, 2> exits_;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/convert.cc b/src/tint/ir/convert.cc
index ed5961b..51e99f3 100644
--- a/src/tint/ir/convert.cc
+++ b/src/tint/ir/convert.cc
@@ -22,10 +22,9 @@
 
 namespace tint::ir {
 
-Convert::Convert(const type::Type* to_type, Value* value) : Base(to_type) {
-    TINT_ASSERT_OR_RETURN(IR, value);
-
-    AddOperand(value);
+Convert::Convert(InstructionResult* result, Value* value) {
+    AddOperand(Convert::kValueOperandOffset, value);
+    AddResult(result);
 }
 
 Convert::~Convert() = default;
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
index 0bb07cf..59d02d7 100644
--- a/src/tint/ir/convert.h
+++ b/src/tint/ir/convert.h
@@ -24,10 +24,13 @@
 /// A value conversion instruction in the IR.
 class Convert : public utils::Castable<Convert, Call> {
   public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
     /// Constructor
-    /// @param to_type the target conversion type
+    /// @param result the result value
     /// @param value the value to convert
-    Convert(const type::Type* to_type, Value* value);
+    Convert(InstructionResult* result, Value* value);
     ~Convert() override;
 };
 
diff --git a/src/tint/ir/convert_test.cc b/src/tint/ir/convert_test.cc
index d1affd4..5851d80 100644
--- a/src/tint/ir/convert_test.cc
+++ b/src/tint/ir/convert_test.cc
@@ -32,14 +32,13 @@
         "");
 }
 
-TEST_F(IR_ConvertTest, Fail_NoArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Convert(mod.Types().f32(), nullptr);
-        },
-        "");
+TEST_F(IR_ConvertTest, Results) {
+    auto* c = b.Convert(mod.Types().i32(), 1_u);
+
+    EXPECT_TRUE(c->HasResults());
+    EXPECT_FALSE(c->HasMultiResults());
+    EXPECT_TRUE(c->Result()->Is<InstructionResult>());
+    EXPECT_EQ(c->Result()->Source(), c);
 }
 
 }  // namespace
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 6d17316..27598f0 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -33,6 +33,7 @@
 #include "src/tint/ir/exit_loop.h"
 #include "src/tint/ir/exit_switch.h"
 #include "src/tint/ir/if.h"
+#include "src/tint/ir/instruction_result.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/multi_in_block.h"
@@ -42,6 +43,7 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/swizzle.h"
 #include "src/tint/ir/transform/block_decorated_structs.h"
+#include "src/tint/ir/unreachable.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/var.h"
 #include "src/tint/switch.h"
@@ -82,13 +84,6 @@
     current_output_start_pos_ = out_.tellp();
 }
 
-void Disassembler::EmitBlockInstructions(Block* b) {
-    for (auto* inst : *b) {
-        Indent();
-        EmitInstruction(inst);
-    }
-}
-
 size_t Disassembler::IdOf(Block* node) {
     TINT_ASSERT(IR, node);
     return block_ids_.GetOrCreate(node, [&] { return block_ids_.Count(); });
@@ -118,7 +113,7 @@
     if (mod_.root_block) {
         Indent() << "# Root block";
         EmitLine();
-        WalkInternal(mod_.root_block);
+        EmitBlock(mod_.root_block);
         EmitLine();
     }
 
@@ -128,15 +123,7 @@
     return out_.str();
 }
 
-void Disassembler::Walk(Block* blk) {
-    if (visited_.Contains(blk)) {
-        return;
-    }
-    visited_.Add(blk);
-    WalkInternal(blk);
-}
-
-void Disassembler::WalkInternal(Block* blk) {
+void Disassembler::EmitBlock(Block* blk) {
     Indent();
 
     SourceMarker sm(this);
@@ -154,7 +141,10 @@
     EmitLine();
     {
         ScopedIndent si(indent_size_);
-        EmitBlockInstructions(blk);
+        for (auto* inst : *blk) {
+            Indent();
+            EmitInstruction(inst);
+        }
     }
     Indent() << "}";
 
@@ -279,17 +269,19 @@
 
     {
         ScopedIndent si(indent_size_);
-        Walk(func->StartTarget());
+        EmitBlock(func->StartTarget());
     }
     Indent() << "}";
     EmitLine();
 }
 
+void Disassembler::EmitValueWithType(Instruction* val) {
+    EmitValueWithType(val->Result());
+}
+
 void Disassembler::EmitValueWithType(Value* val) {
     EmitValue(val);
-    if (auto* i = val->As<ir::Instruction>(); i->Type() != nullptr) {
-        out_ << ":" << i->Type()->FriendlyName();
-    }
+    out_ << ":" << val->Type()->FriendlyName();
 }
 
 void Disassembler::EmitValue(Value* val) {
@@ -338,10 +330,16 @@
             };
             emit(constant->Value());
         },
-        [&](ir::Instruction* i) { out_ << "%" << IdOf(i); },
+        [&](ir::InstructionResult* rv) { out_ << "%" << IdOf(rv); },
         [&](ir::BlockParam* p) { out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName(); },
         [&](ir::FunctionParam* p) { out_ << "%" << IdOf(p); },
-        [&](Default) { out_ << "Unknown value: " << val->TypeInfo().name; });
+        [&](Default) {
+            if (val == nullptr) {
+                out_ << "undef";
+            } else {
+                out_ << "Unknown value: " << val->TypeInfo().name;
+            }
+        });
 }
 
 void Disassembler::EmitInstructionName(std::string_view name, Instruction* inst) {
@@ -494,11 +492,15 @@
 
 void Disassembler::EmitIf(If* i) {
     SourceMarker sm(this);
+    if (i->Result()) {
+        EmitValueWithType(i->Result());
+        out_ << " = ";
+    }
     out_ << "if ";
     EmitOperand(i, i->Condition(), If::kConditionOperandOffset);
 
-    bool has_true = i->True()->HasBranchTarget();
-    bool has_false = i->False()->HasBranchTarget();
+    bool has_true = !i->True()->IsEmpty();
+    bool has_false = !i->False()->IsEmpty();
 
     out_ << " [";
     if (has_true) {
@@ -510,9 +512,6 @@
         }
         out_ << "f: %b" << IdOf(i->False());
     }
-    if (i->Merge()->HasBranchTarget()) {
-        out_ << ", m: %b" << IdOf(i->Merge());
-    }
     out_ << "]";
     sm.Store(i);
 
@@ -523,7 +522,7 @@
         Indent() << "# True block";
         EmitLine();
 
-        Walk(i->True());
+        EmitBlock(i->True());
         EmitLine();
     }
     if (has_false) {
@@ -531,65 +530,50 @@
         Indent() << "# False block";
         EmitLine();
 
-        Walk(i->False());
-        EmitLine();
-    }
-    if (i->Merge()->HasBranchTarget()) {
-        Indent() << "# Merge block";
-        EmitLine();
-        Walk(i->Merge());
+        EmitBlock(i->False());
         EmitLine();
     }
 }
 
 void Disassembler::EmitLoop(Loop* l) {
-    utils::Vector<std::string, 4> parts;
-    if (l->Initializer()->HasBranchTarget()) {
+    utils::Vector<std::string, 3> parts;
+    if (!l->Initializer()->IsEmpty()) {
         parts.Push("i: %b" + std::to_string(IdOf(l->Initializer())));
     }
-    if (l->Body()->HasBranchTarget()) {
+    if (!l->Body()->IsEmpty()) {
         parts.Push("b: %b" + std::to_string(IdOf(l->Body())));
     }
 
-    if (l->Continuing()->HasBranchTarget()) {
+    if (!l->Continuing()->IsEmpty()) {
         parts.Push("c: %b" + std::to_string(IdOf(l->Continuing())));
     }
-    if (l->Merge()->HasBranchTarget()) {
-        parts.Push("m: %b" + std::to_string(IdOf(l->Merge())));
-    }
     SourceMarker sm(this);
     out_ << "loop [" << utils::Join(parts, ", ") << "]";
     sm.Store(l);
+
     EmitLine();
 
-    if (l->Initializer()->HasBranchTarget()) {
+    if (!l->Initializer()->IsEmpty()) {
         ScopedIndent si(indent_size_);
         Indent() << "# Initializer block";
         EmitLine();
-        Walk(l->Initializer());
+        EmitBlock(l->Initializer());
         EmitLine();
     }
 
-    if (l->Body()->HasBranchTarget()) {
+    if (!l->Body()->IsEmpty()) {
         ScopedIndent si(indent_size_);
         Indent() << "# Body block";
         EmitLine();
-        Walk(l->Body());
+        EmitBlock(l->Body());
         EmitLine();
     }
 
-    if (l->Continuing()->HasBranchTarget()) {
+    if (!l->Continuing()->IsEmpty()) {
         ScopedIndent si(indent_size_);
         Indent() << "# Continuing block";
         EmitLine();
-        Walk(l->Continuing());
-        EmitLine();
-    }
-    if (l->Merge()->HasBranchTarget()) {
-        Indent() << "# Merge block";
-        EmitLine();
-
-        Walk(l->Merge());
+        EmitBlock(l->Continuing());
         EmitLine();
     }
 }
@@ -614,10 +598,7 @@
                 EmitValue(selector.val);
             }
         }
-        out_ << ", %b" << IdOf(c.Start()) << ")";
-    }
-    if (s->Merge()->HasBranchTarget()) {
-        out_ << ", m: %b" << IdOf(s->Merge());
+        out_ << ", %b" << IdOf(c.Block()) << ")";
     }
     out_ << "]";
     EmitLine();
@@ -627,14 +608,7 @@
         Indent() << "# Case block";
         EmitLine();
 
-        Walk(c.Start());
-        EmitLine();
-    }
-    if (s->Merge()->HasBranchTarget()) {
-        Indent() << "# Merge block";
-        EmitLine();
-
-        Walk(s->Merge());
+        EmitBlock(c.Block());
         EmitLine();
     }
 }
@@ -642,18 +616,20 @@
 void Disassembler::EmitBranch(Branch* b) {
     SourceMarker sm(this);
     tint::Switch(
-        b,  //
-        [&](Return*) { out_ << "ret"; },
-        [&](Continue* cont) { out_ << "continue %b" << IdOf(cont->Loop()->Continuing()); },
-        [&](ExitIf* ei) { out_ << "exit_if %b" << IdOf(ei->If()->Merge()); },
-        [&](ExitSwitch* es) { out_ << "exit_switch %b" << IdOf(es->Switch()->Merge()); },
-        [&](ExitLoop* el) { out_ << "exit_loop %b" << IdOf(el->Loop()->Merge()); },
-        [&](NextIteration* ni) { out_ << "next_iteration %b" << IdOf(ni->Loop()->Body()); },
-        [&](BreakIf* bi) {
+        b,                                                                                        //
+        [&](ir::Return*) { out_ << "ret"; },                                                      //
+        [&](ir::Continue* cont) { out_ << "continue %b" << IdOf(cont->Loop()->Continuing()); },   //
+        [&](ir::ExitIf*) { out_ << "exit_if"; },                                                  //
+        [&](ir::ExitSwitch*) { out_ << "exit_switch"; },                                          //
+        [&](ir::ExitLoop*) { out_ << "exit_loop"; },                                              //
+        [&](ir::NextIteration* ni) { out_ << "next_iteration %b" << IdOf(ni->Loop()->Body()); },  //
+        [&](ir::Unreachable*) { out_ << "unreachable"; },                                         //
+        [&](ir::BreakIf* bi) {
             out_ << "break_if ";
             EmitValue(bi->Condition());
             out_ << " %b" << IdOf(bi->Loop()->Body());
         },
+        [&](Unreachable*) { out_ << "unreachable"; },
         [&](Default) { out_ << "Unknown branch " << b->TypeInfo().name; });
 
     if (!b->Args().IsEmpty()) {
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index 6d7248c..71773f1 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -48,10 +48,6 @@
     /// @returns the string representation of the module
     std::string Disassemble();
 
-    /// Writes the block instructions to the stream
-    /// @param b the block containing the instructions
-    void EmitBlockInstructions(Block* b);
-
     /// @returns the string representation
     std::string AsString() const { return out_.str(); }
 
@@ -113,14 +109,14 @@
     size_t IdOf(Block* blk);
     std::string_view IdOf(Value* node);
 
-    void Walk(Block* blk);
-    void WalkInternal(Block* blk);
+    void EmitBlock(Block* blk);
     void EmitFunction(Function* func);
     void EmitParamAttributes(FunctionParam* p);
     void EmitReturnAttributes(Function* func);
     void EmitBindingPoint(BindingPoint p);
     void EmitLocation(Location loc);
     void EmitInstruction(Instruction* inst);
+    void EmitValueWithType(Instruction* val);
     void EmitValueWithType(Value* val);
     void EmitValue(Value* val);
     void EmitValueList(utils::Slice<ir::Value* const> values);
@@ -141,7 +137,6 @@
 
     Module& mod_;
     utils::StringStream out_;
-    utils::Hashset<Block*, 32> visited_;
     utils::Hashmap<Block*, size_t, 32> block_ids_;
     utils::Hashmap<Value*, std::string, 32> value_ids_;
     uint32_t indent_size_ = 0;
diff --git a/src/tint/ir/discard.cc b/src/tint/ir/discard.cc
index ab9a511..f5c53a3 100644
--- a/src/tint/ir/discard.cc
+++ b/src/tint/ir/discard.cc
@@ -20,11 +20,7 @@
 
 namespace tint::ir {
 
-Discard::Discard(const type::Type* ty) : Base(ty) {
-    if (ty) {
-        TINT_ASSERT(IR, ty->Is<type::Void>());
-    }
-}
+Discard::Discard() = default;
 
 Discard::~Discard() = default;
 
diff --git a/src/tint/ir/discard.h b/src/tint/ir/discard.h
index 24b9c59..e87474c 100644
--- a/src/tint/ir/discard.h
+++ b/src/tint/ir/discard.h
@@ -25,8 +25,7 @@
 class Discard : public utils::Castable<Discard, Call> {
   public:
     /// Constructor
-    /// @param ty the type of the discard, must be Void type.
-    explicit Discard(const type::Type* ty);
+    Discard();
     ~Discard() override;
 };
 
diff --git a/src/tint/ir/discard_test.cc b/src/tint/ir/discard_test.cc
index 91e1ef1..d6954de 100644
--- a/src/tint/ir/discard_test.cc
+++ b/src/tint/ir/discard_test.cc
@@ -27,17 +27,11 @@
     ASSERT_TRUE(inst->Is<ir::Discard>());
 }
 
-TEST_F(IR_DiscardTest, Fail_NullType) {
-    EXPECT_FATAL_FAILURE({ Discard d(nullptr); }, "");
-}
+TEST_F(IR_DiscardTest, Result) {
+    auto* inst = b.Discard();
 
-TEST_F(IR_DiscardTest, Fail_NonVoidType) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Discard d(mod.Types().i32());
-        },
-        "");
+    EXPECT_FALSE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
 }
 
 }  // namespace
diff --git a/src/tint/ir/exit.cc b/src/tint/ir/exit.cc
new file mode 100644
index 0000000..fd39edd
--- /dev/null
+++ b/src/tint/ir/exit.cc
@@ -0,0 +1,43 @@
+// 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/exit.h"
+
+#include "src/tint/ir/control_instruction.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Exit);
+
+namespace tint::ir {
+
+Exit::~Exit() = default;
+
+void Exit::Destroy() {
+    SetControlInstruction(nullptr);
+    Base::Destroy();
+}
+
+void Exit::SetControlInstruction(ir::ControlInstruction* ctrl_inst) {
+    if (ctrl_inst_ == ctrl_inst) {
+        return;
+    }
+    if (ctrl_inst_) {
+        ctrl_inst_->RemoveExit(this);
+    }
+    ctrl_inst_ = ctrl_inst;
+    if (ctrl_inst_) {
+        ctrl_inst_->AddExit(this);
+    }
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/exit.h b/src/tint/ir/exit.h
new file mode 100644
index 0000000..f9734fb
--- /dev/null
+++ b/src/tint/ir/exit.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_IR_EXIT_H_
+#define SRC_TINT_IR_EXIT_H_
+
+#include "src/tint/ir/branch.h"
+
+// Forward declarations
+namespace tint::ir {
+class ControlInstruction;
+}  // namespace tint::ir
+
+namespace tint::ir {
+
+/// The base class for all exit terminators.
+class Exit : public utils::Castable<Exit, Branch> {
+  public:
+    ~Exit() override;
+
+    /// @copydoc Value::Destroy
+    void Destroy() override;
+
+  protected:
+    /// @return the control instruction that this exit is associated with
+    ir::ControlInstruction* ControlInstruction() { return ctrl_inst_; }
+
+    /// Sets control instruction that this exit is associated with
+    /// @param ctrl_inst the new ControlInstruction that this exit is associated with
+    void SetControlInstruction(ir::ControlInstruction* ctrl_inst);
+
+  private:
+    ir::ControlInstruction* ctrl_inst_ = nullptr;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_EXIT_H_
diff --git a/src/tint/ir/exit_if.cc b/src/tint/ir/exit_if.cc
index 391c786..7c36a05 100644
--- a/src/tint/ir/exit_if.cc
+++ b/src/tint/ir/exit_if.cc
@@ -23,15 +23,19 @@
 
 namespace tint::ir {
 
-ExitIf::ExitIf(ir::If* i, utils::VectorRef<Value*> args /* = utils::Empty */) : if_(i) {
-    TINT_ASSERT(IR, if_);
-
-    if (if_) {
-        if_->Merge()->AddInboundSiblingBranch(this);
-    }
-    AddOperands(std::move(args));
+ExitIf::ExitIf(ir::If* i, utils::VectorRef<Value*> args) {
+    SetIf(i);
+    AddOperands(ExitIf::kArgsOperandOffset, std::move(args));
 }
 
 ExitIf::~ExitIf() = default;
 
+void ExitIf::SetIf(ir::If* i) {
+    SetControlInstruction(i);
+}
+
+ir::If* ExitIf::If() {
+    return static_cast<ir::If*>(ControlInstruction());
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_if.h b/src/tint/ir/exit_if.h
index 5ddaabf..e3cf9e7 100644
--- a/src/tint/ir/exit_if.h
+++ b/src/tint/ir/exit_if.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_IR_EXIT_IF_H_
 #define SRC_TINT_IR_EXIT_IF_H_
 
-#include "src/tint/ir/branch.h"
+#include "src/tint/ir/exit.h"
 #include "src/tint/utils/castable.h"
 
 // Forward declarations
@@ -26,19 +26,23 @@
 namespace tint::ir {
 
 /// A exit if instruction.
-class ExitIf : public utils::Castable<ExitIf, Branch> {
+class ExitIf : public utils::Castable<ExitIf, Exit> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
     /// @param i the if being exited
     /// @param args the branch arguments
     explicit ExitIf(ir::If* i, utils::VectorRef<Value*> args = utils::Empty);
     ~ExitIf() override;
 
-    /// @returns the if being exited
-    ir::If* If() { return if_; }
+    /// Re-associates the exit with the given if instruction
+    /// @param i the new If to exit from
+    void SetIf(ir::If* i);
 
-  private:
-    ir::If* if_ = nullptr;
+    /// @returns the if being exited
+    ir::If* If();
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_if_test.cc b/src/tint/ir/exit_if_test.cc
index f496d6c..b37b95e 100644
--- a/src/tint/ir/exit_if_test.cc
+++ b/src/tint/ir/exit_if_test.cc
@@ -15,7 +15,6 @@
 #include "src/tint/ir/exit_if.h"
 
 #include "gmock/gmock.h"
-#include "gtest/gtest-spi.h"
 #include "src/tint/ir/ir_test_helper.h"
 
 namespace tint::ir {
@@ -32,26 +31,26 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
+    EXPECT_EQ(if_->Result(), nullptr);
 }
 
-TEST_F(IR_ExitIfTest, Fail_NullIf) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitIf(nullptr);
-        },
-        "");
+TEST_F(IR_ExitIfTest, Result) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* if_ = b.If(true);
+    auto* e = b.ExitIf(if_, arg1, arg2);
+
+    EXPECT_FALSE(e->HasResults());
+    EXPECT_FALSE(e->HasMultiResults());
 }
 
-TEST_F(IR_ExitIfTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitIf(b.If(false), nullptr);
-        },
-        "");
+TEST_F(IR_ExitIfTest, Destroy) {
+    auto* if_ = b.If(true);
+    auto* exit = b.ExitIf(if_);
+    EXPECT_THAT(if_->Exits(), testing::UnorderedElementsAre(exit));
+    exit->Destroy();
+    EXPECT_TRUE(if_->Exits().IsEmpty());
+    EXPECT_FALSE(exit->Alive());
 }
 
 }  // namespace
diff --git a/src/tint/ir/exit_loop.cc b/src/tint/ir/exit_loop.cc
index 865b25c..39a59e1 100644
--- a/src/tint/ir/exit_loop.cc
+++ b/src/tint/ir/exit_loop.cc
@@ -24,16 +24,19 @@
 
 namespace tint::ir {
 
-ExitLoop::ExitLoop(ir::Loop* loop, utils::VectorRef<Value*> args /* = utils::Empty */)
-    : loop_(loop) {
-    TINT_ASSERT(IR, loop_);
-
-    if (loop_) {
-        loop_->Merge()->AddInboundSiblingBranch(this);
-    }
-    AddOperands(std::move(args));
+ExitLoop::ExitLoop(ir::Loop* loop, utils::VectorRef<Value*> args /* = utils::Empty */) {
+    SetLoop(loop);
+    AddOperands(ExitLoop::kArgsOperandOffset, std::move(args));
 }
 
 ExitLoop::~ExitLoop() = default;
 
+void ExitLoop::SetLoop(ir::Loop* l) {
+    SetControlInstruction(l);
+}
+
+ir::Loop* ExitLoop::Loop() {
+    return static_cast<ir::Loop*>(ControlInstruction());
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_loop.h b/src/tint/ir/exit_loop.h
index 2e7f2ea..253655e 100644
--- a/src/tint/ir/exit_loop.h
+++ b/src/tint/ir/exit_loop.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_IR_EXIT_LOOP_H_
 #define SRC_TINT_IR_EXIT_LOOP_H_
 
-#include "src/tint/ir/branch.h"
+#include "src/tint/ir/exit.h"
 #include "src/tint/utils/castable.h"
 
 // Forward declarations
@@ -26,19 +26,23 @@
 namespace tint::ir {
 
 /// A exit loop instruction.
-class ExitLoop : public utils::Castable<ExitLoop, Branch> {
+class ExitLoop : public utils::Castable<ExitLoop, Exit> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
     /// @param loop the loop being exited
     /// @param args the branch arguments
     explicit ExitLoop(ir::Loop* loop, utils::VectorRef<Value*> args = utils::Empty);
     ~ExitLoop() override;
 
-    /// @returns the loop being exited
-    ir::Loop* Loop() { return loop_; }
+    /// Re-associates the exit with the given loop instruction
+    /// @param l the new loop to exit from
+    void SetLoop(ir::Loop* l);
 
-  private:
-    ir::Loop* loop_ = nullptr;
+    /// @returns the loop being exited
+    ir::Loop* Loop();
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_loop_test.cc b/src/tint/ir/exit_loop_test.cc
index 2b8b3de..5a69d0f 100644
--- a/src/tint/ir/exit_loop_test.cc
+++ b/src/tint/ir/exit_loop_test.cc
@@ -15,7 +15,6 @@
 #include "src/tint/ir/exit_loop.h"
 
 #include "gmock/gmock.h"
-#include "gtest/gtest-spi.h"
 #include "src/tint/ir/ir_test_helper.h"
 
 namespace tint::ir {
@@ -32,26 +31,16 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
+    EXPECT_EQ(loop->Result(), nullptr);
 }
 
-TEST_F(IR_ExitLoopTest, Fail_NullLoop) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitLoop(nullptr);
-        },
-        "");
-}
-
-TEST_F(IR_ExitLoopTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitLoop(b.Loop(), nullptr);
-        },
-        "");
+TEST_F(IR_ExitLoopTest, Destroy) {
+    auto* loop = b.Loop();
+    auto* exit = b.ExitLoop(loop);
+    EXPECT_THAT(loop->Exits(), testing::UnorderedElementsAre(exit));
+    exit->Destroy();
+    EXPECT_TRUE(loop->Exits().IsEmpty());
+    EXPECT_FALSE(exit->Alive());
 }
 
 }  // namespace
diff --git a/src/tint/ir/exit_switch.cc b/src/tint/ir/exit_switch.cc
index 3372d6d..d56c310 100644
--- a/src/tint/ir/exit_switch.cc
+++ b/src/tint/ir/exit_switch.cc
@@ -23,16 +23,19 @@
 
 namespace tint::ir {
 
-ExitSwitch::ExitSwitch(ir::Switch* sw, utils::VectorRef<Value*> args /* = utils::Empty */)
-    : switch_(sw) {
-    TINT_ASSERT(IR, switch_);
-
-    if (switch_) {
-        switch_->Merge()->AddInboundSiblingBranch(this);
-    }
-    AddOperands(std::move(args));
+ExitSwitch::ExitSwitch(ir::Switch* sw, utils::VectorRef<Value*> args /* = utils::Empty */) {
+    SetSwitch(sw);
+    AddOperands(ExitSwitch::kArgsOperandOffset, std::move(args));
 }
 
 ExitSwitch::~ExitSwitch() = default;
 
+void ExitSwitch::SetSwitch(ir::Switch* s) {
+    SetControlInstruction(s);
+}
+
+ir::Switch* ExitSwitch::Switch() {
+    return static_cast<ir::Switch*>(ControlInstruction());
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_switch.h b/src/tint/ir/exit_switch.h
index 706ac69..9567a59 100644
--- a/src/tint/ir/exit_switch.h
+++ b/src/tint/ir/exit_switch.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_IR_EXIT_SWITCH_H_
 #define SRC_TINT_IR_EXIT_SWITCH_H_
 
-#include "src/tint/ir/branch.h"
+#include "src/tint/ir/exit.h"
 #include "src/tint/utils/castable.h"
 
 // Forward declarations
@@ -26,19 +26,23 @@
 namespace tint::ir {
 
 /// A exit switch instruction.
-class ExitSwitch : public utils::Castable<ExitSwitch, Branch> {
+class ExitSwitch : public utils::Castable<ExitSwitch, Exit> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
     /// @param sw the switch being exited
     /// @param args the branch arguments
     explicit ExitSwitch(ir::Switch* sw, utils::VectorRef<Value*> args = utils::Empty);
     ~ExitSwitch() override;
 
-    /// @returns the switch being exited
-    ir::Switch* Switch() { return switch_; }
+    /// Re-associates the exit with the given switch instruction
+    /// @param s the new switch to exit from
+    void SetSwitch(ir::Switch* s);
 
-  private:
-    ir::Switch* switch_ = nullptr;
+    /// @returns the switch being exited
+    ir::Switch* Switch();
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/exit_switch_test.cc b/src/tint/ir/exit_switch_test.cc
index 85cb228..3ab84df 100644
--- a/src/tint/ir/exit_switch_test.cc
+++ b/src/tint/ir/exit_switch_test.cc
@@ -15,7 +15,6 @@
 #include "src/tint/ir/exit_switch.h"
 
 #include "gmock/gmock.h"
-#include "gtest/gtest-spi.h"
 #include "src/tint/ir/ir_test_helper.h"
 
 namespace tint::ir {
@@ -32,26 +31,26 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
+    EXPECT_EQ(switch_->Result(), nullptr);
 }
 
-TEST_F(IR_ExitSwitchTest, Fail_NullSwitch) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitSwitch(nullptr);
-        },
-        "");
+TEST_F(IR_ExitSwitchTest, Result) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* switch_ = b.Switch(true);
+    auto* e = b.ExitSwitch(switch_, arg1, arg2);
+
+    EXPECT_FALSE(e->HasResults());
+    EXPECT_FALSE(e->HasMultiResults());
 }
 
-TEST_F(IR_ExitSwitchTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.ExitSwitch(b.Switch(false), nullptr);
-        },
-        "");
+TEST_F(IR_ExitSwitchTest, Destroy) {
+    auto* swch = b.Switch(1_i);
+    auto* exit = b.ExitSwitch(swch);
+    EXPECT_THAT(swch->Exits(), testing::UnorderedElementsAre(exit));
+    exit->Destroy();
+    EXPECT_TRUE(swch->Exits().IsEmpty());
+    EXPECT_FALSE(exit->Alive());
 }
 
 }  // namespace
diff --git a/src/tint/ir/from_program.cc b/src/tint/ir/from_program.cc
index 8ffa513..c831d83 100644
--- a/src/tint/ir/from_program.cc
+++ b/src/tint/ir/from_program.cc
@@ -109,10 +109,6 @@
 
 using ResultType = utils::Result<Module, diag::List>;
 
-bool IsConnected(MultiInBlock* b) {
-    return b->InboundSiblingBranches().Length() > 0;
-}
-
 /// Impl is the private-implementation of FromProgram().
 class Impl {
   public:
@@ -186,7 +182,7 @@
         current_block_ = nullptr;
     }
 
-    Branch* FindEnclosingControl(ControlFlags flags) {
+    Instruction* FindEnclosingControl(ControlFlags flags) {
         for (auto it = control_stack_.rbegin(); it != control_stack_.rend(); ++it) {
             if ((*it)->Is<Loop>()) {
                 return *it;
@@ -421,7 +417,7 @@
         }
         ir_func->SetParams(params);
 
-        current_block_ = ir_func->StartTarget();
+        TINT_SCOPED_ASSIGNMENT(current_block_, ir_func->StartTarget());
         EmitBlock(ast_func->body);
 
         // If the branch target has already been set then a `return` was called. Only set in
@@ -439,10 +435,9 @@
         for (auto* s : stmts) {
             EmitStatement(s);
 
-            // If the current flow block has a branch target then the rest of the statements in
-            // this block are dead code. Skip them.
-            if (!NeedBranch()) {
-                break;
+            if (auto* sem = program_->Sem().Get(s);
+                sem && !sem->Behaviors().Contains(sem::Behavior::kNext)) {
+                break;  // Unreachable statement.
             }
         }
     }
@@ -508,7 +503,7 @@
         auto* lhs_value = builder_.Load(lhs.Get());
         current_block_->Append(lhs_value);
 
-        auto* ty = lhs_value->Type();
+        auto* ty = lhs_value->Result()->Type();
 
         auto* rhs =
             ty->is_signed_integer_scalar() ? builder_.Constant(1_i) : builder_.Constant(1_u);
@@ -540,7 +535,7 @@
         auto* lhs_value = builder_.Load(lhs.Get());
         current_block_->Append(lhs_value);
 
-        auto* ty = lhs_value->Type();
+        auto* ty = lhs_value->Result()->Type();
 
         Binary* inst = nullptr;
         switch (stmt->op) {
@@ -616,75 +611,59 @@
         {
             ControlStackScope scope(this, if_inst);
 
-            current_block_ = if_inst->True();
-            EmitBlock(stmt->body);
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->True());
+                EmitBlock(stmt->body);
 
-            // If the true branch did not execute control flow, then go to the Merge().target
-            if (NeedBranch()) {
-                SetBranch(builder_.ExitIf(if_inst));
+                // If the true block did not branch, then emit an exit_if
+                if (NeedBranch()) {
+                    SetBranch(builder_.ExitIf(if_inst));
+                }
             }
 
-            current_block_ = if_inst->False();
             if (stmt->else_statement) {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->False());
                 EmitStatement(stmt->else_statement);
-            }
 
-            // If the false branch did not execute control flow, then go to the Merge().target
-            if (NeedBranch()) {
-                SetBranch(builder_.ExitIf(if_inst));
+                // If the false block did not branch, then emit an exit_if
+                if (NeedBranch()) {
+                    SetBranch(builder_.ExitIf(if_inst));
+                }
             }
         }
-        current_block_ = nullptr;
-
-        // If both branches went somewhere, then they both returned, continued or broke. So,
-        // there is no need for the if merge-block and there is nothing to branch to the merge
-        // block anyway.
-        if (IsConnected(if_inst->Merge())) {
-            current_block_ = if_inst->Merge();
-        }
     }
 
     void EmitLoop(const ast::LoopStatement* stmt) {
         auto* loop_inst = builder_.Loop();
         current_block_->Append(loop_inst);
 
-        {
-            ControlStackScope scope(this, loop_inst);
-            current_block_ = loop_inst->Body();
+        ControlStackScope scope(this, loop_inst);
 
-            // The loop doesn't use EmitBlock because it needs the scope stack to not get popped
-            // until after the continuing block.
-            scopes_.Push();
-            TINT_DEFER(scopes_.Pop());
+        // The loop doesn't use EmitBlock because it needs the scope stack to not get popped until
+        // after the continuing block.
+        scopes_.Push();
+        TINT_DEFER(scopes_.Pop());
+
+        {
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Body());
+
             EmitStatements(stmt->body->statements);
 
             // The current block didn't `break`, `return` or `continue`, go to the continuing block.
             if (NeedBranch()) {
                 SetBranch(builder_.Continue(loop_inst));
             }
-
-            if (IsConnected(loop_inst->Continuing())) {
-                // Note, even if there is no continuing block, we may have branched into the
-                // continue so we have to set the current block and then emit the branch if needed
-                // below otherwise empty continuing blocks will fail to branch back to the start
-                // block.
-                current_block_ = loop_inst->Continuing();
-                if (stmt->continuing) {
-                    EmitBlock(stmt->continuing);
-                }
-                // Branch back to the start node if the continue target didn't branch out already
-                if (NeedBranch()) {
-                    SetBranch(builder_.NextIteration(loop_inst));
-                }
-            }
         }
 
-        // 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() block.
-        current_block_ = loop_inst->Merge();
-        if (!IsConnected(loop_inst->Merge())) {
-            current_block_ = nullptr;
+        {
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Continuing());
+            if (stmt->continuing) {
+                EmitBlock(stmt->continuing);
+            }
+            // Branch back to the start block if the continue target didn't branch out already
+            if (NeedBranch()) {
+                SetBranch(builder_.NextIteration(loop_inst));
+            }
         }
     }
 
@@ -692,14 +671,16 @@
         auto* loop_inst = builder_.Loop();
         current_block_->Append(loop_inst);
 
+        ControlStackScope scope(this, loop_inst);
+
         // Continue is always empty, just go back to the start
-        current_block_ = loop_inst->Continuing();
-        SetBranch(builder_.NextIteration(loop_inst));
+        {
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Continuing());
+            SetBranch(builder_.NextIteration(loop_inst));
+        }
 
         {
-            ControlStackScope scope(this, loop_inst);
-
-            current_block_ = loop_inst->Body();
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Body());
 
             // Emit the while condition into the Start().target of the loop
             auto reg = EmitExpression(stmt->condition);
@@ -711,22 +692,20 @@
             auto* if_inst = builder_.If(reg.Get());
             current_block_->Append(if_inst);
 
-            current_block_ = if_inst->True();
-            SetBranch(builder_.ExitIf(if_inst));
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->True());
+                SetBranch(builder_.ExitIf(if_inst));
+            }
 
-            current_block_ = if_inst->False();
-            SetBranch(builder_.ExitLoop(loop_inst));
-
-            current_block_ = if_inst->Merge();
-            EmitBlock(stmt->body);
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->False());
+                SetBranch(builder_.ExitLoop(loop_inst));
+            }
 
             if (NeedBranch()) {
                 SetBranch(builder_.Continue(loop_inst));
             }
         }
-        // The while loop always has a path to the Merge().target as the break statement comes
-        // before anything inside the loop.
-        current_block_ = loop_inst->Merge();
     }
 
     void EmitForLoop(const ast::ForLoopStatement* stmt) {
@@ -737,52 +716,53 @@
         scopes_.Push();
         TINT_DEFER(scopes_.Pop());
 
-        {
-            ControlStackScope scope(this, loop_inst);
+        ControlStackScope scope(this, loop_inst);
 
-            if (stmt->initializer) {
-                // Emit the for initializer before branching to the body
-                current_block_ = loop_inst->Initializer();
-                EmitStatement(stmt->initializer);
-                SetBranch(builder_.NextIteration(loop_inst));
-            }
+        if (stmt->initializer) {
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Initializer());
 
-            current_block_ = loop_inst->Body();
-            if (stmt->condition) {
-                // Emit the condition into the target target of the loop
-                auto reg = EmitExpression(stmt->condition);
-                if (!reg) {
-                    return;
-                }
+            // Emit the for initializer before branching to the loop body
+            EmitStatement(stmt->initializer);
 
-                // Create an `if (cond) {} else {break;}` control flow
-                auto* if_inst = builder_.If(reg.Get());
-                current_block_->Append(if_inst);
-
-                current_block_ = if_inst->True();
-                SetBranch(builder_.ExitIf(if_inst));
-
-                current_block_ = if_inst->False();
-                SetBranch(builder_.ExitLoop(loop_inst));
-
-                current_block_ = if_inst->Merge();
-            }
-
-            EmitBlock(stmt->body);
             if (NeedBranch()) {
-                SetBranch(builder_.Continue(loop_inst));
-            }
-
-            if (stmt->continuing) {
-                current_block_ = loop_inst->Continuing();
-                EmitStatement(stmt->continuing);
                 SetBranch(builder_.NextIteration(loop_inst));
             }
         }
 
-        // The while loop always has a path to the Merge().target as the break statement comes
-        // before anything inside the loop.
-        current_block_ = loop_inst->Merge();
+        TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Body());
+
+        if (stmt->condition) {
+            // Emit the condition into the target target of the loop body
+            auto reg = EmitExpression(stmt->condition);
+            if (!reg) {
+                return;
+            }
+
+            // Create an `if (cond) {} else {break;}` control flow
+            auto* if_inst = builder_.If(reg.Get());
+            current_block_->Append(if_inst);
+
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->True());
+                SetBranch(builder_.ExitIf(if_inst));
+            }
+
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->False());
+                SetBranch(builder_.ExitLoop(loop_inst));
+            }
+        }
+
+        EmitBlock(stmt->body);
+        if (NeedBranch()) {
+            SetBranch(builder_.Continue(loop_inst));
+        }
+
+        if (stmt->continuing) {
+            TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Continuing());
+            EmitStatement(stmt->continuing);
+            SetBranch(builder_.NextIteration(loop_inst));
+        }
     }
 
     void EmitSwitch(const ast::SwitchStatement* stmt) {
@@ -794,32 +774,25 @@
         auto* switch_inst = builder_.Switch(reg.Get());
         current_block_->Append(switch_inst);
 
-        {
-            ControlStackScope scope(this, switch_inst);
+        ControlStackScope scope(this, switch_inst);
 
-            const auto* sem = program_->Sem().Get(stmt);
-            for (const auto* c : sem->Cases()) {
-                utils::Vector<Switch::CaseSelector, 4> selectors;
-                for (const auto* selector : c->Selectors()) {
-                    if (selector->IsDefault()) {
-                        selectors.Push({nullptr});
-                    } else {
-                        selectors.Push({builder_.Constant(selector->Value()->Clone(clone_ctx_))});
-                    }
-                }
-
-                current_block_ = builder_.Case(switch_inst, selectors);
-                EmitBlock(c->Body()->Declaration());
-
-                if (NeedBranch()) {
-                    SetBranch(builder_.ExitSwitch(switch_inst));
+        const auto* sem = program_->Sem().Get(stmt);
+        for (const auto* c : sem->Cases()) {
+            utils::Vector<Switch::CaseSelector, 4> selectors;
+            for (const auto* selector : c->Selectors()) {
+                if (selector->IsDefault()) {
+                    selectors.Push({nullptr});
+                } else {
+                    selectors.Push({builder_.Constant(selector->Value()->Clone(clone_ctx_))});
                 }
             }
-        }
-        current_block_ = nullptr;
 
-        if (IsConnected(switch_inst->Merge())) {
-            current_block_ = switch_inst->Merge();
+            TINT_SCOPED_ASSIGNMENT(current_block_, builder_.Case(switch_inst, selectors));
+            EmitBlock(c->Body()->Declaration());
+
+            if (NeedBranch()) {
+                SetBranch(builder_.ExitSwitch(switch_inst));
+            }
         }
     }
 
@@ -885,7 +858,7 @@
 
     struct AccessorInfo {
         Value* object = nullptr;
-        Instruction* result = nullptr;
+        Value* result = nullptr;
         const type::Type* result_type = nullptr;
         utils::Vector<Value*, 1> indices;
     };
@@ -944,7 +917,7 @@
         return info.result;
     }
 
-    Instruction* GenerateAccess(const AccessorInfo& info) {
+    Value* GenerateAccess(const AccessorInfo& info) {
         // The access result type should match the source result type. If the source is a pointer,
         // we generate a pointer.
         const type::Type* ty = nullptr;
@@ -957,7 +930,7 @@
 
         auto* a = builder_.Access(ty, info.object, info.indices);
         current_block_->Append(a);
-        return a;
+        return a->Result();
     }
 
     bool GenerateIndexAccessor(const ast::IndexAccessorExpression* expr, AccessorInfo& info) {
@@ -1005,14 +978,15 @@
                     if (auto* ptr = info.object->Type()->As<type::Pointer>()) {
                         auto* load = builder_.Load(info.object);
                         info.result_type = ptr->StoreType();
-                        info.object = load;
+                        info.object = load->Result();
                         current_block_->Append(load);
                     }
                 }
 
-                info.result = builder_.Swizzle(swizzle->Type()->Clone(clone_ctx_.type_ctx),
-                                               info.object, std::move(indices));
-                current_block_->Append(info.result);
+                auto* val = builder_.Swizzle(swizzle->Type()->Clone(clone_ctx_.type_ctx),
+                                             info.object, std::move(indices));
+                current_block_->Append(val);
+                info.result = val->Result();
 
                 info.object = info.result;
                 info.result_type = result_type;
@@ -1065,7 +1039,7 @@
         if (result && sem->Is<sem::Load>()) {
             auto* load = builder_.Load(result.Get());
             current_block_->Append(load);
-            return load;
+            return load->Result();
         }
         return result;
     }
@@ -1097,7 +1071,7 @@
                 }
 
                 // Store the declaration so we can get the instruction to store too
-                scopes_.Set(v->name->symbol, val);
+                scopes_.Set(v->name->symbol, val->Result());
 
                 // Record the original name of the var
                 builder_.ir.SetName(val, v->name->symbol.Name());
@@ -1162,10 +1136,10 @@
         }
 
         current_block_->Append(inst);
-        return inst;
+        return inst->Result();
     }
 
-    // A short-circut needs special treatment. The short-circuit is decomposed into the relevant
+    // A short-circuit needs special treatment. The short-circuit is decomposed into the relevant
     // if statements and declarations.
     utils::Result<Value*> EmitShortCircuit(const ast::BinaryExpression* expr) {
         switch (expr->op) {
@@ -1174,7 +1148,7 @@
                 break;
             default:
                 TINT_ICE(IR, diagnostics_)
-                    << "invalid operation type for short-circut decomposition";
+                    << "invalid operation type for short-circuit decomposition";
                 return utils::Failure;
         }
 
@@ -1187,44 +1161,50 @@
         auto* if_inst = builder_.If(lhs.Get());
         current_block_->Append(if_inst);
 
-        auto* result = builder_.BlockParam(builder_.ir.Types().bool_());
-        if_inst->Merge()->SetParams({result});
+        auto* result = builder_.InstructionResult(builder_.ir.Types().bool_());
+        if_inst->SetResults(result);
 
-        utils::Result<Value*> rhs;
-        {
-            ControlStackScope scope(this, if_inst);
+        ControlStackScope scope(this, if_inst);
 
-            utils::Vector<Value*, 1> alt_args;
-            alt_args.Push(lhs.Get());
-
-            // If this is an `&&` then we only evaluate the RHS expression in the true block.
-            // If this is an `||` then we only evaluate the RHS expression in the false block.
-            if (expr->op == ast::BinaryOp::kLogicalAnd) {
-                // If the lhs is false, then that is the result we want to pass to the merge
-                // block as our argument
-                current_block_ = if_inst->False();
-                SetBranch(builder_.ExitIf(if_inst, std::move(alt_args)));
-
-                current_block_ = if_inst->True();
-            } else {
-                // If the lhs is true, then that is the result we want to pass to the merge
-                // block as our argument
-                current_block_ = if_inst->True();
-                SetBranch(builder_.ExitIf(if_inst, std::move(alt_args)));
-
-                current_block_ = if_inst->False();
+        if (expr->op == ast::BinaryOp::kLogicalAnd) {
+            //   res = lhs && rhs;
+            //
+            // transform into:
+            //
+            //   if (lhs) {
+            //     res = rhs;
+            //   } else {
+            //     res = lhs;
+            //   }
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->True());
+                auto rhs = EmitExpression(expr->rhs);
+                SetBranch(builder_.ExitIf(if_inst, utils::Vector{rhs.Get()}));
             }
-
-            rhs = EmitExpression(expr->rhs);
-            if (!rhs) {
-                return utils::Failure;
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->False());
+                SetBranch(builder_.ExitIf(if_inst, utils::Vector{lhs.Get()}));
             }
-            utils::Vector<Value*, 1> args;
-            args.Push(rhs.Get());
-
-            SetBranch(builder_.ExitIf(if_inst, std::move(args)));
+        } else {
+            //   res = lhs || rhs;
+            //
+            // transform into:
+            //
+            //   if (lhs) {
+            //     res = lhs;
+            //   } else {
+            //     res = rhs;
+            //   }
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->True());
+                SetBranch(builder_.ExitIf(if_inst, utils::Vector{lhs.Get()}));
+            }
+            {
+                TINT_SCOPED_ASSIGNMENT(current_block_, if_inst->False());
+                auto rhs = EmitExpression(expr->rhs);
+                SetBranch(builder_.ExitIf(if_inst, utils::Vector{rhs.Get()}));
+            }
         }
-        current_block_ = if_inst->Merge();
 
         return result;
     }
@@ -1307,7 +1287,7 @@
         }
 
         current_block_->Append(inst);
-        return inst;
+        return inst->Result();
     }
 
     utils::Result<Value*> EmitBitcast(const ast::BitcastExpression* expr) {
@@ -1321,7 +1301,7 @@
         auto* inst = builder_.Bitcast(ty, val.Get());
 
         current_block_->Append(inst);
-        return inst;
+        return inst->Result();
     }
 
     void EmitCall(const ast::CallStatement* stmt) { (void)EmitCall(stmt->expr); }
@@ -1384,7 +1364,7 @@
             return utils::Failure;
         }
         current_block_->Append(inst);
-        return inst;
+        return inst->Result();
     }
 
     utils::Result<Value*> EmitLiteral(const ast::LiteralExpression* lit) {
diff --git a/src/tint/ir/from_program_accessor_test.cc b/src/tint/ir/from_program_accessor_test.cc
index d8e6601..3628f10 100644
--- a/src/tint/ir/from_program_accessor_test.cc
+++ b/src/tint/ir/from_program_accessor_test.cc
@@ -24,7 +24,8 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_FromProgramAccessorTest = ProgramTestHelper;
 
@@ -348,7 +349,7 @@
     // let a: mat3x4<f32> = mat3x4<u32>()
     // let b = a[2][3]
 
-    auto* a = Let("a", ty.mat3x4<f32>(), mat3x4<f32>());
+    auto* a = Let("a", ty.mat3x4<f32>(), Call<mat3x4<f32>>());
     auto* expr = Decl(Let("b", IndexAccessor(IndexAccessor(a, 2_u), 3_u)));
     WrapInFunction(Block(utils::Vector{Decl(a), expr}));
 
@@ -448,7 +449,7 @@
                                          Member("a", ty.i32()),
                                          Member("foo", ty.array(ty.Of(inner), 4_u)),
                                      });
-    auto* a = Let("a", ty.array(ty.Of(outer), 4_u), array(ty.Of(outer), 4_u));
+    auto* a = Let("a", ty.array(ty.Of(outer), 4_u), Call(ty.array(ty.Of(outer), 4_u)));
     auto* expr = Decl(Let(
         "b",
         MemberAccessor(IndexAccessor(MemberAccessor(IndexAccessor(a, 0_u), "foo"), 1_u), "bar")));
diff --git a/src/tint/ir/from_program_binary_test.cc b/src/tint/ir/from_program_binary_test.cc
index a3bcb1e..86570e4 100644
--- a/src/tint/ir/from_program_binary_test.cc
+++ b/src/tint/ir/from_program_binary_test.cc
@@ -451,8 +451,9 @@
 
 TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LogicalAnd) {
     Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
-    auto* expr = If(LogicalAnd(Call("my_func"), false), Block());
-    WrapInFunction(expr);
+    auto* let = Let("logical_and", LogicalAnd(Call("my_func"), false));
+    auto* expr = If(let, Block());
+    WrapInFunction(let, expr);
 
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
@@ -465,37 +466,24 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:bool = call %my_func
-    if %3 [t: %b3, f: %b4, m: %b5]
+    %logical_and:bool = if %3 [t: %b3, f: %b4]
       # True block
       %b3 = block {
-        exit_if %b5 false
+        exit_if false
       }
 
       # False block
       %b4 = block {
-        exit_if %b5 %3
+        exit_if %3
       }
 
-    # Merge block
-    %b5 = block (%4:bool) {
-      if %4:bool [t: %b6, f: %b7, m: %b8]
-        # True block
-        %b6 = block {
-          exit_if %b8
-        }
-
-        # False block
-        %b7 = block {
-          exit_if %b8
-        }
-
-      # Merge block
-      %b8 = block {
-        ret
+    if %logical_and [t: %b5]
+      # True block
+      %b5 = block {
+        exit_if
       }
 
-    }
-
+    ret
   }
 }
 )");
@@ -503,8 +491,9 @@
 
 TEST_F(IR_FromProgramBinaryTest, EmitExpression_Binary_LogicalOr) {
     Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
-    auto* expr = If(LogicalOr(Call("my_func"), true), Block());
-    WrapInFunction(expr);
+    auto* let = Let("logical_or", LogicalOr(Call("my_func"), true));
+    auto* expr = If(let, Block());
+    WrapInFunction(let, expr);
 
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
@@ -517,37 +506,24 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:bool = call %my_func
-    if %3 [t: %b3, f: %b4, m: %b5]
+    %logical_or:bool = if %3 [t: %b3, f: %b4]
       # True block
       %b3 = block {
-        exit_if %b5 %3
+        exit_if %3
       }
 
       # False block
       %b4 = block {
-        exit_if %b5 true
+        exit_if true
       }
 
-    # Merge block
-    %b5 = block (%4:bool) {
-      if %4:bool [t: %b6, f: %b7, m: %b8]
-        # True block
-        %b6 = block {
-          exit_if %b8
-        }
-
-        # False block
-        %b7 = block {
-          exit_if %b8
-        }
-
-      # Merge block
-      %b8 = block {
-        ret
+    if %logical_or [t: %b5]
+      # True block
+      %b5 = block {
+        exit_if
       }
 
-    }
-
+    ret
   }
 }
 )");
@@ -804,27 +780,23 @@
   %b2 = block {
     %3:f32 = call %my_func
     %4:bool = lt %3, 2.0f
-    if %4 [t: %b3, f: %b4, m: %b5]
+    %tint_symbol:bool = if %4 [t: %b3, f: %b4]
       # True block
       %b3 = block {
-        %5:f32 = call %my_func
         %6:f32 = call %my_func
-        %7:f32 = mul 2.29999995231628417969f, %6
-        %8:f32 = div %5, %7
-        %9:bool = gt 2.5f, %8
-        exit_if %b5 %9
+        %7:f32 = call %my_func
+        %8:f32 = mul 2.29999995231628417969f, %7
+        %9:f32 = div %6, %8
+        %10:bool = gt 2.5f, %9
+        exit_if %10
       }
 
       # False block
       %b4 = block {
-        exit_if %b5 %4
+        exit_if %4
       }
 
-    # Merge block
-    %b5 = block (%tint_symbol:bool) {
-      ret
-    }
-
+    ret
   }
 }
 )");
diff --git a/src/tint/ir/from_program_call_test.cc b/src/tint/ir/from_program_call_test.cc
index ce89975..1148f56 100644
--- a/src/tint/ir/from_program_call_test.cc
+++ b/src/tint/ir/from_program_call_test.cc
@@ -21,7 +21,8 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_FromProgramCallTest = ProgramTestHelper;
 
@@ -92,7 +93,7 @@
 
 TEST_F(IR_FromProgramCallTest, EmitExpression_Convert) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
-    auto* expr = Call(ty.f32(), i);
+    auto* expr = Call<f32>(i);
     WrapInFunction(expr);
 
     auto m = Build();
@@ -114,7 +115,7 @@
 }
 
 TEST_F(IR_FromProgramCallTest, EmitExpression_ConstructEmpty) {
-    auto* expr = vec3(ty.f32());
+    auto* expr = Call<vec3<f32>>();
     GlobalVar("i", builtin::AddressSpace::kPrivate, expr);
 
     auto m = Build();
@@ -130,7 +131,7 @@
 
 TEST_F(IR_FromProgramCallTest, EmitExpression_Construct) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
-    auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
+    auto* expr = Call<vec3<f32>>(2_f, 3_f, i);
     WrapInFunction(expr);
 
     auto m = Build();
diff --git a/src/tint/ir/from_program_function_test.cc b/src/tint/ir/from_program_function_test.cc
index c467f28..2fce0e7 100644
--- a/src/tint/ir/from_program_function_test.cc
+++ b/src/tint/ir/from_program_function_test.cc
@@ -21,12 +21,14 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_FromProgramFunctionTest = ProgramTestHelper;
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_Vertex) {
-    Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(0_f, 0_f, 0_f, 0_f))},
+    Func("test", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{Return(Call<vec4<f32>>(0_f, 0_f, 0_f, 0_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
 
@@ -73,8 +75,8 @@
 }
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_Return) {
-    Func("test", utils::Empty, ty.vec3<f32>(), utils::Vector{Return(vec3<f32>(0_f, 0_f, 0_f))},
-         utils::Empty);
+    Func("test", utils::Empty, ty.vec3<f32>(),
+         utils::Vector{Return(Call<vec3<f32>>(0_f, 0_f, 0_f))}, utils::Empty);
 
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
@@ -88,7 +90,8 @@
 }
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnPosition) {
-    Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
+    Func("test", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{Return(Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
 
@@ -104,7 +107,8 @@
 }
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnPositionInvariant) {
-    Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
+    Func("test", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{Return(Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition), Invariant()});
 
@@ -121,7 +125,8 @@
 }
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnLocation) {
-    Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
+    Func("test", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{Return(Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kFragment)}, utils::Vector{Location(1_i)});
 
     auto m = Build();
@@ -137,7 +142,8 @@
 }
 
 TEST_F(IR_FromProgramFunctionTest, EmitFunction_ReturnLocation_Interpolate) {
-    Func("test", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(vec4<f32>(1_f, 2_f, 3_f, 4_f))},
+    Func("test", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{Return(Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f))},
          utils::Vector{Stage(ast::PipelineStage::kFragment)},
          utils::Vector{Location(1_i), Interpolate(builtin::InterpolationType::kLinear,
                                                   builtin::InterpolationSampling::kCentroid)});
diff --git a/src/tint/ir/from_program_test.cc b/src/tint/ir/from_program_test.cc
index 679ec8a..1dd8f24 100644
--- a/src/tint/ir/from_program_test.cc
+++ b/src/tint/ir/from_program_test.cc
@@ -31,10 +31,10 @@
 /// If multiple flow nodes are found with the type T, then an error is raised and the first is
 /// returned.
 template <typename T>
-T* FindSingleValue(Module& mod) {
+T* FindSingleInstruction(Module& mod) {
     T* found = nullptr;
     size_t count = 0;
-    for (auto* node : mod.values.Objects()) {
+    for (auto* node : mod.instructions.Objects()) {
         if (auto* as = node->As<T>()) {
             count++;
             if (!found) {
@@ -43,7 +43,7 @@
         }
     }
     if (count > 1) {
-        ADD_FAILURE() << "FindSingleValue() found " << count << " nodes of type "
+        ADD_FAILURE() << "FindSingleInstruction() found " << count << " nodes of type "
                       << utils::TypeInfo::Of<T>().name;
     }
     return found;
@@ -135,31 +135,24 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* if_ = FindSingleValue<ir::If>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(2u, if_->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    if true [t: %b2, f: %b3, m: %b4]
+    if true [t: %b2, f: %b3]
       # True block
       %b2 = block {
-        exit_if %b4
+        exit_if
       }
 
       # False block
       %b3 = block {
-        exit_if %b4
+        exit_if
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -173,31 +166,19 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* if_ = FindSingleValue<ir::If>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, if_->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    if true [t: %b2, f: %b3, m: %b4]
+    if true [t: %b2]
       # True block
       %b2 = block {
         ret
       }
 
-      # False block
-      %b3 = block {
-        exit_if %b4
-      }
-
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -211,19 +192,16 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* if_ = FindSingleValue<ir::If>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, if_->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    if true [t: %b2, f: %b3, m: %b4]
+    if true [t: %b2, f: %b3]
       # True block
       %b2 = block {
-        exit_if %b4
+        exit_if
       }
 
       # False block
@@ -231,11 +209,7 @@
         ret
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -249,12 +223,9 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* if_ = FindSingleValue<ir::If>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, if_->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
@@ -269,6 +240,7 @@
         ret
       }
 
+    ret
   }
 }
 )");
@@ -287,32 +259,24 @@
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    if true [t: %b2, f: %b3, m: %b4]
+    if true [t: %b2]
       # True block
       %b2 = block {
-        loop [b: %b5, m: %b6]
+        loop [b: %b3, c: %b4]
           # Body block
-          %b5 = block {
-            exit_loop %b6
+          %b3 = block {
+            exit_loop
           }
 
-        # Merge block
-        %b6 = block {
-          exit_if %b4
-        }
+          # Continuing block
+          %b4 = block {
+            next_iteration %b3
+          }
 
+        exit_if
       }
 
-      # False block
-      %b3 = block {
-        exit_if %b4
-      }
-
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -326,28 +290,28 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, m: %b3]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        exit_loop %b3
+        exit_loop
       }
 
-    # Merge block
-    %b3 = block {
-      ret
-    }
+      # Continuing block
+      %b3 = block {
+        next_iteration %b2
+      }
 
+    ret
   }
 }
 )");
@@ -362,36 +326,26 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        if true [t: %b5, f: %b6, m: %b7]
+        if true [t: %b4]
           # True block
-          %b5 = block {
-            exit_loop %b4
+          %b4 = block {
+            exit_loop
           }
 
-          # False block
-          %b6 = block {
-            exit_if %b7
-          }
-
-        # Merge block
-        %b7 = block {
-          continue %b3
-        }
-
+        continue %b3
       }
 
       # Continuing block
@@ -399,11 +353,7 @@
         next_iteration %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -418,18 +368,17 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
         continue %b3
@@ -440,11 +389,7 @@
         break_if true %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -463,7 +408,7 @@
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
         continue %b3
@@ -474,11 +419,7 @@
         break_if true %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -493,13 +434,12 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(0u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -507,22 +447,13 @@
     loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        if true [t: %b4, f: %b5, m: %b6]
+        if true [t: %b4]
           # True block
           %b4 = block {
             ret
           }
 
-          # False block
-          %b5 = block {
-            exit_if %b6
-          }
-
-        # Merge block
-        %b6 = block {
-          continue %b3
-        }
-
+        continue %b3
       }
 
       # Continuing block
@@ -530,6 +461,7 @@
         next_iteration %b2
       }
 
+    ret
   }
 }
 )");
@@ -543,23 +475,28 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(0u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
         ret
       }
 
+      # Continuing block
+      %b3 = block {
+        next_iteration %b2
+      }
+
+    ret
   }
 }
 )");
@@ -582,23 +519,34 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(0u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
         ret
       }
 
+      # Continuing block
+      %b3 = block {
+        break_if true %b2
+      }
+
+    if true [t: %b4]
+      # True block
+      %b4 = block {
+        ret
+      }
+
+    ret
   }
 }
 )");
@@ -613,38 +561,39 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
-    EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(2u, loop->Merge()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, m: %b3]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
         if true [t: %b4, f: %b5]
           # True block
           %b4 = block {
-            exit_loop %b3
+            exit_loop
           }
 
           # False block
           %b5 = block {
-            exit_loop %b3
+            exit_loop
           }
 
+        continue %b3
       }
 
-    # Merge block
-    %b3 = block {
-      ret
-    }
+      # Continuing block
+      %b3 = block {
+        next_iteration %b2
+      }
 
+    ret
   }
 }
 )");
@@ -670,95 +619,61 @@
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        loop [b: %b5, c: %b6, m: %b7]
+        loop [b: %b4, c: %b5]
           # Body block
-          %b5 = block {
-            if true [t: %b8, f: %b9, m: %b10]
+          %b4 = block {
+            if true [t: %b6]
               # True block
-              %b8 = block {
-                exit_loop %b7
+              %b6 = block {
+                exit_loop
               }
 
-              # False block
-              %b9 = block {
-                exit_if %b10
+            if true [t: %b7]
+              # True block
+              %b7 = block {
+                continue %b5
               }
 
-            # Merge block
-            %b10 = block {
-              if true [t: %b11, f: %b12, m: %b13]
-                # True block
-                %b11 = block {
-                  continue %b6
-                }
-
-                # False block
-                %b12 = block {
-                  exit_if %b13
-                }
-
-              # Merge block
-              %b13 = block {
-                continue %b6
-              }
-
-            }
-
+            continue %b5
           }
 
           # Continuing block
-          %b6 = block {
-            loop [b: %b14, m: %b15]
+          %b5 = block {
+            loop [b: %b8, c: %b9]
               # Body block
-              %b14 = block {
-                exit_loop %b15
+              %b8 = block {
+                exit_loop
               }
 
-            # Merge block
-            %b15 = block {
-              loop [b: %b16, c: %b17, m: %b18]
-                # Body block
-                %b16 = block {
-                  continue %b17
-                }
-
-                # Continuing block
-                %b17 = block {
-                  break_if true %b16
-                }
-
-              # Merge block
-              %b18 = block {
-                next_iteration %b5
+              # Continuing block
+              %b9 = block {
+                next_iteration %b8
               }
 
-            }
+            loop [b: %b10, c: %b11]
+              # Body block
+              %b10 = block {
+                continue %b11
+              }
 
+              # Continuing block
+              %b11 = block {
+                break_if true %b10
+              }
+
+            next_iteration %b4
           }
 
-        # Merge block
-        %b7 = block {
-          if true [t: %b19, f: %b20, m: %b21]
-            # True block
-            %b19 = block {
-              exit_loop %b4
-            }
-
-            # False block
-            %b20 = block {
-              exit_if %b21
-            }
-
-          # Merge block
-          %b21 = block {
-            continue %b3
+        if true [t: %b12]
+          # True block
+          %b12 = block {
+            exit_loop
           }
 
-        }
-
+        continue %b3
       }
 
       # Continuing block
@@ -766,11 +681,7 @@
         next_iteration %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -784,36 +695,31 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        if false [t: %b5, f: %b6, m: %b7]
+        if false [t: %b4, f: %b5]
           # True block
-          %b5 = block {
-            exit_if %b7
+          %b4 = block {
+            exit_if
           }
 
           # False block
-          %b6 = block {
-            exit_loop %b4
+          %b5 = block {
+            exit_loop
           }
 
-        # Merge block
-        %b7 = block {
-          continue %b3
-        }
-
+        continue %b3
       }
 
       # Continuing block
@@ -821,11 +727,7 @@
         next_iteration %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -839,36 +741,31 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
-    EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
+    EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3, m: %b4]
+    loop [b: %b2, c: %b3]
       # Body block
       %b2 = block {
-        if true [t: %b5, f: %b6, m: %b7]
+        if true [t: %b4, f: %b5]
           # True block
-          %b5 = block {
-            exit_if %b7
+          %b4 = block {
+            exit_if
           }
 
           # False block
-          %b6 = block {
-            exit_loop %b4
+          %b5 = block {
+            exit_loop
           }
 
-        # Merge block
-        %b7 = block {
-          ret
-        }
-
+        continue %b3
       }
 
       # Continuing block
@@ -876,11 +773,7 @@
         next_iteration %b2
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -907,13 +800,12 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(2u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(2u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m), R"()");
 }
@@ -926,18 +818,17 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [i: %b2, b: %b3, m: %b4]
+    loop [i: %b2, b: %b3]
       # Initializer block
       %b2 = block {
         %i:ptr<function, i32, read_write> = var
@@ -946,14 +837,10 @@
 
       # Body block
       %b3 = block {
-        exit_loop %b4
+        exit_loop
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -967,28 +854,23 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* loop = FindSingleValue<ir::Loop>(m);
+    auto* loop = FindSingleInstruction<ir::Loop>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
     EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Merge()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, m: %b3]
+    loop [b: %b2]
       # Body block
       %b2 = block {
-        exit_loop %b3
+        exit_loop
       }
 
-    # Merge block
-    %b3 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -1005,7 +887,7 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* flow = FindSingleValue<ir::Switch>(m);
+    auto* flow = FindSingleInstruction<ir::Switch>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
@@ -1025,32 +907,26 @@
     ASSERT_EQ(1u, cases[2].selectors.Length());
     EXPECT_TRUE(cases[2].selectors[0].IsDefault());
 
-    EXPECT_EQ(3u, flow->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    switch 1i [c: (0i, %b2), c: (1i, %b3), c: (default, %b4), m: %b5]
+    switch 1i [c: (0i, %b2), c: (1i, %b3), c: (default, %b4)]
       # Case block
       %b2 = block {
-        exit_switch %b5
+        exit_switch
       }
 
       # Case block
       %b3 = block {
-        exit_switch %b5
+        exit_switch
       }
 
       # Case block
       %b4 = block {
-        exit_switch %b5
+        exit_switch
       }
 
-    # Merge block
-    %b5 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -1068,7 +944,7 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* flow = FindSingleValue<ir::Switch>(m);
+    auto* flow = FindSingleInstruction<ir::Switch>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
@@ -1085,22 +961,16 @@
 
     EXPECT_TRUE(cases[0].selectors[2].IsDefault());
 
-    EXPECT_EQ(1u, flow->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    switch 1i [c: (0i 1i default, %b2), m: %b3]
+    switch 1i [c: (0i 1i default, %b2)]
       # Case block
       %b2 = block {
-        exit_switch %b3
+        exit_switch
       }
 
-    # Merge block
-    %b3 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -1114,7 +984,7 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* flow = FindSingleValue<ir::Switch>(m);
+    auto* flow = FindSingleInstruction<ir::Switch>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
@@ -1123,22 +993,16 @@
     ASSERT_EQ(1u, cases[0].selectors.Length());
     EXPECT_TRUE(cases[0].selectors[0].IsDefault());
 
-    EXPECT_EQ(1u, flow->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    switch 1i [c: (default, %b2), m: %b3]
+    switch 1i [c: (default, %b2)]
       # Case block
       %b2 = block {
-        exit_switch %b3
+        exit_switch
       }
 
-    # Merge block
-    %b3 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -1154,7 +1018,7 @@
     ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
     auto m = res.Move();
-    auto* flow = FindSingleValue<ir::Switch>(m);
+    auto* flow = FindSingleInstruction<ir::Switch>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
@@ -1168,28 +1032,23 @@
     ASSERT_EQ(1u, cases[1].selectors.Length());
     EXPECT_TRUE(cases[1].selectors[0].IsDefault());
 
-    EXPECT_EQ(2u, flow->Merge()->InboundSiblingBranches().Length());
     // This is 1 because the if is dead-code eliminated and the return doesn't happen.
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    switch 1i [c: (0i, %b2), c: (default, %b3), m: %b4]
+    switch 1i [c: (0i, %b2), c: (default, %b3)]
       # Case block
       %b2 = block {
-        exit_switch %b4
+        exit_switch
       }
 
       # Case block
       %b3 = block {
-        exit_switch %b4
+        exit_switch
       }
 
-    # Merge block
-    %b4 = block {
-      ret
-    }
-
+    ret
   }
 }
 )");
@@ -1207,7 +1066,7 @@
 
     auto m = res.Move();
 
-    auto* flow = FindSingleValue<ir::Switch>(m);
+    auto* flow = FindSingleInstruction<ir::Switch>(m);
 
     ASSERT_EQ(1u, m.functions.Length());
 
@@ -1221,8 +1080,6 @@
     ASSERT_EQ(1u, cases[1].selectors.Length());
     EXPECT_TRUE(cases[1].selectors[0].IsDefault());
 
-    EXPECT_EQ(0u, flow->Merge()->InboundSiblingBranches().Length());
-
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
@@ -1237,6 +1094,7 @@
         ret
       }
 
+    ret
   }
 }
 )");
diff --git a/src/tint/ir/if.cc b/src/tint/ir/if.cc
index 7f34986..bc2e5b0 100644
--- a/src/tint/ir/if.cc
+++ b/src/tint/ir/if.cc
@@ -20,25 +20,29 @@
 
 namespace tint::ir {
 
-If::If(Value* cond, ir::Block* t, ir::Block* f, ir::MultiInBlock* m)
-    : true_(t), false_(f), merge_(m) {
-    TINT_ASSERT(IR, cond);
+If::If(Value* cond, ir::Block* t, ir::Block* f) : true_(t), false_(f) {
     TINT_ASSERT(IR, true_);
     TINT_ASSERT(IR, false_);
-    TINT_ASSERT(IR, merge_);
 
-    AddOperand(cond);
+    AddOperand(If::kConditionOperandOffset, cond);
+
     if (true_) {
         true_->SetParent(this);
     }
     if (false_) {
         false_->SetParent(this);
     }
-    if (merge_) {
-        merge_->SetParent(this);
-    }
 }
 
 If::~If() = default;
 
+void If::ForeachBlock(const std::function<void(ir::Block*)>& cb) {
+    if (true_) {
+        cb(true_);
+    }
+    if (false_) {
+        cb(false_);
+    }
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index 4434116..6b87463 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -35,11 +35,8 @@
 ///    │  True      │      │  False     │
 ///    | (optional) |      | (optional) |
 ///    └────────────┘      └────────────┘
-///  ExitIf ┃     ┌──────────┐     ┃ ExitIf
-///         ┗━━━━▶│  Merge   │◀━━━━┛
-///               │(optional)│
-///               └──────────┘
-///                    ┃
+///  ExitIf ┃                     ┃ ExitIf
+///         ┗━━━━━━━━━━┳━━━━━━━━━━┛
 ///                    ▼
 ///                   out
 /// ```
@@ -52,12 +49,11 @@
     /// @param cond the if condition
     /// @param t the true block
     /// @param f the false block
-    /// @param m the merge block
-    If(Value* cond, ir::Block* t, ir::Block* f, ir::MultiInBlock* m);
+    If(Value* cond, ir::Block* t, ir::Block* f);
     ~If() override;
 
-    /// @returns the branch arguments
-    utils::Slice<Value* const> Args() override { return utils::Slice<Value*>{}; }
+    /// @copydoc ControlInstruction::ForeachBlock
+    void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
 
     /// @returns the if condition
     Value* Condition() { return operands_[kConditionOperandOffset]; }
@@ -68,13 +64,9 @@
     /// @returns the false branch block
     ir::Block* False() { return false_; }
 
-    /// @returns the merge branch block
-    ir::MultiInBlock* Merge() { return merge_; }
-
   private:
     ir::Block* true_ = nullptr;
     ir::Block* false_ = nullptr;
-    ir::MultiInBlock* merge_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/if_test.cc b/src/tint/ir/if_test.cc
index 60915a6..934a58c 100644
--- a/src/tint/ir/if_test.cc
+++ b/src/tint/ir/if_test.cc
@@ -29,22 +29,18 @@
     EXPECT_THAT(cond->Usages(), testing::UnorderedElementsAre(Usage{if_, 0u}));
 }
 
+TEST_F(IR_IfTest, Result) {
+    auto* if_ = b.If(b.Constant(true));
+
+    EXPECT_FALSE(if_->HasResults());
+    EXPECT_FALSE(if_->HasMultiResults());
+}
+
 TEST_F(IR_IfTest, Parent) {
     auto* cond = b.Constant(true);
     auto* if_ = b.If(cond);
     EXPECT_EQ(if_->True()->Parent(), if_);
     EXPECT_EQ(if_->False()->Parent(), if_);
-    EXPECT_EQ(if_->Merge()->Parent(), if_);
-}
-
-TEST_F(IR_IfTest, Fail_NullCondition) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.If(nullptr);
-        },
-        "");
 }
 
 TEST_F(IR_IfTest, Fail_NullTrueBlock) {
@@ -52,7 +48,7 @@
         {
             Module mod;
             Builder b{mod};
-            If if_(b.Constant(false), nullptr, b.Block(), b.MultiInBlock());
+            If if_(b.Constant(false), nullptr, b.Block());
         },
         "");
 }
@@ -62,17 +58,7 @@
         {
             Module mod;
             Builder b{mod};
-            If if_(b.Constant(false), b.Block(), nullptr, b.MultiInBlock());
-        },
-        "");
-}
-
-TEST_F(IR_IfTest, Fail_NullMultiInBlock) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            If if_(b.Constant(false), b.Block(), b.Block(), nullptr);
+            If if_(b.Constant(false), b.Block(), nullptr);
         },
         "");
 }
diff --git a/src/tint/ir/instruction.cc b/src/tint/ir/instruction.cc
index ea9cf31..e8bc12c 100644
--- a/src/tint/ir/instruction.cc
+++ b/src/tint/ir/instruction.cc
@@ -25,6 +25,18 @@
 
 Instruction::~Instruction() = default;
 
+void Instruction::Destroy() {
+    TINT_ASSERT(IR, Alive());
+    if (Block()) {
+        Remove();
+    }
+    for (auto* result : Results()) {
+        result->SetSource(nullptr);
+        result->Destroy();
+    }
+    alive_ = false;
+}
+
 void Instruction::InsertBefore(Instruction* before) {
     TINT_ASSERT_OR_RETURN(IR, before);
     TINT_ASSERT_OR_RETURN(IR, before->Block() != nullptr);
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 4cf7d70..8145258 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_IR_INSTRUCTION_H_
 #define SRC_TINT_IR_INSTRUCTION_H_
 
+#include "src/tint/ir/instruction_result.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/utils/castable.h"
 
@@ -26,11 +27,38 @@
 namespace tint::ir {
 
 /// An instruction in the IR.
-class Instruction : public utils::Castable<Instruction, Value> {
+class Instruction : public utils::Castable<Instruction> {
   public:
     /// Destructor
     ~Instruction() override;
 
+    /// Set an operand at a given index.
+    /// @param index the operand index
+    /// @param value the value to use
+    virtual void SetOperand(size_t index, ir::Value* value) = 0;
+
+    /// @returns the operands of the instruction
+    virtual utils::VectorRef<ir::Value*> Operands() = 0;
+
+    /// @returns true if the instruction has result values
+    virtual bool HasResults() { return false; }
+    /// @returns true if the instruction has multiple values
+    virtual bool HasMultiResults() { return false; }
+
+    /// @returns the first result. Returns `nullptr` if there are no results, or if ther are
+    /// multi-results
+    virtual InstructionResult* Result() { return nullptr; }
+
+    /// @returns the result values for this instruction
+    virtual utils::VectorRef<InstructionResult*> Results() { return utils::Empty; }
+
+    /// Removes the instruction from the block, and destroys all the result values.
+    /// The result values must not be in use.
+    virtual void Destroy();
+
+    /// @returns true if the Instruction has not been destroyed with Destroy()
+    bool Alive() const { return alive_; }
+
     /// Sets the block that owns this instruction
     /// @param block the new owner block
     void SetBlock(ir::Block* block) { block_ = block; }
@@ -50,10 +78,13 @@
     /// Removes this instruction from the owning block
     void Remove();
 
-    /// Set an operand at a given index.
-    /// @param index the operand index
-    /// @param value the value to use
-    virtual void SetOperand(uint32_t index, ir::Value* value) = 0;
+    /// @param idx the index of the result
+    /// @returns the result with index @p idx, or `nullptr` if there are no results or the index is
+    /// out of bounds.
+    Value* Result(size_t idx) {
+        auto res = Results();
+        return idx < res.Length() ? res[idx] : nullptr;
+    }
 
     /// Pointer to the next instruction in the list
     Instruction* next = nullptr;
@@ -66,6 +97,9 @@
 
     /// The block that owns this instruction
     ir::Block* block_ = nullptr;
+
+  private:
+    bool alive_ = true;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction_result.cc b/src/tint/ir/instruction_result.cc
new file mode 100644
index 0000000..85eaafe
--- /dev/null
+++ b/src/tint/ir/instruction_result.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/instruction_result.h"
+
+#include "src/tint/ir/constant.h"
+#include "src/tint/ir/instruction.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::InstructionResult);
+
+namespace tint::ir {
+
+InstructionResult::InstructionResult(const type::Type* type) : type_(type) {
+    TINT_ASSERT(IR, type_ != nullptr);
+}
+
+InstructionResult::~InstructionResult() = default;
+
+void InstructionResult::Destroy() {
+    TINT_ASSERT(IR, source_ == nullptr);
+    Base::Destroy();
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/instruction_result.h b/src/tint/ir/instruction_result.h
new file mode 100644
index 0000000..0f6192a
--- /dev/null
+++ b/src/tint/ir/instruction_result.h
@@ -0,0 +1,53 @@
+// 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_INSTRUCTION_RESULT_H_
+#define SRC_TINT_IR_INSTRUCTION_RESULT_H_
+
+#include "src/tint/ir/value.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// An instruction result in the IR.
+class InstructionResult : public utils::Castable<InstructionResult, Value> {
+  public:
+    /// Constructor
+    /// @param type the type of the value
+    explicit InstructionResult(const type::Type* type);
+
+    /// Destructor
+    ~InstructionResult() override;
+
+    /// @copydoc Value::Destroy
+    void Destroy() override;
+
+    /// @returns the type of the value
+    const type::Type* Type() override { return type_; }
+
+    /// Sets the source instruction for this value
+    /// @param inst the instruction to set
+    void SetSource(Instruction* inst) { source_ = inst; }
+
+    /// @returns the source instruction, if any
+    Instruction* Source() { return source_; }
+
+  private:
+    Instruction* source_ = nullptr;
+    const type::Type* type_ = nullptr;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_INSTRUCTION_RESULT_H_
diff --git a/src/tint/ir/instruction_result_test.cc b/src/tint/ir/instruction_result_test.cc
new file mode 100644
index 0000000..b21aaac
--- /dev/null
+++ b/src/tint/ir/instruction_result_test.cc
@@ -0,0 +1,39 @@
+// 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 "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
+#include "src/tint/ir/ir_test_helper.h"
+
+using namespace tint::number_suffixes;        // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+
+namespace tint::ir {
+namespace {
+
+using IR_InstructionResultTest = IRTestHelper;
+
+TEST_F(IR_InstructionResultTest, Destroy_HasSource) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+            val->Destroy();
+        },
+        "");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/ir_test_helper.h b/src/tint/ir/ir_test_helper.h
index 737a596..0406606 100644
--- a/src/tint/ir/ir_test_helper.h
+++ b/src/tint/ir/ir_test_helper.h
@@ -34,15 +34,6 @@
     Builder b{mod};
     /// The type manager
     type::Manager& ty{mod.Types()};
-
-    /// Alias to builtin::AddressSpace::kStorage
-    static constexpr auto storage = builtin::AddressSpace::kStorage;
-    /// Alias to builtin::AddressSpace::kUniform
-    static constexpr auto uniform = builtin::AddressSpace::kUniform;
-    /// Alias to builtin::AddressSpace::kPrivate
-    static constexpr auto private_ = builtin::AddressSpace::kPrivate;
-    /// Alias to builtin::AddressSpace::kFunction
-    static constexpr auto function = builtin::AddressSpace::kFunction;
 };
 
 using IRTestHelper = IRTestHelperBase<testing::Test>;
diff --git a/src/tint/ir/load.cc b/src/tint/ir/load.cc
index 4beb11e..0918398 100644
--- a/src/tint/ir/load.cc
+++ b/src/tint/ir/load.cc
@@ -21,12 +21,12 @@
 
 namespace tint::ir {
 
-Load::Load(Value* from) {
-    TINT_ASSERT_OR_RETURN(IR, from);
-    TINT_ASSERT_OR_RETURN(IR, tint::Is<type::Pointer>(from->Type()));
+Load::Load(InstructionResult* result, Value* from) {
+    TINT_ASSERT(IR, from->Type()->Is<type::Pointer>());
+    TINT_ASSERT(IR, from && from->Type()->UnwrapPtr() == result->Type());
 
-    result_type_ = from->Type()->UnwrapPtr();
-    AddOperand(from);
+    AddOperand(Load::kFromOperandOffset, from);
+    AddResult(result);
 }
 
 Load::~Load() = default;
diff --git a/src/tint/ir/load.h b/src/tint/ir/load.h
index 29b3b22..34438a5 100644
--- a/src/tint/ir/load.h
+++ b/src/tint/ir/load.h
@@ -21,22 +21,20 @@
 namespace tint::ir {
 
 /// A load instruction in the IR.
-class Load : public utils::Castable<Load, OperandInstruction<1>> {
+class Load : public utils::Castable<Load, OperandInstruction<1, 1>> {
   public:
+    /// The offset in Operands() for the from value
+    static constexpr size_t kFromOperandOffset = 0;
+
     /// Constructor (infers type)
+    /// @param result the result value
     /// @param from the value being loaded from
-    explicit Load(Value* from);
+    Load(InstructionResult* result, Value* from);
 
     ~Load() override;
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the value being loaded from
-    Value* From() { return operands_[0]; }
-
-  private:
-    const type::Type* result_type_ = nullptr;
+    Value* From() { return operands_[kFromOperandOffset]; }
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/load_test.cc b/src/tint/ir/load_test.cc
index 0cfba85..5e063a8 100644
--- a/src/tint/ir/load_test.cc
+++ b/src/tint/ir/load_test.cc
@@ -21,35 +21,44 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_LoadTest = IRTestHelper;
 
 TEST_F(IR_LoadTest, Create) {
     auto* store_type = ty.i32();
-    auto* var =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, store_type, builtin::Access::kReadWrite));
+    auto* var = b.Var(ty.ptr<function, i32>());
     auto* inst = b.Load(var);
 
     ASSERT_TRUE(inst->Is<Load>());
-    ASSERT_EQ(inst->From(), var);
+    ASSERT_EQ(inst->From(), var->Result());
+    EXPECT_EQ(inst->Result()->Type(), store_type);
 
-    EXPECT_EQ(inst->Type(), store_type);
-
-    ASSERT_TRUE(inst->From()->Is<ir::Var>());
-    EXPECT_EQ(inst->From(), var);
+    auto* result = inst->From()->As<InstructionResult>();
+    ASSERT_NE(result, nullptr);
+    ASSERT_TRUE(result->Source()->Is<ir::Var>());
+    EXPECT_EQ(result->Source(), var);
 }
 
 TEST_F(IR_LoadTest, Usage) {
-    auto* store_type = ty.i32();
-    auto* var =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, store_type, builtin::Access::kReadWrite));
+    auto* var = b.Var(ty.ptr<function, i32>());
     auto* inst = b.Load(var);
 
     ASSERT_NE(inst->From(), nullptr);
     EXPECT_THAT(inst->From()->Usages(), testing::UnorderedElementsAre(Usage{inst, 0u}));
 }
 
+TEST_F(IR_LoadTest, Results) {
+    auto* var = b.Var(ty.ptr<function, i32>());
+    auto* inst = b.Load(var);
+
+    EXPECT_TRUE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Result()->Is<InstructionResult>());
+    EXPECT_EQ(inst->Result()->Source(), inst);
+}
+
 TEST_F(IR_LoadTest, Fail_NonPtr_Builder) {
     EXPECT_FATAL_FAILURE(
         {
@@ -60,25 +69,5 @@
         "");
 }
 
-TEST_F(IR_LoadTest, Fail_NullValue_Builder) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Load(nullptr);
-        },
-        "");
-}
-
-TEST_F(IR_LoadTest, Fail_NullValue) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            Load l(nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/loop.cc b/src/tint/ir/loop.cc
index 4f25fcd..4b931b4 100644
--- a/src/tint/ir/loop.cc
+++ b/src/tint/ir/loop.cc
@@ -22,12 +22,11 @@
 
 namespace tint::ir {
 
-Loop::Loop(ir::Block* i, ir::MultiInBlock* b, ir::MultiInBlock* c, ir::MultiInBlock* m)
-    : initializer_(i), body_(b), continuing_(c), merge_(m) {
+Loop::Loop(ir::Block* i, ir::MultiInBlock* b, ir::MultiInBlock* c)
+    : initializer_(i), body_(b), continuing_(c) {
     TINT_ASSERT(IR, initializer_);
     TINT_ASSERT(IR, body_);
     TINT_ASSERT(IR, continuing_);
-    TINT_ASSERT(IR, merge_);
 
     if (initializer_) {
         initializer_->SetParent(this);
@@ -38,13 +37,22 @@
     if (continuing_) {
         continuing_->SetParent(this);
     }
-    if (merge_) {
-        merge_->SetParent(this);
-    }
 }
 
 Loop::~Loop() = default;
 
+void Loop::ForeachBlock(const std::function<void(ir::Block*)>& cb) {
+    if (initializer_) {
+        cb(initializer_);
+    }
+    if (body_) {
+        cb(body_);
+    }
+    if (continuing_) {
+        cb(continuing_);
+    }
+}
+
 bool Loop::HasInitializer() {
     return initializer_->HasBranchTarget();
 }
diff --git a/src/tint/ir/loop.h b/src/tint/ir/loop.h
index 31f18fe..f5e2ea4 100644
--- a/src/tint/ir/loop.h
+++ b/src/tint/ir/loop.h
@@ -51,12 +51,6 @@
 ///          ┃           ┃ BreakIf(true)
 ///          ┗━━━━━━━━━━▶┃
 ///                      ▼
-///             ┌────────────────┐
-///             │     Merge      │
-///             │  (optional)    │
-///             └────────────────┘
-///                      ┃
-///                      ▼
 ///                     out
 ///
 /// ```
@@ -66,10 +60,12 @@
     /// @param i the initializer block
     /// @param b the body block
     /// @param c the continuing block
-    /// @param m the merge block
-    Loop(ir::Block* i, ir::MultiInBlock* b, ir::MultiInBlock* c, ir::MultiInBlock* m);
+    Loop(ir::Block* i, ir::MultiInBlock* b, ir::MultiInBlock* c);
     ~Loop() override;
 
+    /// @copydoc ControlInstruction::ForeachBlock
+    void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
+
     /// @returns the switch initializer block
     ir::Block* Initializer() { return initializer_; }
 
@@ -83,14 +79,10 @@
     /// @returns the switch continuing block
     ir::MultiInBlock* Continuing() { return continuing_; }
 
-    /// @returns the switch merge branch
-    ir::MultiInBlock* Merge() { return merge_; }
-
   private:
     ir::Block* initializer_ = nullptr;
     ir::MultiInBlock* body_ = nullptr;
     ir::MultiInBlock* continuing_ = nullptr;
-    ir::MultiInBlock* merge_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/loop_test.cc b/src/tint/ir/loop_test.cc
index 38fecfa..1746c8d 100644
--- a/src/tint/ir/loop_test.cc
+++ b/src/tint/ir/loop_test.cc
@@ -27,7 +27,12 @@
     EXPECT_EQ(loop->Initializer()->Parent(), loop);
     EXPECT_EQ(loop->Body()->Parent(), loop);
     EXPECT_EQ(loop->Continuing()->Parent(), loop);
-    EXPECT_EQ(loop->Merge()->Parent(), loop);
+}
+
+TEST_F(IR_LoopTest, Result) {
+    auto* loop = b.Loop();
+    EXPECT_FALSE(loop->HasResults());
+    EXPECT_FALSE(loop->HasMultiResults());
 }
 
 TEST_F(IR_LoopTest, Fail_NullInitializerBlock) {
@@ -35,7 +40,7 @@
         {
             Module mod;
             Builder b{mod};
-            Loop loop(nullptr, b.MultiInBlock(), b.MultiInBlock(), b.MultiInBlock());
+            Loop loop(nullptr, b.MultiInBlock(), b.MultiInBlock());
         },
         "");
 }
@@ -45,7 +50,7 @@
         {
             Module mod;
             Builder b{mod};
-            Loop loop(b.Block(), nullptr, b.MultiInBlock(), b.MultiInBlock());
+            Loop loop(b.Block(), nullptr, b.MultiInBlock());
         },
         "");
 }
@@ -55,17 +60,7 @@
         {
             Module mod;
             Builder b{mod};
-            Loop loop(b.Block(), b.MultiInBlock(), nullptr, b.MultiInBlock());
-        },
-        "");
-}
-
-TEST_F(IR_LoopTest, Fail_NullMultiInBlock) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            Loop loop(b.Block(), b.MultiInBlock(), b.MultiInBlock(), nullptr);
+            Loop loop(b.Block(), b.MultiInBlock(), nullptr);
         },
         "");
 }
diff --git a/src/tint/ir/module.cc b/src/tint/ir/module.cc
index 3b86a80..1ad7c88 100644
--- a/src/tint/ir/module.cc
+++ b/src/tint/ir/module.cc
@@ -26,10 +26,20 @@
 
 Module& Module::operator=(Module&&) = default;
 
+Symbol Module::NameOf(Instruction* inst) {
+    TINT_ASSERT(IR, inst->HasResults() && !inst->HasMultiResults());
+    return NameOf(inst->Result());
+}
+
 Symbol Module::NameOf(Value* value) {
     return value_to_id_.Get(value).value_or(Symbol{});
 }
 
+Symbol Module::SetName(Instruction* inst, std::string_view name) {
+    TINT_ASSERT(IR, inst->HasResults() && !inst->HasMultiResults());
+    return SetName(inst->Result(), name);
+}
+
 Symbol Module::SetName(Value* value, std::string_view name) {
     TINT_ASSERT(IR, !name.empty());
 
diff --git a/src/tint/ir/module.h b/src/tint/ir/module.h
index 4f33f26..e5e292d 100644
--- a/src/tint/ir/module.h
+++ b/src/tint/ir/module.h
@@ -58,10 +58,21 @@
     /// @returns a reference to this module
     Module& operator=(Module&& o);
 
+    /// @param inst the instruction
+    /// @return the name of the given instruction, or an invalid symbol if the instruction is not
+    /// named. Requires that the instruction only has a single return value.
+    Symbol NameOf(Instruction* inst);
+
     /// @param value the value
     /// @return the name of the given value, or an invalid symbol if the value is not named.
     Symbol NameOf(Value* value);
 
+    /// @param inst the instruction to set the name of
+    /// @param name the desired name of the value. May be suffixed on collision.
+    /// @return the unique symbol of the given value
+    /// @note requires the instruction be a single result instruction.
+    Symbol SetName(Instruction* inst, std::string_view name);
+
     /// @param value the value to name.
     /// @param name the desired name of the value. May be suffixed on collision.
     /// @return the unique symbol of the given value.
@@ -76,6 +87,9 @@
     /// The constant value manager
     constant::Manager constant_values;
 
+    /// The instruction allocator
+    utils::BlockAllocator<Instruction> instructions;
+
     /// The value allocator
     utils::BlockAllocator<Value> values;
 
diff --git a/src/tint/ir/module_test.cc b/src/tint/ir/module_test.cc
index 61cd5d4..4d24881 100644
--- a/src/tint/ir/module_test.cc
+++ b/src/tint/ir/module_test.cc
@@ -19,43 +19,39 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
-class IR_ModuleTest : public IRTestHelper {
-  protected:
-    const type::Pointer* ptr(const type::Type* elem) {
-        return ty.ptr(builtin::AddressSpace::kFunction, elem, builtin::Access::kReadWrite);
-    }
-};
+using IR_ModuleTest = IRTestHelper;
 
 TEST_F(IR_ModuleTest, NameOfUnnamed) {
-    auto* v = mod.values.Create<ir::Var>(ptr(ty.i32()));
+    auto* v = b.Var(ty.ptr<function, i32>());
     EXPECT_FALSE(mod.NameOf(v).IsValid());
 }
 
 TEST_F(IR_ModuleTest, SetName) {
-    auto* v = mod.values.Create<ir::Var>(ptr(ty.i32()));
+    auto* v = b.Var(ty.ptr<function, i32>());
     EXPECT_EQ(mod.SetName(v, "a").Name(), "a");
     EXPECT_EQ(mod.NameOf(v).Name(), "a");
 }
 
 TEST_F(IR_ModuleTest, SetNameRename) {
-    auto* v = mod.values.Create<ir::Var>(ptr(ty.i32()));
+    auto* v = b.Var(ty.ptr<function, i32>());
     EXPECT_EQ(mod.SetName(v, "a").Name(), "a");
     EXPECT_EQ(mod.SetName(v, "b").Name(), "b");
     EXPECT_EQ(mod.NameOf(v).Name(), "b");
 }
 
 TEST_F(IR_ModuleTest, SetNameCollision) {
-    auto* a = mod.values.Create<ir::Var>(ptr(ty.i32()));
-    auto* b = mod.values.Create<ir::Var>(ptr(ty.i32()));
-    auto* c = mod.values.Create<ir::Var>(ptr(ty.i32()));
-    EXPECT_EQ(mod.SetName(a, "x").Name(), "x");
-    EXPECT_EQ(mod.SetName(b, "x_1").Name(), "x_1");
-    EXPECT_EQ(mod.SetName(c, "x").Name(), "x_2");
-    EXPECT_EQ(mod.NameOf(a).Name(), "x");
-    EXPECT_EQ(mod.NameOf(b).Name(), "x_1");
-    EXPECT_EQ(mod.NameOf(c).Name(), "x_2");
+    auto* v1 = b.Var(ty.ptr<function, i32>());
+    auto* v2 = b.Var(ty.ptr<function, i32>());
+    auto* v3 = b.Var(ty.ptr<function, i32>());
+    EXPECT_EQ(mod.SetName(v1, "x").Name(), "x");
+    EXPECT_EQ(mod.SetName(v2, "x_1").Name(), "x_1");
+    EXPECT_EQ(mod.SetName(v3, "x").Name(), "x_2");
+    EXPECT_EQ(mod.NameOf(v1).Name(), "x");
+    EXPECT_EQ(mod.NameOf(v2).Name(), "x_1");
+    EXPECT_EQ(mod.NameOf(v3).Name(), "x_2");
 }
 
 }  // namespace
diff --git a/src/tint/ir/multi_in_block.cc b/src/tint/ir/multi_in_block.cc
index a86db11..94e7f59 100644
--- a/src/tint/ir/multi_in_block.cc
+++ b/src/tint/ir/multi_in_block.cc
@@ -26,14 +26,10 @@
 
 void MultiInBlock::SetParams(utils::VectorRef<BlockParam*> params) {
     params_ = std::move(params);
-
-    TINT_ASSERT(IR, !params_.Any(utils::IsNull));
 }
 
 void MultiInBlock::SetParams(std::initializer_list<BlockParam*> params) {
     params_ = std::move(params);
-
-    TINT_ASSERT(IR, !params_.Any(utils::IsNull));
 }
 
 void MultiInBlock::AddInboundSiblingBranch(ir::Branch* node) {
diff --git a/src/tint/ir/multi_in_block.h b/src/tint/ir/multi_in_block.h
index 6f9b3ee..e0d807f 100644
--- a/src/tint/ir/multi_in_block.h
+++ b/src/tint/ir/multi_in_block.h
@@ -27,10 +27,8 @@
 namespace tint::ir {
 
 /// A block that can be the target of multiple branches.
-/// MultiInBlocks maintain a list of inbound branches from branch instructions excluding ir::If,
-/// ir::Switch and ir::Loop which implicitly branch to the internal block.
-/// MultiInBlocks hold a number of BlockParam parameters, used to pass values from the branch source
-/// to this target.
+/// MultiInBlocks maintain a list of inbound branches and a number of BlockParam parameters, used to
+/// pass values from the branch source to this target.
 class MultiInBlock : public utils::Castable<MultiInBlock, Block> {
   public:
     /// Constructor
diff --git a/src/tint/ir/multi_in_block_test.cc b/src/tint/ir/multi_in_block_test.cc
index 2d82715..ef9438c 100644
--- a/src/tint/ir/multi_in_block_test.cc
+++ b/src/tint/ir/multi_in_block_test.cc
@@ -23,18 +23,6 @@
 using namespace tint::number_suffixes;  // NOLINT
 using IR_MultiInBlockTest = IRTestHelper;
 
-TEST_F(IR_MultiInBlockTest, Fail_NullBlockParam) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-
-            auto* blk = b.MultiInBlock();
-            blk->SetParams({nullptr});
-        },
-        "");
-}
-
 TEST_F(IR_MultiInBlockTest, Fail_NullInboundBranch) {
     EXPECT_FATAL_FAILURE(
         {
diff --git a/src/tint/ir/next_iteration.cc b/src/tint/ir/next_iteration.cc
index dc1ccd2..c7f1e80 100644
--- a/src/tint/ir/next_iteration.cc
+++ b/src/tint/ir/next_iteration.cc
@@ -26,10 +26,12 @@
 NextIteration::NextIteration(ir::Loop* loop, utils::VectorRef<Value*> args /* = utils::Empty */)
     : loop_(loop) {
     TINT_ASSERT(IR, loop_);
+
+    AddOperands(NextIteration::kArgsOperandOffset, std::move(args));
+
     if (loop_) {
         loop_->Body()->AddInboundSiblingBranch(this);
     }
-    AddOperands(std::move(args));
 }
 
 NextIteration::~NextIteration() = default;
diff --git a/src/tint/ir/next_iteration.h b/src/tint/ir/next_iteration.h
index e80f628..6b01634 100644
--- a/src/tint/ir/next_iteration.h
+++ b/src/tint/ir/next_iteration.h
@@ -28,6 +28,9 @@
 /// A next iteration instruction.
 class NextIteration : public utils::Castable<NextIteration, Branch> {
   public:
+    /// The base offset in Operands() for the args
+    static constexpr size_t kArgsOperandOffset = 0;
+
     /// Constructor
     /// @param loop the loop being iterated
     /// @param args the branch arguments
diff --git a/src/tint/ir/next_iteration_test.cc b/src/tint/ir/next_iteration_test.cc
index c0d821c..5c6d16d 100644
--- a/src/tint/ir/next_iteration_test.cc
+++ b/src/tint/ir/next_iteration_test.cc
@@ -32,14 +32,11 @@
         "");
 }
 
-TEST_F(IR_NextIterationTest, Fail_NullValue) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.NextIteration(b.Loop(), nullptr);
-        },
-        "");
+TEST_F(IR_NextIterationTest, Result) {
+    auto* inst = b.NextIteration(b.Loop());
+
+    EXPECT_FALSE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
 }
 
 }  // namespace
diff --git a/src/tint/ir/operand_instruction.cc b/src/tint/ir/operand_instruction.cc
index 7d56ed2..9a1f4aa 100644
--- a/src/tint/ir/operand_instruction.cc
+++ b/src/tint/ir/operand_instruction.cc
@@ -14,10 +14,18 @@
 
 #include "src/tint/ir/operand_instruction.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ir::OperandInstruction<1>);
-TINT_INSTANTIATE_TYPEINFO(tint::ir::OperandInstruction<2>);
-TINT_INSTANTIATE_TYPEINFO(tint::ir::OperandInstruction<3>);
-TINT_INSTANTIATE_TYPEINFO(tint::ir::OperandInstruction<4>);
-TINT_INSTANTIATE_TYPEINFO(tint::ir::OperandInstruction<8>);
+using Op10 = tint::ir::OperandInstruction<1, 0>;
+using Op11 = tint::ir::OperandInstruction<1, 1>;
+using Op20 = tint::ir::OperandInstruction<2, 0>;
+using Op21 = tint::ir::OperandInstruction<2, 1>;
+using Op31 = tint::ir::OperandInstruction<3, 1>;
+using Op41 = tint::ir::OperandInstruction<4, 1>;
+
+TINT_INSTANTIATE_TYPEINFO(Op10);
+TINT_INSTANTIATE_TYPEINFO(Op11);
+TINT_INSTANTIATE_TYPEINFO(Op20);
+TINT_INSTANTIATE_TYPEINFO(Op21);
+TINT_INSTANTIATE_TYPEINFO(Op31);
+TINT_INSTANTIATE_TYPEINFO(Op41);
 
 namespace tint::ir {}  // namespace tint::ir
diff --git a/src/tint/ir/operand_instruction.h b/src/tint/ir/operand_instruction.h
index 8a121f7..82957c0 100644
--- a/src/tint/ir/operand_instruction.h
+++ b/src/tint/ir/operand_instruction.h
@@ -15,21 +15,32 @@
 #ifndef SRC_TINT_IR_OPERAND_INSTRUCTION_H_
 #define SRC_TINT_IR_OPERAND_INSTRUCTION_H_
 
+#include <utility>
+
 #include "src/tint/ir/instruction.h"
+#include "src/tint/ir/instruction_result.h"
 
 namespace tint::ir {
 
 /// An instruction in the IR that expects one or more operands.
-template <unsigned N>
-class OperandInstruction : public utils::Castable<OperandInstruction<N>, Instruction> {
+/// @tparam N the number of operands before spilling to the heap
+/// @tparam R the number of result values before spilling to the heap
+template <unsigned N, unsigned R>
+class OperandInstruction : public utils::Castable<OperandInstruction<N, R>, Instruction> {
   public:
     /// Destructor
     ~OperandInstruction() override = default;
 
+    /// @copydoc tint::ir::Value::Destroy
+    void Destroy() override {
+        ClearOperands();
+        Instruction::Destroy();
+    }
+
     /// Set an operand at a given index.
     /// @param index the operand index
     /// @param value the value to use
-    void SetOperand(uint32_t index, ir::Value* value) override {
+    void SetOperand(size_t index, ir::Value* value) override {
         TINT_ASSERT(IR, index < operands_.Length());
         if (operands_[index]) {
             operands_[index]->RemoveUsage({this, index});
@@ -38,30 +49,85 @@
         if (value) {
             value->AddUsage({this, index});
         }
-        return;
     }
 
+    /// Sets the operands to @p operands
+    /// @param operands the new operands for the instruction
+    void SetOperands(utils::VectorRef<ir::Value*> operands) {
+        ClearOperands();
+        operands_ = std::move(operands);
+        for (size_t i = 0; i < operands_.Length(); i++) {
+            operands_[i]->AddUsage({this, static_cast<uint32_t>(i)});
+        }
+    }
+
+    /// Removes all operands from the instruction
+    void ClearOperands() {
+        for (uint32_t i = 0; i < operands_.Length(); i++) {
+            operands_[i]->RemoveUsage({this, i});
+        }
+        operands_.Clear();
+    }
+
+    /// @returns the operands of the instruction
+    utils::VectorRef<ir::Value*> Operands() override { return operands_; }
+
+    /// @returns true if the instruction has result values
+    bool HasResults() override { return !results_.IsEmpty(); }
+    /// @returns true if the instruction has multiple values
+    bool HasMultiResults() override { return results_.Length() > 1; }
+
+    /// @returns the first result. Returns `nullptr` if there are no results, or if ther are
+    /// multi-results
+    InstructionResult* Result() override {
+        if (!HasResults() || HasMultiResults()) {
+            return nullptr;
+        }
+        return results_[0];
+    }
+
+    using Instruction::Result;
+
+    /// @returns the result values for this instruction
+    utils::VectorRef<InstructionResult*> Results() override { return results_; }
+
   protected:
     /// Append a new operand to the operand list for this instruction.
+    /// @param idx the index the operand should be at
     /// @param value the operand value to append
-    void AddOperand(ir::Value* value) {
+    void AddOperand(size_t idx, ir::Value* value) {
+        TINT_ASSERT(IR, idx == operands_.Length());
+
         if (value) {
             value->AddUsage({this, static_cast<uint32_t>(operands_.Length())});
         }
         operands_.Push(value);
     }
 
-    /// Append a list of non-null operands to the operand list for this instruction.
+    /// Append a list of operands to the operand list for this instruction.
+    /// @param start_idx the index from which the values should start
     /// @param values the operand values to append
-    void AddOperands(utils::VectorRef<ir::Value*> values) {
+    void AddOperands(size_t start_idx, utils::VectorRef<ir::Value*> values) {
+        size_t idx = start_idx;
         for (auto* val : values) {
-            TINT_ASSERT(IR, val != nullptr);
-            AddOperand(val);
+            AddOperand(idx, val);
+            idx += 1;
         }
     }
 
+    /// Appends a result value to the instruction
+    /// @param value the value to append
+    void AddResult(InstructionResult* value) {
+        if (value) {
+            value->SetSource(this);
+        }
+        results_.Push(value);
+    }
+
     /// The operands to this instruction.
     utils::Vector<ir::Value*, N> operands_;
+    /// The results of this instruction.
+    utils::Vector<ir::InstructionResult*, R> results_;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/operand_instruction_test.cc b/src/tint/ir/operand_instruction_test.cc
new file mode 100644
index 0000000..9d05a07
--- /dev/null
+++ b/src/tint/ir/operand_instruction_test.cc
@@ -0,0 +1,45 @@
+// 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 "gmock/gmock.h"
+#include "src/tint/ir/ir_test_helper.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_OperandInstructionTest = IRTestHelper;
+
+TEST_F(IR_OperandInstructionTest, Destroy) {
+    auto* block = b.Block();
+    auto* inst = b.Add(ty.i32(), 1_i, 2_i);
+    block->Append(inst);
+    auto* lhs = inst->LHS();
+    auto* rhs = inst->RHS();
+    EXPECT_EQ(inst->Block(), block);
+    EXPECT_THAT(lhs->Usages(), testing::ElementsAre(Usage{inst, 0u}));
+    EXPECT_THAT(rhs->Usages(), testing::ElementsAre(Usage{inst, 1u}));
+    EXPECT_TRUE(inst->Result()->Alive());
+
+    inst->Destroy();
+
+    EXPECT_EQ(inst->Block(), nullptr);
+    EXPECT_TRUE(lhs->Usages().IsEmpty());
+    EXPECT_TRUE(rhs->Usages().IsEmpty());
+    EXPECT_FALSE(inst->Result()->Alive());
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/program_test_helper.h b/src/tint/ir/program_test_helper.h
index 3ce88f0..d982366 100644
--- a/src/tint/ir/program_test_helper.h
+++ b/src/tint/ir/program_test_helper.h
@@ -20,9 +20,9 @@
 #include <utility>
 
 #include "gtest/gtest.h"
+#include "src/tint/builtin/number.h"
 #include "src/tint/ir/disassembler.h"
 #include "src/tint/ir/from_program.h"
-#include "src/tint/number.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/return.cc b/src/tint/ir/return.cc
index 8c0de42..325f283 100644
--- a/src/tint/ir/return.cc
+++ b/src/tint/ir/return.cc
@@ -22,19 +22,13 @@
 
 namespace tint::ir {
 
-Return::Return(Function* func) : func_(func) {
-    TINT_ASSERT_OR_RETURN(IR, func_);
-
-    func_->AddUsage({this, 0u});
+Return::Return(Function* func) {
+    AddOperand(Return::kFunctionOperandOffset, func);
 }
 
-Return::Return(Function* func, Value* arg) : func_(func) {
-    TINT_ASSERT_OR_RETURN(IR, func_);
-    TINT_ASSERT_OR_RETURN(IR, arg);
-
-    func_->AddUsage({this, 0u});
-
-    AddOperand(arg);
+Return::Return(Function* func, ir::Value* arg) {
+    AddOperand(Return::kFunctionOperandOffset, func);
+    AddOperand(Return::kArgOperandOffset, arg);
 }
 
 Return::~Return() = default;
diff --git a/src/tint/ir/return.h b/src/tint/ir/return.h
index 55463e2..403a215 100644
--- a/src/tint/ir/return.h
+++ b/src/tint/ir/return.h
@@ -28,6 +28,12 @@
 /// A return instruction.
 class Return : public utils::Castable<Return, Branch> {
   public:
+    /// The offset in Operands() for the function being returned
+    static constexpr size_t kFunctionOperandOffset = 0;
+
+    /// The offset in Operands() for the return argument
+    static constexpr size_t kArgOperandOffset = 1;
+
     /// Constructor (no return value)
     /// @param func the function being returned
     explicit Return(Function* func);
@@ -35,15 +41,26 @@
     /// Constructor
     /// @param func the function being returned
     /// @param arg the return value
-    Return(Function* func, Value* arg);
+    Return(Function* func, ir::Value* arg);
 
     ~Return() override;
 
     /// @returns the function being returned
-    Function* Func() { return func_; }
+    Function* Func() { return operands_[kFunctionOperandOffset]->As<Function>(); }
 
-  private:
-    Function* func_ = nullptr;
+    /// @returns the return value, or nullptr
+    ir::Value* Value() const {
+        return operands_.Length() > kArgOperandOffset ? operands_[kArgOperandOffset] : nullptr;
+    }
+
+    /// Sets the return value
+    /// @param val the new return value
+    void SetValue(ir::Value* val) { SetOperand(kArgOperandOffset, val); }
+
+    /// @returns the branch arguments
+    utils::Slice<ir::Value* const> Args() override {
+        return operands_.Slice().Offset(kArgOperandOffset);
+    }
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/return_test.cc b/src/tint/ir/return_test.cc
index fbe3330..159e94d 100644
--- a/src/tint/ir/return_test.cc
+++ b/src/tint/ir/return_test.cc
@@ -24,37 +24,42 @@
 using namespace tint::number_suffixes;  // NOLINT
 using IR_ReturnTest = IRTestHelper;
 
-TEST_F(IR_ReturnTest, Fail_NullFunction) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Return(nullptr);
-        },
-        "");
-}
-
-TEST_F(IR_ReturnTest, Fail_NullValue) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            mod.values.Create<Return>(b.Function("myfunc", mod.Types().void_()), nullptr);
-        },
-        "");
-}
-
 TEST_F(IR_ReturnTest, ImplicitNoValue) {
-    auto* ret = b.Return(b.Function("myfunc", ty.void_()));
+    auto* func = b.Function("myfunc", ty.void_());
+    auto* ret = b.Return(func);
+    ASSERT_EQ(ret->Func(), func);
     EXPECT_TRUE(ret->Args().IsEmpty());
+    EXPECT_EQ(ret->Value(), nullptr);
+    EXPECT_THAT(func->Usages(), testing::UnorderedElementsAre(Usage{ret, 0u}));
 }
 
 TEST_F(IR_ReturnTest, WithValue) {
+    auto* func = b.Function("myfunc", ty.i32());
     auto* val = b.Constant(42_i);
-    auto* ret = b.Return(b.Function("myfunc", ty.i32()), val);
+    auto* ret = b.Return(func, val);
+    ASSERT_EQ(ret->Func(), func);
     ASSERT_EQ(ret->Args().Length(), 1u);
     EXPECT_EQ(ret->Args()[0], val);
-    EXPECT_THAT(val->Usages(), testing::UnorderedElementsAre(Usage{ret, 0u}));
+    EXPECT_EQ(ret->Value(), val);
+    EXPECT_THAT(func->Usages(), testing::UnorderedElementsAre(Usage{ret, 0u}));
+    EXPECT_THAT(val->Usages(), testing::UnorderedElementsAre(Usage{ret, 1u}));
+}
+
+TEST_F(IR_ReturnTest, Result) {
+    auto* vfunc = b.Function("vfunc", ty.void_());
+    auto* ifunc = b.Function("ifunc", ty.i32());
+
+    {
+        auto* ret1 = b.Return(vfunc);
+        EXPECT_FALSE(ret1->HasResults());
+        EXPECT_FALSE(ret1->HasMultiResults());
+    }
+
+    {
+        auto* ret2 = b.Return(ifunc, b.Constant(42_i));
+        EXPECT_FALSE(ret2->HasResults());
+        EXPECT_FALSE(ret2->HasMultiResults());
+    }
 }
 
 }  // namespace
diff --git a/src/tint/ir/store.cc b/src/tint/ir/store.cc
index 958f1e4..d1ea611 100644
--- a/src/tint/ir/store.cc
+++ b/src/tint/ir/store.cc
@@ -20,11 +20,8 @@
 namespace tint::ir {
 
 Store::Store(Value* to, Value* from) {
-    TINT_ASSERT(IR, to);
-    TINT_ASSERT(IR, from);
-
-    AddOperand(to);
-    AddOperand(from);
+    AddOperand(Store::kToOperandOffset, to);
+    AddOperand(Store::kFromOperandOffset, from);
 }
 
 Store::~Store() = default;
diff --git a/src/tint/ir/store.h b/src/tint/ir/store.h
index 41289f2..e14a58f 100644
--- a/src/tint/ir/store.h
+++ b/src/tint/ir/store.h
@@ -21,8 +21,14 @@
 namespace tint::ir {
 
 /// A store instruction in the IR.
-class Store : public utils::Castable<Store, OperandInstruction<2>> {
+class Store : public utils::Castable<Store, OperandInstruction<2, 0>> {
   public:
+    /// The offset in Operands() for the `to` value
+    static constexpr size_t kToOperandOffset = 0;
+
+    /// The offset in Operands() for the `from` value
+    static constexpr size_t kFromOperandOffset = 1;
+
     /// Constructor
     /// @param to the value to store too
     /// @param from the value being stored from
@@ -30,10 +36,10 @@
     ~Store() override;
 
     /// @returns the value being stored too
-    Value* To() { return operands_[0]; }
+    Value* To() { return operands_[kToOperandOffset]; }
 
     /// @returns the value being stored
-    Value* From() { return operands_[1]; }
+    Value* From() { return operands_[kFromOperandOffset]; }
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/store_test.cc b/src/tint/ir/store_test.cc
index 5578005..1bf40cb 100644
--- a/src/tint/ir/store_test.cc
+++ b/src/tint/ir/store_test.cc
@@ -21,17 +21,17 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_StoreTest = IRTestHelper;
 
 TEST_F(IR_StoreTest, CreateStore) {
-    auto* to = b.Var(mod.Types().ptr(builtin::AddressSpace::kPrivate, mod.Types().i32(),
-                                     builtin::Access::kReadWrite));
+    auto* to = b.Var(ty.ptr<private_, i32>());
     auto* inst = b.Store(to, 4_i);
 
     ASSERT_TRUE(inst->Is<Store>());
-    ASSERT_EQ(inst->To(), to);
+    ASSERT_EQ(inst->To(), to->Result());
 
     ASSERT_TRUE(inst->From()->Is<Constant>());
     auto lhs = inst->From()->As<Constant>()->Value();
@@ -39,8 +39,8 @@
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_StoreTest, Store_Usage) {
-    auto* to = b.Discard();
+TEST_F(IR_StoreTest, Usage) {
+    auto* to = b.Var(ty.ptr<private_, i32>());
     auto* inst = b.Store(to, 4_i);
 
     ASSERT_NE(inst->To(), nullptr);
@@ -50,26 +50,12 @@
     EXPECT_THAT(inst->From()->Usages(), testing::UnorderedElementsAre(Usage{inst, 1u}));
 }
 
-TEST_F(IR_StoreTest, Fail_NullTo) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Store(nullptr, 1_u);
-        },
-        "");
-}
+TEST_F(IR_StoreTest, Result) {
+    auto* to = b.Var(ty.ptr<private_, i32>());
+    auto* inst = b.Store(to, 4_i);
 
-TEST_F(IR_StoreTest, Fail_NullFrom) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            auto* to = b.Var(mod.Types().ptr(builtin::AddressSpace::kPrivate, mod.Types().i32(),
-                                             builtin::Access::kReadWrite));
-            b.Store(to, nullptr);
-        },
-        "");
+    EXPECT_FALSE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
 }
 
 }  // namespace
diff --git a/src/tint/ir/switch.cc b/src/tint/ir/switch.cc
index 8b70cfc..9d9e75d 100644
--- a/src/tint/ir/switch.cc
+++ b/src/tint/ir/switch.cc
@@ -16,21 +16,20 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Switch);
 
-#include "src/tint/ir/multi_in_block.h"
-
 namespace tint::ir {
 
-Switch::Switch(Value* cond, ir::MultiInBlock* m) : merge_(m) {
+Switch::Switch(Value* cond) {
     TINT_ASSERT(IR, cond);
-    TINT_ASSERT(IR, merge_);
 
-    AddOperand(cond);
-
-    if (merge_) {
-        merge_->SetParent(this);
-    }
+    AddOperand(Switch::kConditionOperandOffset, cond);
 }
 
 Switch::~Switch() = default;
 
+void Switch::ForeachBlock(const std::function<void(ir::Block*)>& cb) {
+    for (auto& c : cases_) {
+        cb(c.Block());
+    }
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 7791c2c..1d16b89 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -35,17 +35,17 @@
 ///        │ Case A │     │ Case B │     │ Case C │
 ///        └────────┘     └────────┘     └────────┘
 ///  ExitSwitch ┃   ExitSwitch ┃   ExitSwitch ┃
-///             ┃              ▼              ┃
-///             ┃       ┌────────────┐        ┃
-///     ╌╌╌╌╌╌╌╌┺━━━━━━▶│ Merge      │◀━━━━━━━┹╌╌╌╌╌╌╌╌
-///                     │ (optional) │
-///                     └────────────┘
+///             ┃              ┃              ┃
+///     ╌╌╌╌╌╌╌╌┺━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┹╌╌╌╌╌╌╌╌
 ///                            ┃
 ///                            ▼
 ///                           out
 /// ```
 class Switch : public utils::Castable<Switch, ControlInstruction> {
   public:
+    /// The offset in Operands() for the condition
+    static constexpr size_t kConditionOperandOffset = 0;
+
     /// A case selector
     struct CaseSelector {
         /// @returns true if this is a default selector
@@ -59,33 +59,28 @@
     struct Case {
         /// The case selector for this node
         utils::Vector<CaseSelector, 4> selectors;
-        /// The start block for the case block.
-        ir::Block* start = nullptr;
+        /// The case block.
+        ir::Block* block = nullptr;
 
-        /// @returns the case start target
-        ir::Block* Start() { return start; }
+        /// @returns the case block
+        ir::Block* Block() { return block; }
     };
 
     /// Constructor
     /// @param cond the condition
-    /// @param m the merge block
-    explicit Switch(Value* cond, ir::MultiInBlock* m);
+    explicit Switch(Value* cond);
     ~Switch() override;
 
-    /// @returns the switch merge branch
-    ir::MultiInBlock* Merge() { return merge_; }
+    /// @copydoc ControlInstruction::ForeachBlock
+    void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
 
     /// @returns the switch cases
     utils::Vector<Case, 4>& Cases() { return cases_; }
 
-    /// @returns the branch arguments
-    utils::Slice<Value* const> Args() override { return {}; }
-
     /// @returns the condition
-    Value* Condition() { return operands_[0]; }
+    Value* Condition() { return operands_[kConditionOperandOffset]; }
 
   private:
-    ir::MultiInBlock* merge_ = nullptr;
     utils::Vector<Case, 4> cases_;
 };
 
diff --git a/src/tint/ir/switch_test.cc b/src/tint/ir/switch_test.cc
index 72e0dda..d881247 100644
--- a/src/tint/ir/switch_test.cc
+++ b/src/tint/ir/switch_test.cc
@@ -30,31 +30,17 @@
     EXPECT_THAT(cond->Usages(), testing::UnorderedElementsAre(Usage{switch_, 0u}));
 }
 
+TEST_F(IR_SwitchTest, Results) {
+    auto* cond = b.Constant(true);
+    auto* switch_ = b.Switch(cond);
+    EXPECT_FALSE(switch_->HasResults());
+    EXPECT_FALSE(switch_->HasMultiResults());
+}
+
 TEST_F(IR_SwitchTest, Parent) {
     auto* switch_ = b.Switch(1_i);
     b.Case(switch_, {Switch::CaseSelector{nullptr}});
-    EXPECT_THAT(switch_->Merge()->Parent(), switch_);
-    EXPECT_THAT(switch_->Cases().Front().Start()->Parent(), switch_);
-}
-
-TEST_F(IR_SwitchTest, Fail_NullCondition) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Switch(nullptr);
-        },
-        "");
-}
-
-TEST_F(IR_SwitchTest, Fail_NullMultiInBlock) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            Switch switch_(b.Constant(false), nullptr);
-        },
-        "");
+    EXPECT_THAT(switch_->Cases().Front().Block()->Parent(), switch_);
 }
 
 }  // namespace
diff --git a/src/tint/ir/swizzle.cc b/src/tint/ir/swizzle.cc
index 58cecb2..e6996bc 100644
--- a/src/tint/ir/swizzle.cc
+++ b/src/tint/ir/swizzle.cc
@@ -22,14 +22,13 @@
 
 namespace tint::ir {
 
-Swizzle::Swizzle(const type::Type* ty, Value* object, utils::VectorRef<uint32_t> indices)
-    : result_type_(ty), indices_(std::move(indices)) {
-    TINT_ASSERT(IR, object != nullptr);
-    TINT_ASSERT(IR, result_type_ != nullptr);
+Swizzle::Swizzle(InstructionResult* result, Value* object, utils::VectorRef<uint32_t> indices)
+    : indices_(std::move(indices)) {
     TINT_ASSERT(IR, !indices.IsEmpty());
     TINT_ASSERT(IR, indices.Length() <= 4);
 
-    AddOperand(object);
+    AddOperand(Swizzle::kObjectOperandOffset, object);
+    AddResult(result);
 
     for (auto idx : indices_) {
         TINT_ASSERT(IR, idx < 4);
diff --git a/src/tint/ir/swizzle.h b/src/tint/ir/swizzle.h
index f340a01..c2ab06e 100644
--- a/src/tint/ir/swizzle.h
+++ b/src/tint/ir/swizzle.h
@@ -21,26 +21,25 @@
 namespace tint::ir {
 
 /// A swizzle instruction in the IR.
-class Swizzle : public utils::Castable<Swizzle, OperandInstruction<1>> {
+class Swizzle : public utils::Castable<Swizzle, OperandInstruction<1, 1>> {
   public:
+    /// The offset in Operands() for the object being swizzled
+    static constexpr size_t kObjectOperandOffset = 0;
+
     /// Constructor
-    /// @param result_type the result type
+    /// @param result the result value
     /// @param object the object being swizzled
     /// @param indices the indices to swizzle
-    Swizzle(const type::Type* result_type, Value* object, utils::VectorRef<uint32_t> indices);
+    Swizzle(InstructionResult* result, Value* object, utils::VectorRef<uint32_t> indices);
     ~Swizzle() override;
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the object used for the access
-    Value* Object() { return operands_[0]; }
+    Value* Object() { return operands_[kObjectOperandOffset]; }
 
     /// @returns the swizzle indices
     utils::VectorRef<uint32_t> Indices() { return indices_; }
 
   private:
-    const type::Type* result_type_ = nullptr;
     utils::Vector<uint32_t, 4> indices_;
 };
 
diff --git a/src/tint/ir/swizzle_test.cc b/src/tint/ir/swizzle_test.cc
index ada269d..ab45e8c 100644
--- a/src/tint/ir/swizzle_test.cc
+++ b/src/tint/ir/swizzle_test.cc
@@ -18,6 +18,8 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/ir/ir_test_helper.h"
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+
 namespace tint::ir {
 namespace {
 
@@ -27,7 +29,17 @@
     auto* var = b.Var(ty.ptr<function, i32>());
     auto* a = b.Swizzle(mod.Types().i32(), var, {1u});
 
-    EXPECT_THAT(var->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
+    EXPECT_THAT(var->Result()->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
+}
+
+TEST_F(IR_SwizzleTest, Results) {
+    auto* var = b.Var(ty.ptr<function, i32>());
+    auto* a = b.Swizzle(mod.Types().i32(), var, {1u});
+
+    EXPECT_TRUE(a->HasResults());
+    EXPECT_FALSE(a->HasMultiResults());
+    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
+    EXPECT_EQ(a->Result()->Source(), a);
 }
 
 TEST_F(IR_SwizzleTest, Fail_NullType) {
@@ -41,16 +53,6 @@
         "");
 }
 
-TEST_F(IR_SwizzleTest, Fail_NullObject) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Swizzle(mod.Types().i32(), nullptr, {1u});
-        },
-        "");
-}
-
 TEST_F(IR_SwizzleTest, Fail_EmptyIndices) {
     EXPECT_FATAL_FAILURE(
         {
diff --git a/src/tint/ir/to_program.cc b/src/tint/ir/to_program.cc
index c536ae6..a286702 100644
--- a/src/tint/ir/to_program.cc
+++ b/src/tint/ir/to_program.cc
@@ -22,6 +22,7 @@
 #include "src/tint/ir/call.h"
 #include "src/tint/ir/constant.h"
 #include "src/tint/ir/exit_if.h"
+#include "src/tint/ir/exit_switch.h"
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/load.h"
@@ -45,6 +46,7 @@
 #include "src/tint/type/texture.h"
 #include "src/tint/utils/hashmap.h"
 #include "src/tint/utils/predicates.h"
+#include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/transform.h"
 #include "src/tint/utils/vector.h"
 
@@ -86,13 +88,16 @@
     /// A hashmap of value to symbol used in the emitted AST
     utils::Hashmap<Value*, Symbol, 32> value_names_;
 
-    // The nesting depth of the currently generated AST
-    // 0 is module scope
-    // 1 is root-level function scope
-    // 2+ is within control flow
+    /// The nesting depth of the currently generated AST
+    /// 0 is module scope
+    /// 1 is root-level function scope
+    /// 2+ is within control flow
     uint32_t nesting_depth_ = 0;
 
-    const ast::Function* Fn(Function* fn) {
+    /// The current switch case block
+    ir::Block* current_switch_case_ = nullptr;
+
+    const ast::Function* Fn(ir::Function* fn) {
         SCOPED_NESTING();
 
         // TODO(crbug.com/tint/1915): Properly implement this when we've fleshed out Function
@@ -105,43 +110,23 @@
 
         auto name = AssignNameTo(fn);
         auto ret_ty = Type(fn->ReturnType());
-        auto* body = BlockGraph(fn->StartTarget());
+        auto* body = Block(fn->StartTarget());
         utils::Vector<const ast::Attribute*, 1> attrs{};
         utils::Vector<const ast::Attribute*, 1> ret_attrs{};
         return b.Func(name, std::move(params), ret_ty, body, std::move(attrs),
                       std::move(ret_attrs));
     }
 
-    const ast::BlockStatement* BlockGraph(ir::Block* start_node) {
-        // TODO(crbug.com/tint/1902): Check if the block is dead
-        utils::Vector<const ast::Statement*,
-                      decltype(ast::BlockStatement::statements)::static_length>
-            stmts;
-
-        ir::Block* block = start_node;
+    const ast::BlockStatement* Block(ir::Block* block) {
+        static constexpr size_t N = decltype(ast::BlockStatement::statements)::static_length;
+        utils::Vector<const ast::Statement*, N> stmts;
 
         // TODO(crbug.com/tint/1902): Handle block arguments.
 
-        while (block) {
-            TINT_ASSERT(IR, block->HasBranchTarget());
-
-            for (auto* inst : *block) {
-                if (auto* stmt = Stmt(inst)) {
-                    stmts.Push(stmt);
-                }
+        for (auto* inst : *block) {
+            if (auto stmt = Stmt(inst)) {
+                stmts.Push(stmt);
             }
-            if (auto* if_ = block->Branch()->As<ir::If>()) {
-                if (if_->Merge()->HasBranchTarget()) {
-                    block = if_->Merge();
-                    continue;
-                }
-            } else if (auto* switch_ = block->Branch()->As<ir::Switch>()) {
-                if (switch_->Merge()->HasBranchTarget()) {
-                    block = switch_->Merge();
-                    continue;
-                }
-            }
-            break;
         }
 
         return b.Block(std::move(stmts));
@@ -158,16 +143,17 @@
     /// @return an ast::Statement from @p inst, or nullptr if there was an error
     const ast::Statement* Stmt(ir::Instruction* inst) {
         return tint::Switch(
-            inst,                                                  //
-            [&](ir::Store* i) { return Store(i); },                //
-            [&](ir::Call* i) { return CallStmt(i); },              //
-            [&](ir::Var* i) { return Var(i); },                    //
-            [&](ir::If* if_) { return If(if_); },                  //
-            [&](ir::Switch* switch_) { return Switch(switch_); },  //
-            [&](ir::Return* ret) { return Return(ret); },          //
-            [&](ir::Value*) { return ValueStmt(inst); },
-            // TODO(dsinclair): Remove when branch is only a parent ...
-            [&](ir::Branch*) { return nullptr; },
+            inst,                                                        //
+            [&](ir::Call* i) { return CallStmt(i); },                    //
+            [&](ir::Var* i) { return Var(i); },                          //
+            [&](ir::Load*) { return nullptr; },                          //
+            [&](ir::Store* i) { return Store(i); },                      //
+            [&](ir::If* if_) { return If(if_); },                        //
+            [&](ir::Switch* switch_) { return Switch(switch_); },        //
+            [&](ir::Return* ret) { return Return(ret); },                //
+            [&](ir::ExitSwitch* e) { return ExitSwitch(e); },            //
+            [&](ir::ExitIf*) { return nullptr; },                        //
+            [&](ir::Instruction* i) { return ValueStmt(i->Result()); },  //
             [&](Default) {
                 UNHANDLED_CASE(inst);
                 return nullptr;
@@ -179,33 +165,31 @@
     const ast::IfStatement* If(ir::If* i) {
         SCOPED_NESTING();
         auto* cond = Expr(i->Condition());
-        auto* t = BlockGraph(i->True());
+        auto* t = Block(i->True());
         if (TINT_UNLIKELY(!t)) {
             return nullptr;
         }
 
-        auto* false_blk = i->False();
-        if (false_blk->Length() > 1 || (false_blk->Length() == 1 && false_blk->HasBranchTarget() &&
-                                        !false_blk->Branch()->Is<ir::ExitIf>())) {
-            // If the else target is an `if` which has a merge target that just bounces to the outer
-            // if merge target then emit an 'else if' instead of a block statement for the else.
-            if (auto* inst = i->False()->Instructions(); inst && inst->As<ir::If>()) {
-                auto* if_ = inst->As<ir::If>();
-                if (auto* br = if_->Merge()->Branch()->As<ir::ExitIf>(); br && br->If() == i) {
-                    auto* f = If(if_);
+        if (auto* false_blk = i->False(); false_blk && !false_blk->IsEmpty()) {
+            bool maybe_elseif = (false_blk->Length() == 1) ||
+                                (false_blk->Length() == 2 && false_blk->Back()->Is<ir::Branch>());
+            if (maybe_elseif) {
+                if (auto* else_if = false_blk->Front()->As<ir::If>()) {
+                    auto* f = If(else_if);
                     if (!f) {
                         return nullptr;
                     }
                     return b.If(cond, t, b.Else(f));
                 }
-            } else {
-                auto* f = BlockGraph(i->False());
-                if (!f) {
-                    return nullptr;
-                }
-                return b.If(cond, t, b.Else(f));
             }
+
+            auto* f = Block(i->False());
+            if (!f) {
+                return nullptr;
+            }
+            return b.If(cond, t, b.Else(f));
         }
+
         return b.If(cond, t);
     }
 
@@ -223,7 +207,12 @@
             utils::Transform(s->Cases(),  //
                              [&](ir::Switch::Case c) -> const tint::ast::CaseStatement* {
                                  SCOPED_NESTING();
-                                 auto* body = BlockGraph(c.start);
+
+                                 const ast::BlockStatement* body = nullptr;
+                                 {
+                                     TINT_SCOPED_ASSIGNMENT(current_switch_case_, c.Block());
+                                     body = Block(c.Block());
+                                 }
                                  if (!body) {
                                      return nullptr;
                                  }
@@ -253,6 +242,13 @@
         return b.Switch(cond, std::move(cases));
     }
 
+    const ast::BreakStatement* ExitSwitch(const ir::ExitSwitch* e) {
+        if (current_switch_case_ && current_switch_case_->Branch() == e) {
+            return nullptr;  // No need to emit
+        }
+        return b.Break();
+    }
+
     /// @param ret the ir::Return
     /// @return an ast::ReturnStatement from @p ret, or nullptr if there was an error
     const ast::ReturnStatement* Return(ir::Return* ret) {
@@ -290,7 +286,7 @@
     /// @return an ast::VariableDeclStatement from @p var
     const ast::VariableDeclStatement* Var(ir::Var* var) {
         Symbol name = AssignNameTo(var);
-        auto* ptr = var->Type();
+        auto* ptr = var->Result()->Type()->As<type::Pointer>();
         auto ty = Type(ptr->StoreType());
         const ast::Expression* init = nullptr;
         if (var->Initializer()) {
@@ -321,8 +317,8 @@
 
         // Determine whether the value should be placed into a let, or inlined in its single place
         // of usage. Currently a value is inlined if it has a single usage and is unnamed.
-        // TODO(crbug.com/tint/1902): This logic needs to check that the sequence of side-effecting
-        // expressions is not changed by inlining the expression. This needs fixing.
+        // TODO(crbug.com/tint/1902): This logic needs to check that the sequence of side -
+        // effecting expressions is not changed by inlining the expression. This needs fixing.
         bool create_let = val->Usages().Count() > 1 || mod.NameOf(val).IsValid();
         if (create_let) {
             auto* init = Expr(val);  // Must come before giving the value a name
@@ -342,20 +338,36 @@
     // This prevents littering the ToProgram logic with expensive error checking code.
     ////////////////////////////////////////////////////////////////////////////////////////////////
 
-    /// @param val the ir::Expression
-    /// @return an ast::Expression from @p val.
-    /// @note May be a semantically-invalid placeholder expression on error.
+    /// @param val the value
+    /// @returns the ast::Expression from the values source instruction
     const ast::Expression* Expr(ir::Value* val) {
         if (auto name = value_names_.Get(val)) {
             return b.Expr(name.value());
         }
 
         return tint::Switch(
-            val,                                            //
-            [&](ir::Constant* c) { return ConstExpr(c); },  //
-            [&](ir::Load* l) { return LoadExpr(l); },       //
-            [&](ir::Unary* u) { return UnaryExpr(u); },     //
-            [&](ir::Binary* u) { return BinaryExpr(u); },   //
+            val,                                                          //
+            [&](ir::Constant* c) { return ConstExpr(c); },                //
+            [&](ir::InstructionResult* r) { return Expr(r->Source()); },  //
+            [&](Default) -> const ast::Expression* {
+                UNHANDLED_CASE(val);
+                return b.Expr("<error>");
+            });
+    }
+
+    /// @param val the ir::Expression
+    /// @return an ast::Expression from @p val.
+    /// @note May be a semantically-invalid placeholder expression on error.
+    const ast::Expression* Expr(ir::Instruction* val) {
+        if (auto name = value_names_.Get(val->Result())) {
+            return b.Expr(name.value());
+        }
+
+        return tint::Switch(
+            val,                                           //
+            [&](ir::Load* l) { return LoadExpr(l); },      //
+            [&](ir::Unary* u) { return UnaryExpr(u); },    //
+            [&](ir::Binary* u) { return BinaryExpr(u); },  //
             [&](Default) {
                 UNHANDLED_CASE(val);
                 return b.Expr("<error>");
@@ -554,6 +566,11 @@
     // Helpers
     ////////////////////////////////////////////////////////////////////////////////////////////////
 
+    /// Creates and returns a new, unique name for the instructions result value, or returns the
+    /// previously created name. Must not be called with a multi-result instruction.
+    /// @return the instruction values name
+    Symbol AssignNameTo(Instruction* inst) { return AssignNameTo(inst->Result()); }
+
     /// Creates and returns a new, unique name for the given value, or returns the previously
     /// created name.
     /// @return the value's name
diff --git a/src/tint/ir/transform/add_empty_entry_point.cc b/src/tint/ir/transform/add_empty_entry_point.cc
index 7a5506d..3c28e59 100644
--- a/src/tint/ir/transform/add_empty_entry_point.cc
+++ b/src/tint/ir/transform/add_empty_entry_point.cc
@@ -37,7 +37,7 @@
     ir::Builder builder(*ir);
     auto* ep = builder.Function("unused_entry_point", ir->Types().void_(),
                                 Function::PipelineStage::kCompute, std::array{1u, 1u, 1u});
-    ep->StartTarget()->SetInstructions({builder.Return(ep)});
+    ep->StartTarget()->Append(builder.Return(ep));
     ir->functions.Push(ep);
 }
 
diff --git a/src/tint/ir/transform/add_empty_entry_point_test.cc b/src/tint/ir/transform/add_empty_entry_point_test.cc
index 558e2af..c5c60a1 100644
--- a/src/tint/ir/transform/add_empty_entry_point_test.cc
+++ b/src/tint/ir/transform/add_empty_entry_point_test.cc
@@ -39,7 +39,7 @@
 
 TEST_F(IR_AddEmptyEntryPointTest, ExistingEntryPoint) {
     auto* ep = b.Function("main", mod.Types().void_(), Function::PipelineStage::kFragment);
-    ep->StartTarget()->SetInstructions({b.Return(ep)});
+    ep->StartTarget()->Append(b.Return(ep));
     mod.functions.Push(ep);
 
     auto* expect = R"(
diff --git a/src/tint/ir/transform/block_decorated_structs.cc b/src/tint/ir/transform/block_decorated_structs.cc
index 5c466a5..40805b5 100644
--- a/src/tint/ir/transform/block_decorated_structs.cc
+++ b/src/tint/ir/transform/block_decorated_structs.cc
@@ -45,7 +45,7 @@
         if (!var) {
             continue;
         }
-        auto* ptr = var->Type()->As<type::Pointer>();
+        auto* ptr = var->Result()->Type()->As<type::Pointer>();
         if (!ptr || !(ptr->AddressSpace() == builtin::AddressSpace::kStorage ||
                       ptr->AddressSpace() == builtin::AddressSpace::kUniform)) {
             continue;
@@ -55,7 +55,7 @@
 
     // Now process the buffer variables.
     for (auto* var : buffer_variables) {
-        auto* ptr = var->Type()->As<type::Pointer>();
+        auto* ptr = var->Result()->Type()->As<type::Pointer>();
         auto* store_ty = ptr->StoreType();
 
         bool wrapped = false;
@@ -98,15 +98,15 @@
         var->ReplaceWith(new_var);
 
         // Replace uses of the old variable.
-        var->ReplaceAllUsesWith([&](Usage use) -> Value* {
+        var->Result()->ReplaceAllUsesWith([&](Usage use) -> Value* {
             if (wrapped) {
                 // The structure has been wrapped, so replace all uses of the old variable with a
                 // member accessor on the new variable.
-                auto* access = builder.Access(var->Type(), new_var, 0_u);
+                auto* access = builder.Access(var->Result()->Type(), new_var, 0_u);
                 access->InsertBefore(use.instruction);
-                return access;
+                return access->Result();
             }
-            return new_var;
+            return new_var->Result();
         });
     }
 }
diff --git a/src/tint/ir/transform/block_decorated_structs_test.cc b/src/tint/ir/transform/block_decorated_structs_test.cc
index e86c563..92e3922 100644
--- a/src/tint/ir/transform/block_decorated_structs_test.cc
+++ b/src/tint/ir/transform/block_decorated_structs_test.cc
@@ -24,9 +24,10 @@
 namespace tint::ir::transform {
 namespace {
 
-using IR_BlockDecoratedStructsTest = TransformTest;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
-using namespace tint::number_suffixes;  // NOLINT
+using IR_BlockDecoratedStructsTest = TransformTest;
 
 TEST_F(IR_BlockDecoratedStructsTest, NoRootBlock) {
     auto* func = b.Function("foo", ty.void_());
@@ -52,6 +53,7 @@
     b.RootBlock()->Append(buffer);
 
     auto* func = b.Function("foo", ty.i32());
+
     auto* block = func->StartTarget();
     auto* load = block->Append(b.Load(buffer));
     block->Append(b.Return(func, load));
@@ -116,15 +118,17 @@
 }
 
 TEST_F(IR_BlockDecoratedStructsTest, RuntimeArray) {
-    auto* buffer = b.Var(ty.ptr(storage, ty.runtime_array(ty.i32()), builtin::Access::kReadWrite));
+    auto* buffer = b.Var(ty.ptr<storage, array<i32>>());
     buffer->SetBindingPoint(0, 0);
     b.RootBlock()->Append(buffer);
 
     auto* func = b.Function("foo", ty.void_());
-    auto* block = func->StartTarget();
-    auto* access = block->Append(b.Access(ty.ptr<storage, i32>(), buffer, 1_u));
-    block->Append(b.Store(access, 42_i));
-    block->Append(b.Return(func));
+
+    auto sb = b.With(func->StartTarget());
+    auto* access = sb.Access(ty.ptr<storage, i32>(), buffer, 1_u);
+    sb.Store(access, 42_i);
+    sb.Return(func);
+
     mod.functions.Push(func);
 
     auto* expect = R"(
@@ -153,12 +157,11 @@
 }
 
 TEST_F(IR_BlockDecoratedStructsTest, RuntimeArray_InStruct) {
-    utils::Vector<const type::StructMember*, 4> members;
-    members.Push(ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 0u, 0u, 4u, 4u,
-                                            type::StructMemberAttributes{}));
-    members.Push(ty.Get<type::StructMember>(mod.symbols.New(), ty.runtime_array(ty.i32()), 1u, 4u,
-                                            4u, 4u, type::StructMemberAttributes{}));
-    auto* structure = ty.Get<type::Struct>(mod.symbols.New(), members, 4u, 8u, 8u);
+    auto* structure =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("i"), ty.i32()},
+                                                   {mod.symbols.New("arr"), ty.array<i32>()},
+                                               });
 
     auto* buffer = b.Var(ty.ptr(storage, structure, builtin::Access::kReadWrite));
     buffer->SetBindingPoint(0, 0);
@@ -167,28 +170,30 @@
     auto* i32_ptr = ty.ptr<storage, i32>();
 
     auto* func = b.Function("foo", ty.void_());
-    auto* block = func->StartTarget();
-    auto* val_ptr = block->Append(b.Access(i32_ptr, buffer, 0_u));
-    auto* load = block->Append(b.Load(val_ptr));
-    auto* elem_ptr = block->Append(b.Access(i32_ptr, buffer, 1_u, 3_u));
-    block->Append(b.Store(elem_ptr, load));
-    block->Append(b.Return(func));
+
+    auto sb = b.With(func->StartTarget());
+    auto* val_ptr = sb.Access(i32_ptr, buffer, 0_u);
+    auto* load = sb.Load(val_ptr);
+    auto* elem_ptr = sb.Access(i32_ptr, buffer, 1_u, 3_u);
+    sb.Store(elem_ptr, load);
+    sb.Return(func);
+
     mod.functions.Push(func);
 
     auto* expect = R"(
-tint_symbol_2 = struct @align(4) {
-  tint_symbol:i32 @offset(0)
-  tint_symbol_1:array<i32> @offset(4)
+MyStruct = struct @align(4) {
+  i:i32 @offset(0)
+  arr:array<i32> @offset(4)
 }
 
-tint_symbol_3 = struct @align(4), @block {
-  tint_symbol:i32 @offset(0)
-  tint_symbol_1:array<i32> @offset(4)
+tint_symbol = struct @align(4), @block {
+  i:i32 @offset(0)
+  arr:array<i32> @offset(4)
 }
 
 # Root block
 %b1 = block {
-  %1:ptr<storage, tint_symbol_3, read_write> = var @binding_point(0, 0)
+  %1:ptr<storage, tint_symbol, read_write> = var @binding_point(0, 0)
 }
 
 %foo = func():void -> %b2 {
@@ -208,19 +213,16 @@
 }
 
 TEST_F(IR_BlockDecoratedStructsTest, StructUsedElsewhere) {
-    utils::Vector<const type::StructMember*, 4> members;
-    members.Push(ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 0u, 0u, 4u, 4u,
-                                            type::StructMemberAttributes{}));
-    members.Push(ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 1u, 4u, 4u, 4u,
-                                            type::StructMemberAttributes{}));
-    auto* structure = ty.Get<type::Struct>(mod.symbols.New(), members, 4u, 8u, 8u);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), ty.i32()},
+                                                                 {mod.symbols.New("b"), ty.i32()},
+                                                             });
 
     auto* buffer = b.Var(ty.ptr(storage, structure, builtin::Access::kReadWrite));
     buffer->SetBindingPoint(0, 0);
     b.RootBlock()->Append(buffer);
 
-    auto* private_var =
-        b.Var(ty.ptr(builtin::AddressSpace::kPrivate, structure, builtin::Access::kReadWrite));
+    auto* private_var = b.Var(ty.ptr<private_, read_write>(structure));
     b.RootBlock()->Append(private_var);
 
     auto* func = b.Function("foo", ty.void_());
@@ -229,24 +231,24 @@
     mod.functions.Push(func);
 
     auto* expect = R"(
-tint_symbol_2 = struct @align(4) {
-  tint_symbol:i32 @offset(0)
-  tint_symbol_1:i32 @offset(4)
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:i32 @offset(4)
 }
 
-tint_symbol_4 = struct @align(4), @block {
-  tint_symbol_3:tint_symbol_2 @offset(0)
+tint_symbol_1 = struct @align(4), @block {
+  tint_symbol:MyStruct @offset(0)
 }
 
 # Root block
 %b1 = block {
-  %1:ptr<storage, tint_symbol_4, read_write> = var @binding_point(0, 0)
-  %2:ptr<private, tint_symbol_2, read_write> = var
+  %1:ptr<storage, tint_symbol_1, read_write> = var @binding_point(0, 0)
+  %2:ptr<private, MyStruct, read_write> = var
 }
 
 %foo = func():void -> %b2 {
   %b2 = block {
-    %4:ptr<storage, tint_symbol_2, read_write> = access %1, 0u
+    %4:ptr<storage, MyStruct, read_write> = access %1, 0u
     store %4, %2
     ret
   }
diff --git a/src/tint/ir/transform/merge_return.cc b/src/tint/ir/transform/merge_return.cc
new file mode 100644
index 0000000..0b95a3a
--- /dev/null
+++ b/src/tint/ir/transform/merge_return.cc
@@ -0,0 +1,298 @@
+// 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/transform/merge_return.h"
+
+#include <utility>
+
+#include "src/tint/ir/builder.h"
+#include "src/tint/ir/module.h"
+#include "src/tint/switch.h"
+#include "src/tint/utils/reverse.h"
+#include "src/tint/utils/transform.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::transform::MergeReturn);
+
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+namespace tint::ir::transform {
+
+MergeReturn::MergeReturn() = default;
+
+MergeReturn::~MergeReturn() = default;
+
+/// PIMPL state for the transform, for a single function.
+struct MergeReturn::State {
+    /// The IR module.
+    Module* ir = nullptr;
+
+    /// The IR builder.
+    Builder b{*ir};
+
+    /// The type manager.
+    type::Manager& ty{ir->Types()};
+
+    /// The "has not returned" flag.
+    Var* continue_execution = nullptr;
+
+    /// The variable that holds the return value.
+    /// Null when the function does not return a value.
+    Var* return_val = nullptr;
+
+    /// The final return at the end of the function block.
+    /// May be null when the function returns in all blocks of a control instruction.
+    Return* fn_return = nullptr;
+
+    /// A set of control instructions that transitively hold a return instruction
+    utils::Hashset<ControlInstruction*, 8> holds_return_;
+
+    /// Constructor
+    /// @param mod the module
+    explicit State(Module* mod) : ir(mod) {}
+
+    /// Process the function.
+    /// @param fn the function to process
+    void Process(Function* fn) {
+        // Find all of the nested return instructions in the function.
+        for (const auto& usage : fn->Usages()) {
+            if (auto* ret = usage.instruction->As<Return>()) {
+                TransitivelyMarkAsReturning(ret->Block()->Parent());
+            }
+        }
+
+        if (holds_return_.IsEmpty()) {
+            // No control instructions hold a return statement, so nothing needs to be done.
+            return;
+        }
+
+        // Create a boolean variable that can be used to check whether the function is returning.
+        continue_execution = b.Var(ty.ptr<function, bool>());
+        continue_execution->SetInitializer(b.Constant(true));
+        fn->StartTarget()->Prepend(continue_execution);
+        ir->SetName(continue_execution, "continue_execution");
+
+        // Create a variable to hold the return value if needed.
+        if (!fn->ReturnType()->Is<type::Void>()) {
+            return_val = b.Var(ty.ptr(function, fn->ReturnType()));
+            fn->StartTarget()->Prepend(return_val);
+            ir->SetName(return_val, "return_value");
+        }
+
+        // Look to see if the function ends with a return
+        fn_return = tint::As<Return>(fn->StartTarget()->Branch());
+
+        // Process the function's block.
+        // This will traverse into control instructions that hold returns, and apply the necessary
+        // changes to remove returns.
+        ProcessBlock(fn->StartTarget());
+
+        // If the function didn't end with a return, add one
+        if (!fn_return) {
+            AppendFinalReturn(fn);
+        }
+
+        // Cleanup - if 'continue_execution' was only ever assigned, remove it.
+        continue_execution->DestroyIfOnlyAssigned();
+    }
+
+    /// Marks all the control instructions from ctrl to the function as holding a return.
+    /// @param ctrl the control instruction to mark as returning, along with all ancestor control
+    /// instructions.
+    void TransitivelyMarkAsReturning(ControlInstruction* ctrl) {
+        for (; ctrl; ctrl = ctrl->Block()->Parent()) {
+            if (!holds_return_.Add(ctrl)) {
+                return;
+            }
+        }
+    }
+
+    /// Walks the instructions of @p block, processing control instructions that are marked as
+    /// holding a return instruction. After processing a control instruction with a return, the
+    /// instructions following the control instruction will be wrapped in a 'if' that only executes
+    /// if a return was not reached.
+    /// @param block the block to process
+    void ProcessBlock(Block* block) {
+        If* inner_if = nullptr;
+        for (auto* inst = *block->begin(); inst;) {  // For each instruction in 'block'
+            // As we're modifying the block that we're iterating over, grab the pointer to the next
+            // instruction before (potentially) moving 'inst' to another block.
+            auto* next = inst->next;
+            TINT_DEFER(inst = next);
+
+            if (auto* ret = inst->As<Return>()) {
+                // Note: Return instructions are processed without being moved into the 'if' block.
+                ProcessReturn(ret, inner_if);
+                break;  // All instructions processed.
+            }
+
+            if (inst->Is<Unreachable>()) {
+                // Unreachable can become reachable once returns are turned into exits.
+                // As this is the terminator for the branch, simply stop processing the
+                // instructions. A appropriate terminator will be created for this block below.
+                inst->Remove();
+                break;
+            }
+
+            // If we've already passed a control instruction holding a return, then we need to move
+            // the instructions that follow the control instruction into the inner-most 'if'.
+            if (inner_if) {
+                inst->Remove();
+                inner_if->True()->Append(inst);
+            }
+
+            // Control instructions holding a return need to be processed, and then a new 'if' needs
+            // to be created to hold the instructions that are between the control instruction and
+            // the block's terminating instruction.
+            if (auto* ctrl = inst->As<ControlInstruction>()) {
+                if (holds_return_.Contains(ctrl)) {
+                    // Control instruction transitively holds a return.
+                    ctrl->ForeachBlock([&](Block* ctrl_block) { ProcessBlock(ctrl_block); });
+                    if (next && next != fn_return && !utils::IsAnyOf<Exit, Unreachable>(next)) {
+                        inner_if = CreateIfContinueExecution(ctrl);
+                    }
+                }
+            }
+        }
+
+        if (inner_if) {
+            // new_value_with_type returns a new RuntimeValue with the same type as 'v'
+            auto new_value_with_type = [&](Value* v) { return b.InstructionResult(v->Type()); };
+
+            if (inner_if->True()->HasBranchTarget()) {
+                if (auto* exit_if = inner_if->True()->Branch()->As<ExitIf>()) {
+                    // Ensure the associated 'if' is updated.
+                    exit_if->SetIf(inner_if);
+
+                    if (!exit_if->Args().IsEmpty()) {
+                        // Inner-most 'if' has a 'exit_if' that returns values.
+                        // These need propagating through the if stack.
+                        inner_if->SetResults(
+                            utils::Transform<8>(exit_if->Args(), new_value_with_type));
+                    }
+                }
+            } else {
+                // Inner-most if doesn't have a terminating instruction. Add an 'exit_if'.
+                inner_if->True()->Append(b.ExitIf(inner_if));
+            }
+
+            // Loop over the 'if' instructions, starting with the inner-most, and add any missing
+            // terminating instructions to the blocks holding the 'if'.
+            for (auto* i = inner_if; i; i = tint::As<If>(i->Block()->Parent())) {
+                if (!i->Block()->HasBranchTarget()) {
+                    // Append the exit instruction to the block holding the 'if'.
+                    utils::Vector<InstructionResult*, 8> exit_args = i->Results();
+                    if (!i->HasResults()) {
+                        i->SetResults(utils::Transform(exit_args, new_value_with_type));
+                    }
+                    auto* exit = b.Exit(i->Block()->Parent(), std::move(exit_args));
+                    i->Block()->Append(exit);
+                }
+            }
+        }
+    }
+
+    /// Transforms a return instruction.
+    /// @param ret the return instruction
+    /// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
+    /// @note unlike other instructions, return instructions are not automatically moved into the
+    /// 'if(continue_execution)' block.
+    void ProcessReturn(Return* ret, If* cond) {
+        if (ret == fn_return) {
+            // 'ret' is the final instruction for the function.
+            ProcessFunctionBlockReturn(ret, cond);
+        } else {
+            // Return is in a nested block
+            ProcessNestedReturn(ret, cond);
+        }
+    }
+
+    /// Transforms the return instruction that is the last instruction in the function's block.
+    /// @param ret the return instruction
+    /// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
+    void ProcessFunctionBlockReturn(Return* ret, If* cond) {
+        if (!return_val) {
+            return;  // No need to transform non-value, end-of-function returns
+        }
+
+        // Assign the return's value to 'return_val' inside a 'if(continue_execution)'
+        if (!cond) {
+            cond = CreateIfContinueExecution(ret->prev);
+        }
+        cond->True()->Append(b.Store(return_val, ret->Value()));
+        cond->True()->Append(b.ExitIf(cond));
+
+        // Change the function return to unconditionally load 'return_val' and return it
+        auto* load = b.Load(return_val);
+        load->InsertBefore(ret);
+        ret->SetValue(load->Result());
+    }
+
+    /// Transforms the return instruction that is found in a control instruction.
+    /// @param ret the return instruction
+    /// @param cond the possibly null 'if(continue_execution)' instruction for the current block.
+    void ProcessNestedReturn(Return* ret, If* cond) {
+        // If we have a 'if(continue_execution)' block, then insert instructions into that,
+        // otherwise insert into the block holding the return.
+        auto* block = cond ? cond->True() : ret->Block();
+
+        // Set the 'continue_execution' flag to false, and store the return value into 'return_val',
+        // if present.
+        block->Append(b.Store(continue_execution, false));
+        if (return_val) {
+            block->Append(b.Store(return_val, ret->Args()[0]));
+        }
+
+        // If the outermost control instruction is expecting exit values, then return them as
+        // 'undef' values.
+        auto* ctrl = block->Parent();
+        utils::Vector<Value*, 8> exit_args;
+        exit_args.Resize(ctrl->Results().Length());
+
+        // Replace the return instruction with an exit instruction.
+        block->Append(b.Exit(ctrl, std::move(exit_args)));
+        ret->Destroy();
+    }
+
+    /// Builds instructions to create a 'if(continue_execution)' conditional.
+    /// @param after new instructions will be inserted after this instruction
+    /// @return the 'If' control instruction
+    If* CreateIfContinueExecution(Instruction* after) {
+        auto* load = b.Load(continue_execution);
+        auto* cond = b.If(load);
+        load->InsertAfter(after);
+        cond->InsertAfter(load);
+        return cond;
+    }
+
+    /// Adds a final return instruction to the end of @p fn
+    /// @param fn the function
+    void AppendFinalReturn(Function* fn) {
+        auto fb = b.With(fn->StartTarget());
+        if (return_val) {
+            fb.Return(fn, fb.Load(return_val));
+        } else {
+            fb.Return(fn);
+        }
+    }
+};
+
+void MergeReturn::Run(Module* ir, const DataMap&, DataMap&) const {
+    // Process each function.
+    for (auto* fn : ir->functions) {
+        State{ir}.Process(fn);
+    }
+}
+
+}  // namespace tint::ir::transform
diff --git a/src/tint/ir/transform/merge_return.h b/src/tint/ir/transform/merge_return.h
new file mode 100644
index 0000000..d127366
--- /dev/null
+++ b/src/tint/ir/transform/merge_return.h
@@ -0,0 +1,40 @@
+// 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_TRANSFORM_MERGE_RETURN_H_
+#define SRC_TINT_IR_TRANSFORM_MERGE_RETURN_H_
+
+#include "src/tint/ir/transform/transform.h"
+
+namespace tint::ir::transform {
+
+/// MergeReturn is a transform merges multiple return statements in a function into a single return
+/// at the end of the function.
+class MergeReturn final : public utils::Castable<MergeReturn, Transform> {
+  public:
+    /// Constructor
+    MergeReturn();
+    /// Destructor
+    ~MergeReturn() override;
+
+    /// @copydoc Transform::Run
+    void Run(ir::Module* module, const DataMap& inputs, DataMap& outputs) const override;
+
+  private:
+    struct State;
+};
+
+}  // namespace tint::ir::transform
+
+#endif  // SRC_TINT_IR_TRANSFORM_MERGE_RETURN_H_
diff --git a/src/tint/ir/transform/merge_return_test.cc b/src/tint/ir/transform/merge_return_test.cc
new file mode 100644
index 0000000..cf2a2cc
--- /dev/null
+++ b/src/tint/ir/transform/merge_return_test.cc
@@ -0,0 +1,2134 @@
+// 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/transform/merge_return.h"
+
+#include <utility>
+
+#include "src/tint/ir/transform/test_helper.h"
+
+namespace tint::ir::transform {
+namespace {
+
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+using IR_MergeReturnTest = TransformTest;
+
+TEST_F(IR_MergeReturnTest, NoModify_SingleReturnInRootBlock) {
+    auto* in = b.FunctionParam(ty.i32());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({in});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+    sb.Return(func, sb.Add(ty.i32(), in, 1_i));
+
+    auto* src = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = add %2, 1i
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, NoModify_SingleReturnInMergeBlock) {
+    auto* in = b.FunctionParam(ty.i32());
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({in});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    ifelse->SetResults(b.InstructionResult(ty.i32()));
+    auto tb = b.With(ifelse->True());
+    tb.ExitIf(ifelse, tb.Add(ty.i32(), in, 1_i));
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse, fb.Add(ty.i32(), in, 2_i));
+
+    sb.Return(func, ifelse->Result(0));
+
+    auto* src = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = if %4 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        %5:i32 = add %2, 1i
+        exit_if %5
+      }
+
+      # False block
+      %b3 = block {
+        %6:i32 = add %2, 2i
+        exit_if %6
+      }
+
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, NoModify_SingleReturnInNestedMergeBlock) {
+    auto* in = b.FunctionParam(ty.i32());
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({in});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* swtch = sb.Switch(in);
+    b.Case(swtch, {Switch::CaseSelector{}})->Append(b.ExitSwitch(swtch));
+
+    sb.Loop();
+
+    auto* ifelse = sb.If(cond);
+    ifelse->SetResults(b.InstructionResult(ty.i32()));
+    auto tb = b.With(ifelse->True());
+    tb.ExitIf(ifelse, tb.Add(ty.i32(), in, 1_i));
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse, fb.Add(ty.i32(), in, 2_i));
+
+    sb.Return(func, ifelse->Result(0));
+
+    auto* src = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    switch %2 [c: (default, %b2)]
+      # Case block
+      %b2 = block {
+        exit_switch
+      }
+
+    loop []
+    %3:i32 = if %4 [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        %5:i32 = add %2, 1i
+        exit_if %5
+      }
+
+      # False block
+      %b4 = block {
+        %6:i32 = add %2, 2i
+        exit_if %6
+      }
+
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_OneSideReturns) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    auto tb = b.With(ifelse->True());
+    tb.Return(func);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse);
+
+    sb.Return(func);
+
+    auto* src = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        exit_if
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+// This is the same as the above tests, but we create the return instructions in a different order
+// to make sure that creation order doesn't matter.
+TEST_F(IR_MergeReturnTest, IfElse_OneSideReturns_ReturnsCreatedInDifferentOrder) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    sb.Return(func);
+
+    auto tb = b.With(ifelse->True());
+    tb.Return(func);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse);
+
+    auto* src = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        exit_if
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_OneSideReturns_WithValue) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    auto tb = b.With(ifelse->True());
+    tb.Return(func, 1_i);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse);
+
+    sb.Return(func, 2_i);
+
+    auto* src = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret 1i
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret 2i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        store %continue_execution, false
+        store %return_value, 1i
+        exit_if
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b4]
+      # True block
+      %b4 = block {
+        store %return_value, 2i
+        exit_if
+      }
+
+    %6:i32 = load %return_value
+    ret %6
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_OneSideReturns_WithValue_MergeHasBasicBlockArguments) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    ifelse->SetResults(b.InstructionResult(ty.i32()));
+    auto tb = b.With(ifelse->True());
+    tb.Return(func, 1_i);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse, 2_i);
+
+    sb.Return(func, ifelse->Result(0));
+
+    auto* src = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret 1i
+      }
+
+      # False block
+      %b3 = block {
+        exit_if 2i
+      }
+
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    %5:i32 = if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        store %continue_execution, false
+        store %return_value, 1i
+        exit_if undef
+      }
+
+      # False block
+      %b3 = block {
+        exit_if 2i
+      }
+
+    %6:bool = load %continue_execution
+    if %6 [t: %b4]
+      # True block
+      %b4 = block {
+        store %return_value, %5
+        exit_if
+      }
+
+    %7:i32 = load %return_value
+    ret %7
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_OneSideReturns_WithValue_MergeHasUndefBasicBlockArguments) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    ifelse->SetResults(b.InstructionResult(ty.i32()));
+    auto tb = b.With(ifelse->True());
+    tb.Return(func, 1_i);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse, b.Constant(8_i));
+
+    sb.Return(func, ifelse->Result(0));
+
+    auto* src = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret 1i
+      }
+
+      # False block
+      %b3 = block {
+        exit_if 8i
+      }
+
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    %5:i32 = if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        store %continue_execution, false
+        store %return_value, 1i
+        exit_if undef
+      }
+
+      # False block
+      %b3 = block {
+        exit_if 8i
+      }
+
+    %6:bool = load %continue_execution
+    if %6 [t: %b4]
+      # True block
+      %b4 = block {
+        store %return_value, %5
+        exit_if
+      }
+
+    %7:i32 = load %return_value
+    ret %7
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_BothSidesReturn) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    auto tb = b.With(ifelse->True());
+    tb.Return(func);
+    auto fb = b.With(ifelse->False());
+    fb.Return(func);
+
+    sb.Unreachable();
+
+    auto* src = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        ret
+      }
+
+      # False block
+      %b3 = block {
+        ret
+      }
+
+    unreachable
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    if %2 [t: %b2, f: %b3]
+      # True block
+      %b2 = block {
+        exit_if
+      }
+
+      # False block
+      %b3 = block {
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_ThenStatements) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    auto tb = b.With(ifelse->True());
+    tb.Return(func);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse);
+
+    sb.Store(global, 42_i);
+    sb.Return(func);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):void -> %b2 {
+  %b2 = block {
+    if %3 [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        ret
+      }
+
+      # False block
+      %b4 = block {
+        exit_if
+      }
+
+    store %1, 42i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):void -> %b2 {
+  %b2 = block {
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if %3 [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        store %continue_execution, false
+        exit_if
+      }
+
+      # False block
+      %b4 = block {
+        exit_if
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b5]
+      # True block
+      %b5 = block {
+        store %1, 42i
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+// This is the same as the above tests, but we create the return instructions in a different order
+// to make sure that creation order doesn't matter.
+TEST_F(IR_MergeReturnTest, IfElse_ThenStatements_ReturnsCreatedInDifferentOrder) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse = sb.If(cond);
+    sb.Store(global, 42_i);
+    sb.Return(func);
+
+    auto tb = b.With(ifelse->True());
+    tb.Return(func);
+    auto fb = b.With(ifelse->False());
+    fb.ExitIf(ifelse);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):void -> %b2 {
+  %b2 = block {
+    if %3 [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        ret
+      }
+
+      # False block
+      %b4 = block {
+        exit_if
+      }
+
+    store %1, 42i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):void -> %b2 {
+  %b2 = block {
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if %3 [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        store %continue_execution, false
+        exit_if
+      }
+
+      # False block
+      %b4 = block {
+        exit_if
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b5]
+      # True block
+      %b5 = block {
+        store %1, 42i
+        exit_if
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_Nested) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* func = b.Function("foo", ty.i32());
+    auto* condA = b.FunctionParam(ty.bool_());
+    auto* condB = b.FunctionParam(ty.bool_());
+    auto* condC = b.FunctionParam(ty.bool_());
+    mod.SetName(condA, "condA");
+    mod.SetName(condB, "condB");
+    mod.SetName(condC, "condC");
+    func->SetParams({condA, condB, condC});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse_outer = sb.If(condA);
+    auto outer_true = b.With(ifelse_outer->True());
+    outer_true.Return(func, 3_i);
+    auto outer_false = b.With(ifelse_outer->False());
+    auto* ifelse_middle = outer_false.If(condB);
+
+    sb.Store(global, 3_i);
+    sb.Return(func, sb.Add(ty.i32(), 5_i, 6_i));
+
+    auto middle_true = b.With(ifelse_middle->True());
+    auto* ifelse_inner = middle_true.If(condC);
+    auto middle_false = b.With(ifelse_middle->False());
+    middle_false.ExitIf(ifelse_middle);
+
+    outer_false.Store(global, 2_i);
+    outer_false.ExitIf(ifelse_outer);
+
+    auto inner_true = b.With(ifelse_inner->True());
+    inner_true.Return(func, 1_i);
+    auto inner_false = b.With(ifelse_inner->False());
+    inner_false.ExitIf(ifelse_inner);
+
+    middle_true.Store(global, 1_i);
+    middle_true.Return(func, 2_i);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        ret 3i
+      }
+
+      # False block
+      %b4 = block {
+        if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                ret 1i
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            store %1, 1i
+            ret 2i
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        store %1, 2i
+        exit_if
+      }
+
+    store %1, 3i
+    %6:i32 = add 5i, 6i
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        store %continue_execution, false
+        store %return_value, 3i
+        exit_if
+      }
+
+      # False block
+      %b4 = block {
+        if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                store %continue_execution, false
+                store %return_value, 1i
+                exit_if
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            %8:bool = load %continue_execution
+            if %8 [t: %b9]
+              # True block
+              %b9 = block {
+                store %1, 1i
+                store %continue_execution, false
+                store %return_value, 2i
+                exit_if
+              }
+
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        %9:bool = load %continue_execution
+        if %9 [t: %b10]
+          # True block
+          %b10 = block {
+            store %1, 2i
+            exit_if
+          }
+
+        exit_if
+      }
+
+    %10:bool = load %continue_execution
+    if %10 [t: %b11]
+      # True block
+      %b11 = block {
+        store %1, 3i
+        %11:i32 = add 5i, 6i
+        store %return_value, %11
+        exit_if
+      }
+
+    %12:i32 = load %return_value
+    ret %12
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_Nested_TrivialMerge) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* func = b.Function("foo", ty.i32());
+    auto* condA = b.FunctionParam(ty.bool_());
+    auto* condB = b.FunctionParam(ty.bool_());
+    auto* condC = b.FunctionParam(ty.bool_());
+    mod.SetName(condA, "condA");
+    mod.SetName(condB, "condB");
+    mod.SetName(condC, "condC");
+    func->SetParams({condA, condB, condC});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse_outer = sb.If(condA);
+    auto outer_true = b.With(ifelse_outer->True());
+    outer_true.Return(func, 3_i);
+    auto outer_false = b.With(ifelse_outer->False());
+    auto* ifelse_middle = outer_false.If(condB);
+
+    sb.Return(func, 3_i);
+
+    auto middle_true = b.With(ifelse_middle->True());
+    auto* ifelse_inner = middle_true.If(condC);
+    auto middle_false = b.With(ifelse_middle->False());
+    middle_false.ExitIf(ifelse_middle);
+
+    outer_false.ExitIf(ifelse_outer);
+
+    auto inner_true = b.With(ifelse_inner->True());
+    inner_true.Return(func, 1_i);
+    auto inner_false = b.With(ifelse_inner->False());
+    inner_false.ExitIf(ifelse_inner);
+
+    middle_true.ExitIf(ifelse_middle);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        ret 3i
+      }
+
+      # False block
+      %b4 = block {
+        if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                ret 1i
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        exit_if
+      }
+
+    ret 3i
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        store %continue_execution, false
+        store %return_value, 3i
+        exit_if
+      }
+
+      # False block
+      %b4 = block {
+        if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                store %continue_execution, false
+                store %return_value, 1i
+                exit_if
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        exit_if
+      }
+
+    %8:bool = load %continue_execution
+    if %8 [t: %b9]
+      # True block
+      %b9 = block {
+        store %return_value, 3i
+        exit_if
+      }
+
+    %9:i32 = load %return_value
+    ret %9
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, IfElse_Nested_WithBasicBlockArguments) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* func = b.Function("foo", ty.i32());
+    auto* condA = b.FunctionParam(ty.bool_());
+    auto* condB = b.FunctionParam(ty.bool_());
+    auto* condC = b.FunctionParam(ty.bool_());
+    mod.SetName(condA, "condA");
+    mod.SetName(condB, "condB");
+    mod.SetName(condC, "condC");
+    func->SetParams({condA, condB, condC});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* ifelse_outer = sb.If(condA);
+    ifelse_outer->SetResults(b.InstructionResult(ty.i32()));
+    auto outer_true = b.With(ifelse_outer->True());
+    outer_true.Return(func, 3_i);
+    auto outer_false = b.With(ifelse_outer->False());
+    auto* ifelse_middle = outer_false.If(condB);
+    ifelse_middle->SetResults(b.InstructionResult(ty.i32()));
+
+    sb.Return(func, sb.Add(ty.i32(), ifelse_outer->Result(0), 1_i));
+
+    auto middle_true = b.With(ifelse_middle->True());
+    auto* ifelse_inner = middle_true.If(condC);
+    auto middle_false = b.With(ifelse_middle->False());
+    middle_false.ExitIf(ifelse_middle, middle_false.Add(ty.i32(), 43_i, 2_i));
+
+    outer_false.ExitIf(ifelse_outer, outer_false.Add(ty.i32(), ifelse_middle->Result(0), 1_i));
+
+    auto inner_true = b.With(ifelse_inner->True());
+    inner_true.Return(func, 1_i);
+    auto inner_false = b.With(ifelse_inner->False());
+    inner_false.ExitIf(ifelse_inner);
+
+    middle_true.ExitIf(ifelse_middle, middle_true.Add(ty.i32(), 42_i, 1_i));
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        ret 3i
+      }
+
+      # False block
+      %b4 = block {
+        %7:i32 = if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                ret 1i
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            %8:i32 = add 42i, 1i
+            exit_if %8
+          }
+
+          # False block
+          %b6 = block {
+            %9:i32 = add 43i, 2i
+            exit_if %9
+          }
+
+        %10:i32 = add %7, 1i
+        exit_if %10
+      }
+
+    %11:i32 = add %6, 1i
+    ret %11
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%condA:bool, %condB:bool, %condC:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    %8:i32 = if %condA [t: %b3, f: %b4]
+      # True block
+      %b3 = block {
+        store %continue_execution, false
+        store %return_value, 3i
+        exit_if undef
+      }
+
+      # False block
+      %b4 = block {
+        %9:i32 = if %condB [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            if %condC [t: %b7, f: %b8]
+              # True block
+              %b7 = block {
+                store %continue_execution, false
+                store %return_value, 1i
+                exit_if
+              }
+
+              # False block
+              %b8 = block {
+                exit_if
+              }
+
+            %10:bool = load %continue_execution
+            %11:i32 = if %10 [t: %b9]
+              # True block
+              %b9 = block {
+                %12:i32 = add 42i, 1i
+                exit_if %12
+              }
+
+            exit_if %11
+          }
+
+          # False block
+          %b6 = block {
+            %13:i32 = add 43i, 2i
+            exit_if %13
+          }
+
+        %14:bool = load %continue_execution
+        %15:i32 = if %14 [t: %b10]
+          # True block
+          %b10 = block {
+            %16:i32 = add %9, 1i
+            exit_if %16
+          }
+
+        exit_if %15
+      }
+
+    %17:bool = load %continue_execution
+    if %17 [t: %b11]
+      # True block
+      %b11 = block {
+        %18:i32 = add %8, 1i
+        store %return_value, %18
+        exit_if
+      }
+
+    %19:i32 = load %return_value
+    ret %19
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Loop_UnconditionalReturnInBody) {
+    auto* func = b.Function("foo", ty.i32());
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* loop = sb.Loop();
+    loop->Body()->Append(b.Return(func, 42_i));
+
+    sb.Unreachable();
+
+    auto* src = R"(
+%foo = func():i32 -> %b1 {
+  %b1 = block {
+    loop [b: %b2]
+      # Body block
+      %b2 = block {
+        ret 42i
+      }
+
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    loop [b: %b2]
+      # Body block
+      %b2 = block {
+        store %return_value, 42i
+        exit_loop
+      }
+
+    %3:i32 = load %return_value
+    ret %3
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Loop_ConditionalReturnInBody) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* loop = sb.Loop();
+    auto lb = b.With(loop->Body());
+    auto* ifelse = lb.If(cond);
+    {
+        auto tb = b.With(ifelse->True());
+        tb.Return(func, 42_i);
+        auto fb = b.With(ifelse->False());
+        fb.ExitIf(ifelse);
+    }
+    lb.Store(global, 2_i);
+    lb.Continue(loop);
+
+    auto cb = b.With(loop->Continuing());
+    cb.Store(global, 1_i);
+    cb.BreakIf(true, loop);
+
+    sb.Store(global, 3_i);
+    sb.Return(func, 43_i);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            ret 42i
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        store %1, 2i
+        continue %b4
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        break_if true %b3
+      }
+
+    store %1, 3i
+    ret 43i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            store %continue_execution, false
+            store %return_value, 42i
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        %6:bool = load %continue_execution
+        if %6 [t: %b7]
+          # True block
+          %b7 = block {
+            store %1, 2i
+            continue %b4
+          }
+
+        exit_loop
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        break_if true %b3
+      }
+
+    %7:bool = load %continue_execution
+    if %7 [t: %b8]
+      # True block
+      %b8 = block {
+        store %1, 3i
+        store %return_value, 43i
+        exit_if
+      }
+
+    %8:i32 = load %return_value
+    ret %8
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Loop_ConditionalReturnInBody_UnreachableMerge) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* loop = sb.Loop();
+    auto lb = b.With(loop->Body());
+    auto* ifelse = lb.If(cond);
+    {
+        auto tb = b.With(ifelse->True());
+        tb.Return(func, 42_i);
+        auto fb = b.With(ifelse->False());
+        fb.ExitIf(ifelse);
+    }
+    lb.Store(global, 2_i);
+    lb.Continue(loop);
+
+    auto cb = b.With(loop->Continuing());
+    cb.Store(global, 1_i);
+    cb.NextIteration(loop);
+
+    sb.Unreachable();
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            ret 42i
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        store %1, 2i
+        continue %b4
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        next_iteration %b3
+      }
+
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            store %continue_execution, false
+            store %return_value, 42i
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        %6:bool = load %continue_execution
+        if %6 [t: %b7]
+          # True block
+          %b7 = block {
+            store %1, 2i
+            continue %b4
+          }
+
+        exit_loop
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        next_iteration %b3
+      }
+
+    %7:i32 = load %return_value
+    ret %7
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Loop_WithBasicBlockArgumentsOnMerge) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* loop = sb.Loop();
+    loop->SetResults(b.InstructionResult(ty.i32()));
+    auto lb = b.With(loop->Body());
+    auto* ifelse = lb.If(cond);
+    {
+        auto tb = b.With(ifelse->True());
+        tb.Return(func, 42_i);
+        auto fb = b.With(ifelse->False());
+        fb.ExitIf(ifelse);
+    }
+    lb.Store(global, 2_i);
+    lb.Continue(loop);
+
+    auto cb = b.With(loop->Continuing());
+    cb.Store(global, 1_i);
+    cb.BreakIf(true, loop, 4_i);
+
+    sb.Store(global, 3_i);
+    sb.Return(func, loop->Result(0));
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            ret 42i
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        store %1, 2i
+        continue %b4
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        break_if true %b3 4i
+      }
+
+    store %1, 3i
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:bool):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    loop [b: %b3, c: %b4]
+      # Body block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            store %continue_execution, false
+            store %return_value, 42i
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        %6:bool = load %continue_execution
+        if %6 [t: %b7]
+          # True block
+          %b7 = block {
+            store %1, 2i
+            continue %b4
+          }
+
+        exit_loop
+      }
+
+      # Continuing block
+      %b4 = block {
+        store %1, 1i
+        break_if true %b3 4i
+      }
+
+    %7:bool = load %continue_execution
+    if %7 [t: %b8]
+      # True block
+      %b8 = block {
+        store %1, 3i
+        store %return_value, %8
+        exit_if
+      }
+
+    %9:i32 = load %return_value
+    ret %9
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Switch_UnconditionalReturnInCase) {
+    auto* cond = b.FunctionParam(ty.i32());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* sw = sb.Switch(cond);
+    auto caseA = b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(1_i)}}));
+    caseA.Return(func, 42_i);
+    auto caseB = b.With(b.Case(sw, {Switch::CaseSelector{}}));
+    caseB.ExitSwitch(sw);
+
+    sb.Return(func, 0_i);
+
+    auto* src = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    switch %2 [c: (1i, %b2), c: (default, %b3)]
+      # Case block
+      %b2 = block {
+        ret 42i
+      }
+
+      # Case block
+      %b3 = block {
+        exit_switch
+      }
+
+    ret 0i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    switch %2 [c: (1i, %b2), c: (default, %b3)]
+      # Case block
+      %b2 = block {
+        store %continue_execution, false
+        store %return_value, 42i
+        exit_switch
+      }
+
+      # Case block
+      %b3 = block {
+        exit_switch
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b4]
+      # True block
+      %b4 = block {
+        store %return_value, 0i
+        exit_if
+      }
+
+    %6:i32 = load %return_value
+    ret %6
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Switch_ConditionalReturnInBody) {
+    auto* global = b.Var(ty.ptr<private_, i32>());
+    b.RootBlock()->Append(global);
+
+    auto* cond = b.FunctionParam(ty.i32());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* sw = sb.Switch(cond);
+    auto caseA = b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(1_i)}}));
+    auto* ifelse = caseA.If(cond);
+    {
+        auto tb = b.With(ifelse->True());
+        tb.Return(func, 42_i);
+        auto fb = b.With(ifelse->False());
+        fb.ExitIf(ifelse);
+    }
+    caseA.Store(global, 2_i);
+    caseA.ExitSwitch(sw);
+
+    auto caseB = b.With(b.Case(sw, {Switch::CaseSelector{}}));
+    caseB.ExitSwitch(sw);
+
+    sb.Return(func, 0_i);
+
+    auto* src = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:i32):i32 -> %b2 {
+  %b2 = block {
+    switch %3 [c: (1i, %b3), c: (default, %b4)]
+      # Case block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            ret 42i
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        store %1, 2i
+        exit_switch
+      }
+
+      # Case block
+      %b4 = block {
+        exit_switch
+      }
+
+    ret 0i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+# Root block
+%b1 = block {
+  %1:ptr<private, i32, read_write> = var
+}
+
+%foo = func(%3:i32):i32 -> %b2 {
+  %b2 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    switch %3 [c: (1i, %b3), c: (default, %b4)]
+      # Case block
+      %b3 = block {
+        if %3 [t: %b5, f: %b6]
+          # True block
+          %b5 = block {
+            store %continue_execution, false
+            store %return_value, 42i
+            exit_if
+          }
+
+          # False block
+          %b6 = block {
+            exit_if
+          }
+
+        %6:bool = load %continue_execution
+        if %6 [t: %b7]
+          # True block
+          %b7 = block {
+            store %1, 2i
+            exit_switch
+          }
+
+        exit_switch
+      }
+
+      # Case block
+      %b4 = block {
+        exit_switch
+      }
+
+    %7:bool = load %continue_execution
+    if %7 [t: %b8]
+      # True block
+      %b8 = block {
+        store %return_value, 0i
+        exit_if
+      }
+
+    %8:i32 = load %return_value
+    ret %8
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, Switch_WithBasicBlockArgumentsOnMerge) {
+    auto* cond = b.FunctionParam(ty.i32());
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({cond});
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+
+    auto* sw = sb.Switch(cond);
+    sw->SetResults(b.InstructionResult(ty.i32()));  // NOLINT: false detection of std::tuple
+    auto caseA = b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(1_i)}}));
+    caseA.Return(func, 42_i);
+    auto caseB = b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(2_i)}}));
+    caseB.Return(func, 99_i);
+    auto caseC = b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(3_i)}}));
+    caseC.ExitSwitch(sw, 1_i);
+    auto caseD = b.With(b.Case(sw, {Switch::CaseSelector{}}));
+    caseD.ExitSwitch(sw, 0_i);
+
+    sb.Return(func, sw->Result(0));
+
+    auto* src = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    switch %2 [c: (1i, %b2), c: (2i, %b3), c: (3i, %b4), c: (default, %b5)]
+      # Case block
+      %b2 = block {
+        ret 42i
+      }
+
+      # Case block
+      %b3 = block {
+        ret 99i
+      }
+
+      # Case block
+      %b4 = block {
+        exit_switch 1i
+      }
+
+      # Case block
+      %b5 = block {
+        exit_switch 0i
+      }
+
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    switch %2 [c: (1i, %b2), c: (2i, %b3), c: (3i, %b4), c: (default, %b5)]
+      # Case block
+      %b2 = block {
+        store %continue_execution, false
+        store %return_value, 42i
+        exit_switch undef
+      }
+
+      # Case block
+      %b3 = block {
+        store %continue_execution, false
+        store %return_value, 99i
+        exit_switch undef
+      }
+
+      # Case block
+      %b4 = block {
+        exit_switch 1i
+      }
+
+      # Case block
+      %b5 = block {
+        exit_switch 0i
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b6]
+      # True block
+      %b6 = block {
+        store %return_value, %6
+        exit_if
+      }
+
+    %7:i32 = load %return_value
+    ret %7
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, LoopIfReturnThenContinue) {
+    auto* func = b.Function("foo", ty.void_());
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+    {
+        auto* loop = sb.Loop();
+        auto lb = sb.With(loop->Body());
+        {
+            auto ib = lb.With(lb.If(true)->True());
+            ib.Return(func);
+        }
+        lb.Continue(loop);
+    }
+    sb.Unreachable();
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    loop [b: %b2]
+      # Body block
+      %b2 = block {
+        if true [t: %b3]
+          # True block
+          %b3 = block {
+            ret
+          }
+
+        continue %b4
+      }
+
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    loop [b: %b2]
+      # Body block
+      %b2 = block {
+        if true [t: %b3]
+          # True block
+          %b3 = block {
+            store %continue_execution, false
+            exit_if
+          }
+
+        %3:bool = load %continue_execution
+        if %3 [t: %b4]
+          # True block
+          %b4 = block {
+            continue %b5
+          }
+
+        exit_loop
+      }
+
+    ret
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_MergeReturnTest, NestedIfsWithReturns) {
+    auto* func = b.Function("foo", ty.i32());
+    mod.functions.Push(func);
+
+    auto sb = b.With(func->StartTarget());
+    {
+        auto outer = b.With(sb.If(true)->True());
+        {
+            auto inner = b.With(outer.If(true)->True());
+            inner.Return(func, 1_i);
+        }
+        outer.Return(func, 2_i);
+    }
+    sb.Return(func, 3_i);
+
+    auto* src = R"(
+%foo = func():i32 -> %b1 {
+  %b1 = block {
+    if true [t: %b2]
+      # True block
+      %b2 = block {
+        if true [t: %b3]
+          # True block
+          %b3 = block {
+            ret 1i
+          }
+
+        ret 2i
+      }
+
+    ret 3i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():i32 -> %b1 {
+  %b1 = block {
+    %return_value:ptr<function, i32, read_write> = var
+    %continue_execution:ptr<function, bool, read_write> = var, true
+    if true [t: %b2]
+      # True block
+      %b2 = block {
+        if true [t: %b3]
+          # True block
+          %b3 = block {
+            store %continue_execution, false
+            store %return_value, 1i
+            exit_if
+          }
+
+        %4:bool = load %continue_execution
+        if %4 [t: %b4]
+          # True block
+          %b4 = block {
+            store %continue_execution, false
+            store %return_value, 2i
+            exit_if
+          }
+
+        exit_if
+      }
+
+    %5:bool = load %continue_execution
+    if %5 [t: %b5]
+      # True block
+      %b5 = block {
+        store %return_value, 3i
+        exit_if
+      }
+
+    %6:i32 = load %return_value
+    ret %6
+  }
+}
+)";
+
+    Run<MergeReturn>();
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::ir::transform
diff --git a/src/tint/ir/transform/test_helper.h b/src/tint/ir/transform/test_helper.h
index ea75fcf..5cb6392 100644
--- a/src/tint/ir/transform/test_helper.h
+++ b/src/tint/ir/transform/test_helper.h
@@ -24,6 +24,7 @@
 #include "src/tint/ir/builder.h"
 #include "src/tint/ir/disassembler.h"
 #include "src/tint/ir/transform/transform.h"
+#include "src/tint/ir/validate.h"
 #include "src/tint/transform/manager.h"
 
 namespace tint::ir::transform {
@@ -39,10 +40,28 @@
     Transform::DataMap Run(const Transform::DataMap& data = {}) {
         tint::transform::Manager manager;
         tint::transform::DataMap outputs;
+
+        // Validate the input IR.
+        {
+            auto res = ir::Validate(mod);
+            EXPECT_TRUE(res) << res.Failure().str();
+            if (!res) {
+                return outputs;
+            }
+        }
+
+        // Run the transforms.
         for (auto* transform_ptr : std::initializer_list<Transform*>{new TRANSFORMS()...}) {
             manager.append(std::unique_ptr<Transform>(transform_ptr));
         }
         manager.Run(&mod, data, outputs);
+
+        // Validate the output IR.
+        {
+            auto res = ir::Validate(mod);
+            EXPECT_TRUE(res) << res.Failure().str();
+        }
+
         return outputs;
     }
 
@@ -59,18 +78,6 @@
     ir::Builder b{mod};
     /// The type manager.
     type::Manager& ty{mod.Types()};
-
-    /// Alias to builtin::AddressSpace::kStorage
-    static constexpr auto storage = builtin::AddressSpace::kStorage;
-    /// Alias to builtin::AddressSpace::kUniform
-    static constexpr auto uniform = builtin::AddressSpace::kUniform;
-    /// Alias to builtin::AddressSpace::kPrivate
-    static constexpr auto private_ = builtin::AddressSpace::kPrivate;
-    /// Alias to builtin::AddressSpace::kFunction
-    static constexpr auto function = builtin::AddressSpace::kFunction;
-
-  private:
-    std::vector<std::unique_ptr<Source::File>> files_;
 };
 
 using TransformTest = TransformTestBase<testing::Test>;
diff --git a/src/tint/ir/transform/transform.h b/src/tint/ir/transform/transform.h
index a09fe77..e95a7b7 100644
--- a/src/tint/ir/transform/transform.h
+++ b/src/tint/ir/transform/transform.h
@@ -19,6 +19,7 @@
 
 #include <utility>
 
+#include "src/tint/builtin/address_space.h"
 #include "src/tint/utils/castable.h"
 
 // Forward declarations
diff --git a/src/tint/ir/transform/var_for_dynamic_index.cc b/src/tint/ir/transform/var_for_dynamic_index.cc
index b72dc34..27ddec4 100644
--- a/src/tint/ir/transform/var_for_dynamic_index.cc
+++ b/src/tint/ir/transform/var_for_dynamic_index.cc
@@ -63,7 +63,7 @@
 };
 
 std::optional<AccessToReplace> ShouldReplace(Access* access) {
-    if (access->Type()->Is<type::Pointer>()) {
+    if (access->Result()->Type()->Is<type::Pointer>()) {
         // No need to modify accesses into pointer types.
         return {};
     }
@@ -101,7 +101,7 @@
 
     // Find the access instructions that need replacing.
     utils::Vector<AccessToReplace, 4> worklist;
-    for (auto* inst : ir->values.Objects()) {
+    for (auto* inst : ir->instructions.Objects()) {
         if (auto* access = inst->As<Access>()) {
             if (auto to_replace = ShouldReplace(access)) {
                 worklist.Push(to_replace.value());
@@ -125,7 +125,7 @@
                 auto* intermediate_source = builder.Access(to_replace.dynamic_index_source_type,
                                                            source_object, partial_access.indices);
                 intermediate_source->InsertBefore(access);
-                return intermediate_source;
+                return intermediate_source->Result();
             });
         }
 
@@ -136,14 +136,14 @@
                                             builtin::Access::kReadWrite));
             decl->SetInitializer(source_object);
             decl->InsertBefore(access);
-            return decl;
+            return decl->Result();
         });
 
         // Create a new access instruction using the local variable as the source.
         utils::Vector<Value*, 4> indices{access->Indices().Offset(to_replace.first_dynamic_index)};
         auto* new_access =
-            builder.Access(ir->Types().ptr(builtin::AddressSpace::kFunction, access->Type(),
-                                           builtin::Access::kReadWrite),
+            builder.Access(ir->Types().ptr(builtin::AddressSpace::kFunction,
+                                           access->Result()->Type(), builtin::Access::kReadWrite),
                            local, indices);
         access->ReplaceWith(new_access);
 
@@ -152,7 +152,7 @@
         load->InsertAfter(new_access);
 
         // Replace all uses of the old access instruction with the loaded result.
-        access->ReplaceAllUsesWith([&](Usage) { return load; });
+        access->Result()->ReplaceAllUsesWith([&](Usage) { return load->Result(); });
     }
 }
 
diff --git a/src/tint/ir/transform/var_for_dynamic_index_test.cc b/src/tint/ir/transform/var_for_dynamic_index_test.cc
index 6b64025..785927d 100644
--- a/src/tint/ir/transform/var_for_dynamic_index_test.cc
+++ b/src/tint/ir/transform/var_for_dynamic_index_test.cc
@@ -24,14 +24,10 @@
 namespace tint::ir::transform {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
-class IR_VarForDynamicIndexTest : public TransformTest {
-  protected:
-    const type::Type* ptr(const type::Type* elem) {
-        return ty.ptr(builtin::AddressSpace::kFunction, elem, builtin::Access::kReadWrite);
-    }
-};
+using IR_VarForDynamicIndexTest = TransformTest;
 
 TEST_F(IR_VarForDynamicIndexTest, NoModify_ConstantIndex_ArrayValue) {
     auto* arr = b.FunctionParam(ty.array<i32, 4u>());
@@ -82,13 +78,13 @@
 }
 
 TEST_F(IR_VarForDynamicIndexTest, NoModify_DynamicIndex_ArrayPointer) {
-    auto* arr = b.FunctionParam(ptr(ty.array<i32, 4u>()));
+    auto* arr = b.FunctionParam(ty.ptr<function, array<i32, 4u>>());
     auto* idx = b.FunctionParam(ty.i32());
     auto* func = b.Function("foo", ty.i32());
     func->SetParams({arr, idx});
 
     auto* block = func->StartTarget();
-    auto* access = block->Append(b.Access(ptr(ty.i32()), arr, idx));
+    auto* access = block->Append(b.Access(ty.ptr<function, i32>(), arr, idx));
     auto* load = block->Append(b.Load(access));
     block->Append(b.Return(func, load));
     mod.functions.Push(func);
@@ -109,13 +105,13 @@
 }
 
 TEST_F(IR_VarForDynamicIndexTest, NoModify_DynamicIndex_MatrixPointer) {
-    auto* mat = b.FunctionParam(ptr(ty.mat2x2<f32>()));
+    auto* mat = b.FunctionParam(ty.ptr<function, mat2x2<f32>>());
     auto* idx = b.FunctionParam(ty.i32());
     auto* func = b.Function("foo", ty.f32());
     func->SetParams({mat, idx});
 
     auto* block = func->StartTarget();
-    auto* access = block->Append(b.Access(ptr(ty.f32()), mat, idx, idx));
+    auto* access = block->Append(b.Access(ty.ptr<function, f32>(), mat, idx, idx));
     auto* load = block->Append(b.Load(access));
     block->Append(b.Return(func, load));
     mod.functions.Push(func);
@@ -188,22 +184,22 @@
 }
 
 TEST_F(IR_VarForDynamicIndexTest, DynamicIndex_MatrixValue) {
-    auto* arr = b.FunctionParam(ty.mat2x2<f32>());
+    auto* mat = b.FunctionParam(ty.mat2x2<f32>());
     auto* idx = b.FunctionParam(ty.i32());
-    auto* func = b.Function("foo", ty.f32());
-    func->SetParams({arr, idx});
+    auto* func = b.Function("foo", ty.vec2<f32>());
+    func->SetParams({mat, idx});
 
     auto* block = func->StartTarget();
-    auto* access = block->Append(b.Access(ty.f32(), arr, idx));
+    auto* access = block->Append(b.Access(ty.vec2<f32>(), mat, idx));
     block->Append(b.Return(func, access));
     mod.functions.Push(func);
 
     auto* expect = R"(
-%foo = func(%2:mat2x2<f32>, %3:i32):f32 -> %b1 {
+%foo = func(%2:mat2x2<f32>, %3:i32):vec2<f32> -> %b1 {
   %b1 = block {
     %4:ptr<function, mat2x2<f32>, read_write> = var, %2
-    %5:ptr<function, f32, read_write> = access %4, %3
-    %6:f32 = load %5
+    %5:ptr<function, vec2<f32>, read_write> = access %4, %3
+    %6:vec2<f32> = load %5
     ret %6
   }
 }
@@ -298,17 +294,12 @@
 }
 
 TEST_F(IR_VarForDynamicIndexTest, AccessChain_SkipConstantIndices_Struct) {
-    auto* str_ty = ty.Get<type::Struct>(
-        mod.symbols.Register("MyStruct"),
-        utils::Vector{
-            ty.Get<type::StructMember>(mod.symbols.Register("arr1"), ty.array<f32, 1024>(), 0u, 0u,
-                                       4u, 4096u, type::StructMemberAttributes{}),
-            ty.Get<type::StructMember>(mod.symbols.Register("mat"), ty.mat4x4<f32>(), 1u, 4096u,
-                                       16u, 64u, type::StructMemberAttributes{}),
-            ty.Get<type::StructMember>(mod.symbols.Register("arr2"), ty.array<f32, 1024>(), 2u,
-                                       4160u, 4u, 4096u, type::StructMemberAttributes{}),
-        },
-        16u, 32u, 32u);
+    auto* str_ty = ty.Struct(mod.symbols.Register("MyStruct"),
+                             {
+                                 {mod.symbols.Register("arr1"), ty.array<f32, 1024>()},
+                                 {mod.symbols.Register("mat"), ty.mat4x4<f32>()},
+                                 {mod.symbols.Register("arr2"), ty.array<f32, 1024>()},
+                             });
     auto* str_val = b.FunctionParam(str_ty);
     auto* idx = b.FunctionParam(ty.i32());
     auto* func = b.Function("foo", ty.f32());
diff --git a/src/tint/ir/unary.cc b/src/tint/ir/unary.cc
index 90509e0..f5952ea 100644
--- a/src/tint/ir/unary.cc
+++ b/src/tint/ir/unary.cc
@@ -19,11 +19,9 @@
 
 namespace tint::ir {
 
-Unary::Unary(enum Kind k, const type::Type* res_ty, Value* val) : kind_(k), result_type_(res_ty) {
-    TINT_ASSERT(IR, val != nullptr);
-    TINT_ASSERT(IR, result_type_ != nullptr);
-
-    AddOperand(val);
+Unary::Unary(InstructionResult* result, enum Kind k, Value* val) : kind_(k) {
+    AddOperand(Unary::kValueOperandOffset, val);
+    AddResult(result);
 }
 
 Unary::~Unary() = default;
diff --git a/src/tint/ir/unary.h b/src/tint/ir/unary.h
index baf1218..3f09a3e 100644
--- a/src/tint/ir/unary.h
+++ b/src/tint/ir/unary.h
@@ -21,8 +21,11 @@
 namespace tint::ir {
 
 /// A unary instruction in the IR.
-class Unary : public utils::Castable<Unary, OperandInstruction<1>> {
+class Unary : public utils::Castable<Unary, OperandInstruction<1, 1>> {
   public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
     /// The kind of instruction.
     enum class Kind {
         kComplement,
@@ -30,24 +33,20 @@
     };
 
     /// Constructor
+    /// @param result the result value
     /// @param kind the kind of unary instruction
-    /// @param result_type the result type
     /// @param val the input value for the instruction
-    Unary(enum Kind kind, const type::Type* result_type, Value* val);
+    Unary(InstructionResult* result, enum Kind kind, Value* val);
     ~Unary() override;
 
-    /// @returns the type of the value
-    const type::Type* Type() override { return result_type_; }
-
     /// @returns the value for the instruction
-    Value* Val() { return operands_[0]; }
+    Value* Val() { return operands_[kValueOperandOffset]; }
 
     /// @returns the kind of unary instruction
     enum Kind Kind() { return kind_; }
 
   private:
     enum Kind kind_;
-    const type::Type* result_type_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
index 7802dd4..b13cc09 100644
--- a/src/tint/ir/unary_test.cc
+++ b/src/tint/ir/unary_test.cc
@@ -49,7 +49,7 @@
     EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 }
 
-TEST_F(IR_UnaryTest, Unary_Usage) {
+TEST_F(IR_UnaryTest, Usage) {
     auto* inst = b.Negation(mod.Types().i32(), 4_i);
 
     EXPECT_EQ(inst->Kind(), Unary::Kind::kNegation);
@@ -58,6 +58,14 @@
     EXPECT_THAT(inst->Val()->Usages(), testing::UnorderedElementsAre(Usage{inst, 0u}));
 }
 
+TEST_F(IR_UnaryTest, Result) {
+    auto* inst = b.Negation(mod.Types().i32(), 4_i);
+    EXPECT_TRUE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Result()->Is<InstructionResult>());
+    EXPECT_EQ(inst->Result()->Source(), inst);
+}
+
 TEST_F(IR_UnaryTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
@@ -68,15 +76,5 @@
         "");
 }
 
-TEST_F(IR_UnaryTest, Fail_NullValue) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Negation(mod.Types().i32(), nullptr);
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/unreachable.cc b/src/tint/ir/unreachable.cc
new file mode 100644
index 0000000..b1aadfd
--- /dev/null
+++ b/src/tint/ir/unreachable.cc
@@ -0,0 +1,23 @@
+// 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/unreachable.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Unreachable);
+
+namespace tint::ir {
+
+Unreachable::~Unreachable() = default;
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/unreachable.h b/src/tint/ir/unreachable.h
new file mode 100644
index 0000000..51fdc24
--- /dev/null
+++ b/src/tint/ir/unreachable.h
@@ -0,0 +1,30 @@
+// 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_UNREACHABLE_H_
+#define SRC_TINT_IR_UNREACHABLE_H_
+
+#include "src/tint/ir/branch.h"
+
+namespace tint::ir {
+
+/// An unreachable instruction in the IR.
+class Unreachable : public utils::Castable<Unreachable, Branch> {
+  public:
+    ~Unreachable() override;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_UNREACHABLE_H_
diff --git a/src/tint/ir/user_call.cc b/src/tint/ir/user_call.cc
index e963e14..a1ddfee 100644
--- a/src/tint/ir/user_call.cc
+++ b/src/tint/ir/user_call.cc
@@ -22,12 +22,10 @@
 
 namespace tint::ir {
 
-UserCall::UserCall(const type::Type* ty, Function* func, utils::VectorRef<Value*> arguments)
-    : Base(ty) {
-    TINT_ASSERT(IR, func);
-
-    AddOperand(func);
-    AddOperands(std::move(arguments));
+UserCall::UserCall(InstructionResult* result, Function* func, utils::VectorRef<Value*> arguments) {
+    AddOperand(UserCall::kFunctionOperandOffset, func);
+    AddOperands(UserCall::kArgsOperandOffset, std::move(arguments));
+    AddResult(result);
 }
 
 UserCall::~UserCall() = default;
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
index 2d08482..9d3ea24 100644
--- a/src/tint/ir/user_call.h
+++ b/src/tint/ir/user_call.h
@@ -24,20 +24,26 @@
 /// A user call instruction in the IR.
 class UserCall : public utils::Castable<UserCall, Call> {
   public:
+    /// The offset in Operands() for the function being called
+    static constexpr size_t kFunctionOperandOffset = 0;
+
+    /// The base offset in Operands() for the call arguments
+    static constexpr size_t kArgsOperandOffset = 1;
+
     /// Constructor
-    /// @param type the result type
+    /// @param result the result value
     /// @param func the function being called
     /// @param args the function arguments
-    UserCall(const type::Type* type, Function* func, utils::VectorRef<Value*> args);
+    UserCall(InstructionResult* result, Function* func, utils::VectorRef<Value*> args);
     ~UserCall() override;
 
     /// @returns the call arguments
-    utils::Slice<Value* const> Args() override { return operands_.Slice().Offset(1); }
+    utils::Slice<Value* const> Args() override {
+        return operands_.Slice().Offset(kArgsOperandOffset);
+    }
 
     /// @returns the called function name
-    Function* Func() { return operands_.Front()->As<ir::Function>(); }
-
-  private:
+    Function* Func() { return operands_[kFunctionOperandOffset]->As<ir::Function>(); }
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/user_call_test.cc b/src/tint/ir/user_call_test.cc
index 6b73272..eb2cedb 100644
--- a/src/tint/ir/user_call_test.cc
+++ b/src/tint/ir/user_call_test.cc
@@ -34,6 +34,18 @@
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 2u}));
 }
 
+TEST_F(IR_UserCallTest, Results) {
+    auto* func = b.Function("myfunc", mod.Types().void_());
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* e = b.Call(mod.Types().void_(), func, utils::Vector{arg1, arg2});
+
+    EXPECT_TRUE(e->HasResults());
+    EXPECT_FALSE(e->HasMultiResults());
+    EXPECT_TRUE(e->Result()->Is<InstructionResult>());
+    EXPECT_EQ(e->Result()->Source(), e);
+}
+
 TEST_F(IR_UserCallTest, Fail_NullType) {
     EXPECT_FATAL_FAILURE(
         {
@@ -44,26 +56,5 @@
         "");
 }
 
-TEST_F(IR_UserCallTest, Fail_NullFunction) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Call(mod.Types().f32(), nullptr);
-        },
-        "");
-}
-
-TEST_F(IR_UserCallTest, Fail_NullArg) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Call(mod.Types().void_(), b.Function("myfunc", mod.Types().void_()),
-                   utils::Vector<Value*, 1>{nullptr});
-        },
-        "");
-}
-
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/validate.cc b/src/tint/ir/validate.cc
index 19beb21..7d4a15c 100644
--- a/src/tint/ir/validate.cc
+++ b/src/tint/ir/validate.cc
@@ -41,6 +41,7 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/swizzle.h"
 #include "src/tint/ir/unary.h"
+#include "src/tint/ir/unreachable.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/var.h"
 #include "src/tint/switch.h"
@@ -186,8 +187,11 @@
             [&](Binary*) {},                     //
             [&](Branch* b) { CheckBranch(b); },  //
             [&](Call* c) { CheckCall(c); },      //
+            [&](If* if_) { CheckIf(if_); },      //
             [&](Load*) {},                       //
+            [&](Loop*) {},                       //
             [&](Store*) {},                      //
+            [&](Switch*) {},                     //
             [&](Swizzle*) {},                    //
             [&](Unary*) {},                      //
             [&](Var*) {},                        //
@@ -267,8 +271,8 @@
             }
         }
 
-        auto* want_ty = a->Type()->UnwrapPtr();
-        bool want_ptr = a->Type()->Is<type::Pointer>();
+        auto* want_ty = a->Result()->Type()->UnwrapPtr();
+        bool want_ptr = a->Result()->Type()->Is<type::Pointer>();
         if (TINT_UNLIKELY(ty != want_ty || is_ptr != want_ptr)) {
             std::string want =
                 want_ptr ? "ptr<" + want_ty->FriendlyName() + ">" : want_ty->FriendlyName();
@@ -280,21 +284,19 @@
 
     void CheckBranch(ir::Branch* b) {
         tint::Switch(
-            b,                               //
-            [&](BreakIf*) {},                //
-            [&](Continue*) {},               //
-            [&](ExitIf*) {},                 //
-            [&](ExitLoop*) {},               //
-            [&](ExitSwitch*) {},             //
-            [&](If* if_) { CheckIf(if_); },  //
-            [&](Loop*) {},                   //
-            [&](NextIteration*) {},          //
-            [&](Return* ret) {
+            b,                           //
+            [&](ir::BreakIf*) {},        //
+            [&](ir::Continue*) {},       //
+            [&](ir::ExitIf*) {},         //
+            [&](ir::ExitLoop*) {},       //
+            [&](ir::ExitSwitch*) {},     //
+            [&](ir::NextIteration*) {},  //
+            [&](ir::Return* ret) {
                 if (ret->Func() == nullptr) {
                     AddError("return: null function");
                 }
-            },                //
-            [&](Switch*) {},  //
+            },
+            [&](ir::Unreachable*) {},  //
             [&](Default) {
                 AddError(std::string("missing validation of branch: ") + b->TypeInfo().name);
             });
diff --git a/src/tint/ir/validate_test.cc b/src/tint/ir/validate_test.cc
index d2f07d0..c05674a 100644
--- a/src/tint/ir/validate_test.cc
+++ b/src/tint/ir/validate_test.cc
@@ -25,14 +25,14 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using IR_ValidateTest = IRTestHelper;
 
 TEST_F(IR_ValidateTest, RootBlock_Var) {
     mod.root_block = b.RootBlock();
-    mod.root_block->Append(
-        b.Var(ty.ptr(builtin::AddressSpace::kPrivate, ty.i32(), builtin::Access::kReadWrite)));
+    mod.root_block->Append(b.Var(ty.ptr<private_, i32>()));
     auto res = ir::Validate(mod);
     EXPECT_TRUE(res) << res.Failure().str();
 }
@@ -73,7 +73,8 @@
     mod.functions.Push(f);
 
     f->SetParams({b.FunctionParam(ty.i32()), b.FunctionParam(ty.f32())});
-    f->StartTarget()->SetInstructions({b.Return(f)});
+    f->StartTarget()->Append(b.Return(f));
+
     auto res = ir::Validate(mod);
     EXPECT_TRUE(res) << res.Failure().str();
 }
@@ -102,8 +103,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.f32(), obj, 1_u, 0_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.f32(), obj, 1_u, 0_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     EXPECT_TRUE(res) << res.Failure().str();
@@ -111,13 +113,13 @@
 
 TEST_F(IR_ValidateTest, Valid_Access_Ptr) {
     auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(
-        ty.ptr(builtin::AddressSpace::kPrivate, ty.mat3x2<f32>(), builtin::Access::kReadWrite));
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.ptr<private_, f32>(), obj, 1_u, 0_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.ptr<private_, f32>(), obj, 1_u, 0_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     EXPECT_TRUE(res) << res.Failure().str();
@@ -129,8 +131,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.f32(), obj, -1_i));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.f32(), obj, -1_i);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -158,8 +161,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.f32(), obj, 1_u, 3_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.f32(), obj, 1_u, 3_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -187,13 +191,13 @@
 
 TEST_F(IR_ValidateTest, Access_OOB_Index_Ptr) {
     auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(
-        ty.ptr(builtin::AddressSpace::kPrivate, ty.mat3x2<f32>(), builtin::Access::kReadWrite));
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.ptr<private_, f32>(), obj, 1_u, 3_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.ptr<private_, f32>(), obj, 1_u, 3_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -226,8 +230,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.f32(), obj, 1_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.f32(), obj, 1_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -255,8 +260,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.ptr<private_, f32>(), obj, 1_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.ptr<private_, f32>(), obj, 1_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -279,13 +285,10 @@
 }
 
 TEST_F(IR_ValidateTest, Access_DynamicallyUnindexableType_Value) {
-    utils::Vector members{
-        ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 0u, 0u, 4u, 4u,
-                                   type::StructMemberAttributes{}),
-        ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 1u, 4u, 4u, 4u,
-                                   type::StructMemberAttributes{}),
-    };
-    auto* str_ty = ty.Get<type::Struct>(mod.symbols.New(), std::move(members), 4u, 8u, 8u);
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.i32()},
+                                                          });
 
     auto* f = b.Function("my_func", ty.void_());
     auto* obj = b.FunctionParam(str_ty);
@@ -293,13 +296,14 @@
     f->SetParams({obj, idx});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.i32(), obj, idx));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.i32(), obj, idx);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:8:25 error: access: type tint_symbol_2 cannot be dynamically indexed
+              R"(:8:25 error: access: type MyStruct cannot be dynamically indexed
     %4:i32 = access %2, %3
                         ^^
 
@@ -308,12 +312,12 @@
   ^^^^^^^^^^^
 
 note: # Disassembly
-tint_symbol_2 = struct @align(4) {
-  tint_symbol:i32 @offset(0)
-  tint_symbol_1:i32 @offset(4)
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:i32 @offset(4)
 }
 
-%my_func = func(%2:tint_symbol_2, %3:i32):void -> %b1 {
+%my_func = func(%2:MyStruct, %3:i32):void -> %b1 {
   %b1 = block {
     %4:i32 = access %2, %3
     ret
@@ -323,28 +327,25 @@
 }
 
 TEST_F(IR_ValidateTest, Access_DynamicallyUnindexableType_Ptr) {
-    utils::Vector members{
-        ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 0u, 0u, 4u, 4u,
-                                   type::StructMemberAttributes{}),
-        ty.Get<type::StructMember>(mod.symbols.New(), ty.i32(), 1u, 4u, 4u, 4u,
-                                   type::StructMemberAttributes{}),
-    };
-    auto* str_ty = ty.Get<type::Struct>(mod.symbols.New(), std::move(members), 4u, 8u, 8u);
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.i32()},
+                                                          });
 
     auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(
-        ty.ptr(builtin::AddressSpace::kPrivate, str_ty, builtin::Access::kReadWrite));
+    auto* obj = b.FunctionParam(ty.ptr<private_, read_write>(str_ty));
     auto* idx = b.FunctionParam(ty.i32());
     f->SetParams({obj, idx});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.i32(), obj, idx));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.i32(), obj, idx);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:8:25 error: access: type ptr<tint_symbol_2> cannot be dynamically indexed
+              R"(:8:25 error: access: type ptr<MyStruct> cannot be dynamically indexed
     %4:i32 = access %2, %3
                         ^^
 
@@ -353,12 +354,12 @@
   ^^^^^^^^^^^
 
 note: # Disassembly
-tint_symbol_2 = struct @align(4) {
-  tint_symbol:i32 @offset(0)
-  tint_symbol_1:i32 @offset(4)
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:i32 @offset(4)
 }
 
-%my_func = func(%2:ptr<private, tint_symbol_2, read_write>, %3:i32):void -> %b1 {
+%my_func = func(%2:ptr<private, MyStruct, read_write>, %3:i32):void -> %b1 {
   %b1 = block {
     %4:i32 = access %2, %3
     ret
@@ -373,8 +374,9 @@
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.i32(), obj, 1_u, 1_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.i32(), obj, 1_u, 1_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -399,13 +401,13 @@
 
 TEST_F(IR_ValidateTest, Access_Incorrect_Type_Ptr_Ptr) {
     auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(
-        ty.ptr(builtin::AddressSpace::kPrivate, ty.mat3x2<f32>(), builtin::Access::kReadWrite));
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.ptr<private_, i32>(), obj, 1_u, 1_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.ptr<private_, i32>(), obj, 1_u, 1_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -431,13 +433,13 @@
 
 TEST_F(IR_ValidateTest, Access_Incorrect_Type_Ptr_Value) {
     auto* f = b.Function("my_func", ty.void_());
-    auto* obj = b.FunctionParam(
-        ty.ptr(builtin::AddressSpace::kPrivate, ty.mat3x2<f32>(), builtin::Access::kReadWrite));
+    auto* obj = b.FunctionParam(ty.ptr<private_, mat3x2<f32>>());
     f->SetParams({obj});
     mod.functions.Push(f);
 
-    f->StartTarget()->Append(b.Access(ty.f32(), obj, 1_u, 1_u));
-    f->StartTarget()->Append(b.Return(f));
+    auto sb = b.With(f->StartTarget());
+    sb.Access(ty.f32(), obj, 1_u, 1_u);
+    sb.Return(f);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -465,7 +467,10 @@
     auto* f = b.Function("my_func", ty.void_());
     mod.functions.Push(f);
 
-    f->StartTarget()->SetInstructions({b.Return(f), b.Return(f)});
+    auto sb = b.With(f->StartTarget());
+    sb.Return(f);
+    sb.Return(f);
+
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(), R"(:3:5 error: block: branch which isn't the final instruction
@@ -495,6 +500,7 @@
     if_->False()->Append(b.Return(f));
 
     f->StartTarget()->Append(if_);
+    f->StartTarget()->Append(b.Return(f));
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -520,6 +526,7 @@
         ret
       }
 
+    ret
   }
 }
 )");
diff --git a/src/tint/ir/value.cc b/src/tint/ir/value.cc
index 5eb2e1e..1471681 100644
--- a/src/tint/ir/value.cc
+++ b/src/tint/ir/value.cc
@@ -25,6 +25,12 @@
 
 Value::~Value() = default;
 
+void Value::Destroy() {
+    TINT_ASSERT(IR, Alive());
+    TINT_ASSERT(IR, Usages().Count() == 0);
+    alive_ = false;
+}
+
 void Value::ReplaceAllUsesWith(std::function<Value*(Usage use)> replacer) {
     while (!uses_.IsEmpty()) {
         auto& use = *uses_.begin();
@@ -33,4 +39,11 @@
     }
 }
 
+void Value::ReplaceAllUsesWith(Value* replacement) {
+    while (!uses_.IsEmpty()) {
+        auto& use = *uses_.begin();
+        use.instruction->SetOperand(use.operand_index, replacement);
+    }
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index 1d7f88d..0efa6d0 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -31,7 +31,7 @@
     /// The instruction that is using the value;
     Instruction* instruction = nullptr;
     /// The index of the operand that is the value being used.
-    uint32_t operand_index = 0u;
+    size_t operand_index = 0u;
 
     /// A specialization of utils::Hasher for Usage.
     struct Hasher {
@@ -56,6 +56,16 @@
     /// Destructor
     ~Value() override;
 
+    /// @returns the type of the value
+    virtual const type::Type* Type() { return nullptr; }
+
+    /// Destroys the Value. Once called, the Value must not be used again.
+    /// The Value must not be in use by any instruction.
+    virtual void Destroy();
+
+    /// @returns true if the Value has not been destroyed with Destroy()
+    bool Alive() const { return alive_; }
+
     /// Adds a usage of this value.
     /// @param u the usage
     void AddUsage(Usage u) { uses_.Add(u); }
@@ -68,19 +78,21 @@
     /// uses the value for multiple different operands.
     const utils::Hashset<Usage, 4, Usage::Hasher>& Usages() { return uses_; }
 
-    /// @returns the type of the value
-    virtual const type::Type* Type() { return nullptr; }
-
     /// Replace all uses of the value.
     /// @param replacer a function which returns a replacement for a given use
     void ReplaceAllUsesWith(std::function<Value*(Usage use)> replacer);
 
+    /// Replace all uses of the value.
+    /// @param replacement the replacement value
+    void ReplaceAllUsesWith(Value* replacement);
+
   protected:
     /// Constructor
     Value();
 
   private:
     utils::Hashset<Usage, 4, Usage::Hasher> uses_;
+    bool alive_ = true;
 };
 }  // namespace tint::ir
 
diff --git a/src/tint/ir/value_test.cc b/src/tint/ir/value_test.cc
new file mode 100644
index 0000000..fc0d2fc
--- /dev/null
+++ b/src/tint/ir/value_test.cc
@@ -0,0 +1,80 @@
+// 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 "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
+#include "src/tint/ir/ir_test_helper.h"
+
+using namespace tint::number_suffixes;        // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+
+namespace tint::ir {
+namespace {
+
+using IR_ValueTest = IRTestHelper;
+
+TEST_F(IR_ValueTest, ReplaceAllUsesWith_Value) {
+    auto* val_old = b.InstructionResult(ty.i32());
+    auto* val_new = b.InstructionResult(ty.i32());
+    auto* inst = b.Add(mod.Types().i32(), val_old, 1_i);
+    EXPECT_EQ(inst->LHS(), val_old);
+    val_old->ReplaceAllUsesWith(val_new);
+    EXPECT_EQ(inst->LHS(), val_new);
+}
+
+TEST_F(IR_ValueTest, ReplaceAllUsesWith_Fn) {
+    auto* val_old = b.InstructionResult(ty.i32());
+    auto* val_new = b.InstructionResult(ty.i32());
+    auto* inst = b.Add(mod.Types().i32(), val_old, 1_i);
+    EXPECT_EQ(inst->LHS(), val_old);
+    val_old->ReplaceAllUsesWith([&](Usage use) {
+        EXPECT_EQ(use.instruction, inst);
+        EXPECT_EQ(use.operand_index, 0u);
+        return val_new;
+    });
+    EXPECT_EQ(inst->LHS(), val_new);
+}
+
+TEST_F(IR_ValueTest, Destroy) {
+    auto* val = b.InstructionResult(ty.i32());
+    EXPECT_TRUE(val->Alive());
+    val->Destroy();
+    EXPECT_FALSE(val->Alive());
+}
+
+TEST_F(IR_ValueTest, Destroy_HasUsage) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+            auto* val = b.InstructionResult(mod.Types().i32());
+            b.Add(mod.Types().i32(), val, 1_i);
+            val->Destroy();
+        },
+        "");
+}
+
+TEST_F(IR_ValueTest, Destroy_HasSource) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Module mod;
+            Builder b{mod};
+            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+            val->Destroy();
+        },
+        "");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/var.cc b/src/tint/ir/var.cc
index 4dd00ce..38cafb0 100644
--- a/src/tint/ir/var.cc
+++ b/src/tint/ir/var.cc
@@ -14,22 +14,37 @@
 
 #include "src/tint/ir/var.h"
 #include "src/tint/debug.h"
+#include "src/tint/ir/store.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Var);
 
 namespace tint::ir {
 
-Var::Var(const type::Pointer* ty) : type_(ty) {
-    TINT_ASSERT(IR, type_ != nullptr);
+Var::Var(InstructionResult* result) {
+    if (result && result->Type()) {
+        TINT_ASSERT(IR, result->Type()->Is<type::Pointer>());
+    }
 
     // Default to no initializer.
-    AddOperand(nullptr);
+    AddOperand(Var::kInitializerOperandOffset, nullptr);
+    AddResult(result);
 }
 
 Var::~Var() = default;
 
 void Var::SetInitializer(Value* initializer) {
-    SetOperand(0, initializer);
+    SetOperand(Var::kInitializerOperandOffset, initializer);
+}
+
+void Var::DestroyIfOnlyAssigned() {
+    auto* result = Result();
+    if (result->Usages().All([](const Usage& u) { return u.instruction->Is<ir::Store>(); })) {
+        while (!result->Usages().IsEmpty()) {
+            auto& usage = *result->Usages().begin();
+            usage.instruction->Destroy();
+        }
+        Destroy();
+    }
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/var.h b/src/tint/ir/var.h
index 6f22c7d..0e76756 100644
--- a/src/tint/ir/var.h
+++ b/src/tint/ir/var.h
@@ -26,21 +26,21 @@
 namespace tint::ir {
 
 /// A var instruction in the IR.
-class Var : public utils::Castable<Var, OperandInstruction<1>> {
+class Var : public utils::Castable<Var, OperandInstruction<1, 1>> {
   public:
-    /// Constructor
-    /// @param type the type of the var
-    explicit Var(const type::Pointer* type);
-    ~Var() override;
+    /// The offset in Operands() for the initializer
+    static constexpr size_t kInitializerOperandOffset = 0;
 
-    /// @returns the type of the var
-    const type::Pointer* Type() override { return type_; }
+    /// Constructor
+    /// @param result the result value
+    explicit Var(InstructionResult* result);
+    ~Var() override;
 
     /// Sets the var initializer
     /// @param initializer the initializer
     void SetInitializer(Value* initializer);
     /// @returns the initializer
-    Value* Initializer() { return operands_[0]; }
+    Value* Initializer() { return operands_[kInitializerOperandOffset]; }
 
     /// Sets the binding point
     /// @param group the group
@@ -49,8 +49,10 @@
     /// @returns the binding points if `Attributes` contains `kBindingPoint`
     std::optional<struct BindingPoint> BindingPoint() { return binding_point_; }
 
+    /// Destroys this instruction along with any assignment instructions, if the var is never read.
+    void DestroyIfOnlyAssigned();
+
   private:
-    const type::Pointer* type_ = nullptr;
     std::optional<struct BindingPoint> binding_point_;
 };
 
diff --git a/src/tint/ir/var_test.cc b/src/tint/ir/var_test.cc
index 7173237..5674213 100644
--- a/src/tint/ir/var_test.cc
+++ b/src/tint/ir/var_test.cc
@@ -23,7 +23,9 @@
 namespace tint::ir {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using IR_VarTest = IRTestHelper;
 
 TEST_F(IR_VarTest, Fail_NullType) {
@@ -36,6 +38,14 @@
         "");
 }
 
+TEST_F(IR_VarTest, Results) {
+    auto* var = b.Var(ty.ptr<function, f32>());
+    EXPECT_TRUE(var->HasResults());
+    EXPECT_FALSE(var->HasMultiResults());
+    EXPECT_TRUE(var->Result()->Is<InstructionResult>());
+    EXPECT_EQ(var->Result()->Source(), var);
+}
+
 TEST_F(IR_VarTest, Initializer_Usage) {
     Module mod;
     Builder b{mod};
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 52a73b3..f8866ac 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -50,6 +50,7 @@
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/increment_decrement_statement.h"
 #include "src/tint/ast/index_accessor_expression.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/invariant_attribute.h"
@@ -76,10 +77,11 @@
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ast/workgroup_attribute.h"
 #include "src/tint/builtin/extension.h"
+#include "src/tint/builtin/fluent_types.h"
 #include "src/tint/builtin/interpolation_sampling.h"
 #include "src/tint/builtin/interpolation_type.h"
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/manager.h"
-#include "src/tint/number.h"
 #include "src/tint/program.h"
 #include "src/tint/program_id.h"
 #include "src/tint/sem/array_count.h"
@@ -117,13 +119,10 @@
 
 namespace tint {
 
-// A sentinel type used by some template arguments to signal that the a type should be inferred.
-struct Infer {};
-
 /// Evaluates to true if T is a Infer, AInt or AFloat.
 template <typename T>
 static constexpr const bool IsInferOrAbstract =
-    std::is_same_v<std::decay_t<T>, Infer> || IsAbstract<std::decay_t<T>>;
+    std::is_same_v<std::decay_t<T>, builtin::fluent_types::Infer> || IsAbstract<std::decay_t<T>>;
 
 // Forward declare metafunction that evaluates to true iff T can be wrapped in a statement.
 template <typename T, typename = void>
@@ -1007,17 +1006,6 @@
             return array(builder->source_, subtype, std::move(attrs));
         }
 
-        /// @param subtype the array element type
-        /// @param n the array size. nullptr represents a runtime-array
-        /// @param attrs the optional attributes for the array
-        /// @return an array of size `n` of type `T`
-        template <typename COUNT, typename = DisableIfVectorLike<COUNT>>
-        ast::Type array(ast::Type subtype,
-                        COUNT&& n,
-                        utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
-            return array(builder->source_, subtype, std::forward<COUNT>(n), std::move(attrs));
-        }
-
         /// @param source the Source of the node
         /// @param subtype the array element type
         /// @param attrs the optional attributes for the array
@@ -1033,6 +1021,17 @@
                                                           std::move(attrs)))};
         }
 
+        /// @param subtype the array element type
+        /// @param n the array size. nullptr represents a runtime-array
+        /// @param attrs the optional attributes for the array
+        /// @return an array of size `n` of type `T`
+        template <typename COUNT, typename = DisableIfVectorLike<COUNT>>
+        ast::Type array(ast::Type subtype,
+                        COUNT&& n,
+                        utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
+            return array(builder->source_, subtype, std::forward<COUNT>(n), std::move(attrs));
+        }
+
         /// @param source the Source of the node
         /// @param subtype the array element type
         /// @param n the array size. nullptr represents a runtime-array
@@ -1054,53 +1053,47 @@
 
         /// @param source the Source of the node
         /// @return a inferred-size or runtime-sized array of type `T`
-        template <typename T, typename = EnableIfInferOrAbstract<T>>
+        template <typename T, int N = 0, typename = EnableIfInferOrAbstract<T>>
         ast::Type array(const Source& source) const {
+            static_assert(N == 0, "arrays with a count cannot be inferred");
             return (*this)(source, "array");
         }
 
         /// @return a inferred-size or runtime-sized array of type `T`
-        template <typename T, typename = EnableIfInferOrAbstract<T>>
+        template <typename T, int N = 0, typename = EnableIfInferOrAbstract<T>>
         ast::Type array() const {
+            static_assert(N == 0, "arrays with a count cannot be inferred");
             return array<T>(builder->source_);
         }
 
         /// @param source the Source of the node
         /// @param attrs the optional attributes for the array
         /// @return a inferred-size or runtime-sized array of type `T`
-        template <typename T, typename = DisableIfInferOrAbstract<T>>
+        template <typename T, int N = 0, typename = DisableIfInferOrAbstract<T>>
         ast::Type array(const Source& source,
                         utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
-            return ast::Type{builder->Expr(
-                builder->create<ast::TemplatedIdentifier>(source, builder->Sym("array"),
-                                                          utils::Vector<const ast::Expression*, 1>{
-                                                              Of<T>().expr,
-                                                          },
-                                                          std::move(attrs)))};
-        }
-
-        /// @param attrs the optional attributes for the array
-        /// @return a inferred-size or runtime-sized array of type `T`
-        template <typename T, typename = DisableIfInferOrAbstract<T>>
-        ast::Type array(utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
-            return array<T>(builder->source_, std::move(attrs));
-        }
-
-        /// @param source the Source of the node
-        /// @param attrs the optional attributes for the array
-        /// @return an array of size `N` of type `T`
-        template <typename T, int N>
-        ast::Type array(const Source& source,
-                        utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
-            static_assert(!IsInferOrAbstract<T>, "arrays with a count cannot be inferred");
-            return array(source, Of<T>(), tint::u32(N), std::move(attrs));
+            if constexpr (N == 0) {
+                return ast::Type{builder->Expr(builder->create<ast::TemplatedIdentifier>(
+                    source, builder->Sym("array"),
+                    utils::Vector<const ast::Expression*, 1>{
+                        Of<T>().expr,
+                    },
+                    std::move(attrs)))};
+            } else {
+                return ast::Type{builder->Expr(builder->create<ast::TemplatedIdentifier>(
+                    source, builder->Sym("array"),
+                    utils::Vector{
+                        Of<T>().expr,
+                        builder->Expr(builder->source_, tint::u32(N)),
+                    },
+                    std::move(attrs)))};
+            }
         }
 
         /// @param attrs the optional attributes for the array
         /// @return an array of size `N` of type `T`
-        template <typename T, int N>
+        template <typename T, int N = 0, typename = DisableIfInferOrAbstract<T>>
         ast::Type array(utils::VectorRef<const ast::Attribute*> attrs = utils::Empty) const {
-            static_assert(!IsInferOrAbstract<T>, "arrays with a count cannot be inferred");
             return array<T, N>(builder->source_, std::move(attrs));
         }
 
@@ -1160,9 +1153,48 @@
         }
 
         /// @param source the Source of the node
+        /// @return the pointer to type `T` with the builtin::AddressSpace `ADDRESS` and access
+        /// control `ACCESS`.
+        template <builtin::AddressSpace ADDRESS,
+                  typename T,
+                  builtin::Access ACCESS = builtin::Access::kUndefined>
+        ast::Type ptr(const Source& source) const {
+            return ptr<T>(source, ADDRESS, ACCESS);
+        }
+
+        /// @param type the type of the pointer
+        /// @return the pointer to the given type with the builtin::AddressSpace `ADDRESS` and
+        /// access control `ACCESS`.
+        template <builtin::AddressSpace ADDRESS,
+                  builtin::Access ACCESS = builtin::Access::kUndefined>
+        ast::Type ptr(ast::Type type) const {
+            return ptr(builder->source_, ADDRESS, type, ACCESS);
+        }
+
+        /// @param source the Source of the node
+        /// @param type the type of the pointer
+        /// @return the pointer to the given type with the builtin::AddressSpace `ADDRESS` and
+        /// access control `ACCESS`.
+        template <builtin::AddressSpace ADDRESS,
+                  builtin::Access ACCESS = builtin::Access::kUndefined>
+        ast::Type ptr(const Source& source, ast::Type type) const {
+            return ptr(source, ADDRESS, type, ACCESS);
+        }
+
+        /// @return the pointer to type `T` with the builtin::AddressSpace `ADDRESS` and access
+        /// control `ACCESS`.
+        template <builtin::AddressSpace ADDRESS,
+                  typename T,
+                  builtin::Access ACCESS = builtin::Access::kUndefined>
+        ast::Type ptr() const {
+            return ptr<T>(builder->source_, ADDRESS, ACCESS);
+        }
+
+        /// @param source the Source of the node
         /// @param address_space the address space of the pointer
         /// @param access the optional access control of the pointer
-        /// @return the pointer to type `T` with the given builtin::AddressSpace.
+        /// @return the pointer to type `T` the builtin::AddressSpace `ADDRESS` and access control
+        /// `ACCESS`.
         template <typename T>
         ast::Type ptr(const Source& source,
                       builtin::AddressSpace address_space,
@@ -1654,325 +1686,6 @@
         return Call(source, ty.vec(type, size), std::forward<ARGS>(args)...);
     }
 
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 2-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* vec2(ARGS&&... args) {
-        return vec2<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 2-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* vec2(const Source& source, ARGS&&... args) {
-        return Call(source, ty.vec2<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 2-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec2(ast::Type type, ARGS&&... args) {
-        return vec2(source_, type, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 2-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec2(const Source& source, ast::Type type, ARGS&&... args) {
-        return Call(source, ty.vec2(type), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 3-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* vec3(ARGS&&... args) {
-        return vec3<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 3-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* vec3(const Source& source, ARGS&&... args) {
-        return Call(source, ty.vec3<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 3-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec3(ast::Type type, ARGS&&... args) {
-        return vec3(source_, type, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 3-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec3(const Source& source, ast::Type type, ARGS&&... args) {
-        return Call(source, ty.vec3(type), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 4-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* vec4(ARGS&&... args) {
-        return vec4<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 4-element vector of type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* vec4(const Source& source, ARGS&&... args) {
-        return Call(source, ty.vec4<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 4-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec4(ast::Type type, ARGS&&... args) {
-        return vec4(source_, type, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the vector source
-    /// @param type the element type of the vector
-    /// @param args the arguments for the vector constructor
-    /// @return an `ast::CallExpression` of a 4-element vector of type @p type, constructed with the
-    /// values @p args.
-    template <typename... ARGS>
-    const ast::CallExpression* vec4(const Source& source, ast::Type type, ARGS&&... args) {
-        return Call(source, ty.vec4(type), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat2x2(ARGS&&... args) {
-        return mat2x2<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat2x2(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat2x2<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat2x3(ARGS&&... args) {
-        return mat2x3<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat2x3(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat2x3<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat2x4(ARGS&&... args) {
-        return mat2x4<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 2x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat2x4(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat2x4<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat3x2(ARGS&&... args) {
-        return mat3x2<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat3x2(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat3x2<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat3x3(ARGS&&... args) {
-        return mat3x3<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat3x3(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat3x3<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat3x4(ARGS&&... args) {
-        return mat3x4<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 3x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat3x4(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat3x4<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat4x2(ARGS&&... args) {
-        return mat4x2<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x2 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat4x2(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat4x2<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat4x3(ARGS&&... args) {
-        return mat4x3<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x3 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat4x3(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat4x3<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* mat4x4(ARGS&&... args) {
-        return mat4x4<T>(source_, std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the matrix source
-    /// @param args the arguments for the matrix constructor
-    /// @return an `ast::CallExpression` of a 4x4 matrix of type
-    /// `T`, constructed with the values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* mat4x4(const Source& source, ARGS&&... args) {
-        return Call(source, ty.mat4x4<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* array(ARGS&&... args) {
-        return Call(ty.array<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the array source
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type `T`, constructed with the
-    /// values @p args.
-    template <typename T, typename... ARGS>
-    const ast::CallExpression* array(const Source& source, ARGS&&... args) {
-        return Call(source, ty.array<T>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type `T` and size `N`, constructed
-    /// with the values @p args.
-    template <typename T, int N, typename... ARGS, typename = DisableIfSource<ARGS...>>
-    const ast::CallExpression* array(ARGS&&... args) {
-        return Call(ty.array<T, N>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the array source
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type `T` and size `N`, constructed
-    /// with the values @p args.
-    template <typename T, int N, typename... ARGS>
-    const ast::CallExpression* array(const Source& source, ARGS&&... args) {
-        return Call(source, ty.array<T, N>(), std::forward<ARGS>(args)...);
-    }
-
-    /// @param subtype the array element type
-    /// @param n the array size. nullptr represents a runtime-array.
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type
-    /// `subtype`, constructed with the values @p args.
-    template <typename EXPR, typename... ARGS>
-    const ast::CallExpression* array(ast::Type subtype, EXPR&& n, ARGS&&... args) {
-        return Call(ty.array(subtype, std::forward<EXPR>(n)), std::forward<ARGS>(args)...);
-    }
-
-    /// @param source the array source
-    /// @param subtype the array element type
-    /// @param n the array size. nullptr represents a runtime-array.
-    /// @param args the arguments for the array constructor
-    /// @return an `ast::CallExpression` of an array with element type
-    /// `subtype`, constructed with the values @p args.
-    template <typename EXPR, typename... ARGS>
-    const ast::CallExpression* array(const Source& source,
-                                     ast::Type subtype,
-                                     EXPR&& n,
-                                     ARGS&&... args) {
-        return Call(source, ty.array(subtype, std::forward<EXPR>(n)), std::forward<ARGS>(args)...);
-    }
-
     /// Adds the extension to the list of enable directives at the top of the module.
     /// @param extension the extension to enable
     /// @return an `ast::Enable` enabling the given extension.
@@ -3552,6 +3265,23 @@
         return create<ast::LocationAttribute>(source_, Expr(std::forward<EXPR>(location)));
     }
 
+    /// Creates an ast::IndexAttribute
+    /// @param source the source information
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::IndexAttribute* Index(const Source& source, EXPR&& index) {
+        return create<ast::IndexAttribute>(source, Expr(std::forward<EXPR>(index)));
+    }
+
+    /// Creates an ast::IndexAttribute
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::IndexAttribute* Index(EXPR&& index) {
+        return create<ast::IndexAttribute>(source_, Expr(std::forward<EXPR>(index)));
+    }
+
     /// Creates an ast::IdAttribute
     /// @param source the source information
     /// @param id the id value
@@ -3933,6 +3663,28 @@
 struct ProgramBuilder::TypesBuilder::CToAST<bool> {
     static ast::Type get(const ProgramBuilder::TypesBuilder* t) { return t->bool_(); }
 };
+template <typename T, uint32_t N>
+struct ProgramBuilder::TypesBuilder::CToAST<tint::builtin::fluent_types::array<T, N>> {
+    static ast::Type get(const ProgramBuilder::TypesBuilder* t) { return t->array<T, N>(); }
+};
+template <typename T>
+struct ProgramBuilder::TypesBuilder::CToAST<tint::builtin::fluent_types::atomic<T>> {
+    static ast::Type get(const ProgramBuilder::TypesBuilder* t) { return t->atomic<T>(); }
+};
+template <uint32_t C, uint32_t R, typename T>
+struct ProgramBuilder::TypesBuilder::CToAST<tint::builtin::fluent_types::mat<C, R, T>> {
+    static ast::Type get(const ProgramBuilder::TypesBuilder* t) { return t->mat<T>(C, R); }
+};
+template <uint32_t N, typename T>
+struct ProgramBuilder::TypesBuilder::CToAST<tint::builtin::fluent_types::vec<N, T>> {
+    static ast::Type get(const ProgramBuilder::TypesBuilder* t) { return t->vec<T, N>(); }
+};
+template <builtin::AddressSpace ADDRESS, typename T, builtin::Access ACCESS>
+struct ProgramBuilder::TypesBuilder::CToAST<tint::builtin::fluent_types::ptr<ADDRESS, T, ACCESS>> {
+    static ast::Type get(const ProgramBuilder::TypesBuilder* t) {
+        return t->ptr<ADDRESS, T, ACCESS>();
+    }
+};
 //! @endcond
 
 /// @param builder the ProgramBuilder
diff --git a/src/tint/program_id.h b/src/tint/program_id.h
index 4169ba1..35ab476 100644
--- a/src/tint/program_id.h
+++ b/src/tint/program_id.h
@@ -103,11 +103,11 @@
 /// valid program identifiers.
 #if TINT_CHECK_FOR_CROSS_PROGRAM_LEAKS
 #define TINT_ASSERT_PROGRAM_IDS_EQUAL(system, a, b)                        \
-    detail::AssertProgramIDsEqual(                                         \
+    tint::detail::AssertProgramIDsEqual(                                   \
         ProgramIDOf(a), ProgramIDOf(b), false, tint::diag::System::system, \
         "TINT_ASSERT_PROGRAM_IDS_EQUAL(" #system "," #a ", " #b ")", __FILE__, __LINE__)
 #define TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(system, a, b)              \
-    detail::AssertProgramIDsEqual(                                        \
+    tint::detail::AssertProgramIDsEqual(                                  \
         ProgramIDOf(a), ProgramIDOf(b), true, tint::diag::System::system, \
         "TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(" #system ", " #a ", " #b ")", __FILE__, __LINE__)
 #else
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index c78b9a0..6f56081 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -3895,14 +3895,31 @@
 
     if (op == spv::Op::OpCompositeConstruct) {
         ExpressionList operands;
+        bool all_same = true;
+        uint32_t first_id = 0u;
         for (uint32_t iarg = 0; iarg < inst.NumInOperands(); ++iarg) {
             auto operand = MakeOperand(inst, iarg);
             if (!operand) {
                 return {};
             }
             operands.Push(operand.expr);
+
+            // Check if this argument is different from the others.
+            auto arg_id = inst.GetSingleWordInOperand(iarg);
+            if (first_id != 0u) {
+                if (arg_id != first_id) {
+                    all_same = false;
+                }
+            } else {
+                first_id = arg_id;
+            }
         }
-        return {ast_type, builder_.Call(ast_type->Build(builder_), std::move(operands))};
+        if (all_same && ast_type->Is<Vector>()) {
+            // We're constructing a vector and all the operands were the same, so use a splat.
+            return {ast_type, builder_.Call(ast_type->Build(builder_), operands[0])};
+        } else {
+            return {ast_type, builder_.Call(ast_type->Build(builder_), std::move(operands))};
+        }
     }
 
     if (op == spv::Op::OpCompositeExtract) {
@@ -4693,40 +4710,68 @@
     const auto vec0_len = type_mgr_->GetType(vec0.type_id())->AsVector()->element_count();
     const auto vec1_len = type_mgr_->GetType(vec1.type_id())->AsVector()->element_count();
 
-    // Idiomatic vector accessors.
+    // Helper to get the name for the component index `i`.
+    auto component_name = [](uint32_t i) {
+        constexpr const char* names[] = {"x", "y", "z", "w"};
+        TINT_ASSERT(Reader, i < 4);
+        return names[i];
+    };
 
-    // Generate an ast::TypeInitializer expression.
+    // Build a swizzle for each consecutive set of indices that fall within the same vector.
     // Assume the literal indices are valid, and there is a valid number of them.
     auto source = GetSourceForInst(inst);
     const Vector* result_type = As<Vector>(parser_impl_.ConvertType(inst.type_id()));
+    uint32_t last_vec_id = 0u;
+    std::string swizzle;
     ExpressionList values;
     for (uint32_t i = 2; i < inst.NumInOperands(); ++i) {
-        const auto index = inst.GetSingleWordInOperand(i);
+        // Select the source vector and determine the index within it.
+        uint32_t vec_id = 0u;
+        uint32_t index = inst.GetSingleWordInOperand(i);
         if (index < vec0_len) {
-            auto expr = MakeExpression(vec0_id);
-            if (!expr) {
-                return {};
-            }
-            values.Push(create<ast::MemberAccessorExpression>(source, expr.expr, Swizzle(index)));
+            vec_id = vec0_id;
         } else if (index < vec0_len + vec1_len) {
-            const auto sub_index = index - vec0_len;
-            TINT_ASSERT(Reader, sub_index < kMaxVectorLen);
-            auto expr = MakeExpression(vec1_id);
-            if (!expr) {
-                return {};
-            }
-            values.Push(
-                create<ast::MemberAccessorExpression>(source, expr.expr, Swizzle(sub_index)));
+            vec_id = vec1_id;
+            index -= vec0_len;
+            TINT_ASSERT(Reader, index < kMaxVectorLen);
         } else if (index == 0xFFFFFFFF) {
-            // By rule, this maps to OpUndef.  Instead, make it zero.
-            values.Push(parser_impl_.MakeNullValue(result_type->type));
+            // By rule, this maps to OpUndef. Instead, take the first component of the first vector.
+            vec_id = vec0_id;
+            index = 0u;
         } else {
             Fail() << "invalid vectorshuffle ID %" << inst.result_id()
                    << ": index too large: " << index;
             return {};
         }
+
+        if (vec_id != last_vec_id && !swizzle.empty()) {
+            // The source vector has changed, so emit the swizzle so far.
+            auto expr = MakeExpression(last_vec_id);
+            if (!expr) {
+                return {};
+            }
+            values.Push(builder_.MemberAccessor(source, expr.expr, builder_.Ident(swizzle)));
+            swizzle.clear();
+        }
+        swizzle += component_name(index);
+        last_vec_id = vec_id;
     }
-    return {result_type, builder_.Call(source, result_type->Build(builder_), std::move(values))};
+
+    // Emit the final swizzle.
+    auto expr = MakeExpression(last_vec_id);
+    if (!expr) {
+        return {};
+    }
+    values.Push(builder_.MemberAccessor(source, expr.expr, builder_.Ident(swizzle)));
+
+    if (values.Length() == 1) {
+        // There's only one swizzle, so just return it.
+        return {result_type, values[0]};
+    } else {
+        // There's multiple swizzles, so generate a type constructor expression to combine them.
+        return {result_type,
+                builder_.Call(source, result_type->Build(builder_), std::move(values))};
+    }
 }
 
 bool FunctionEmitter::RegisterSpecialBuiltInVariables() {
diff --git a/src/tint/reader/spirv/function_composite_test.cc b/src/tint/reader/spirv/function_composite_test.cc
index 442d6fc..3d311f1 100644
--- a/src/tint/reader/spirv/function_composite_test.cc
+++ b/src/tint/reader/spirv/function_composite_test.cc
@@ -101,6 +101,27 @@
 )"));
 }
 
+TEST_F(SpvParserTest_Composite_Construct, VectorSplat) {
+    const auto assembly = Preamble() + R"(
+     %100 = OpFunction %void None %voidfn
+     %entry = OpLabel
+     %1 = OpCompositeConstruct %v4uint %uint_10 %uint_10 %uint_10 %uint_10
+     %2 = OpCompositeConstruct %v2int %int_30 %int_30
+     %3 = OpCompositeConstruct %v2float %float_50 %float_50
+     OpReturn
+     OpFunctionEnd
+  )";
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << assembly;
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_1 = vec4u(10u);
+let x_2 = vec2i(30i);
+let x_3 = vec2f(50.0f);
+)"));
+}
+
 TEST_F(SpvParserTest_Composite_Construct, Matrix) {
     const auto assembly = Preamble() + R"(
      %100 = OpFunction %void None %voidfn
@@ -117,7 +138,7 @@
     EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = mat3x2f("
                                                                   "vec2f(50.0f, 60.0f), "
                                                                   "vec2f(60.0f, 50.0f), "
-                                                                  "vec2f(70.0f, 70.0f));"));
+                                                                  "vec2f(70.0f));"));
 }
 
 TEST_F(SpvParserTest_Composite_Construct, Array) {
@@ -779,7 +800,46 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 = vec4u(x_2.y, x_2.x, x_1.y, x_1.x);"));
+                HasSubstr("let x_10 = vec4u(x_2.yx, x_1.yx);"));
+}
+
+TEST_F(SpvParserTest_VectorShuffle, FunctionScopeOperands_UseBoth_Swizzle) {
+    // Use the same vector for both source operands.
+    const auto assembly = Preamble() + R"(
+     %100 = OpFunction %void None %voidfn
+     %entry = OpLabel
+     %1 = OpCopyObject %v2uint %v2uint_3_4
+     %10 = OpVectorShuffle %v4uint %1 %1 1 0 2 3
+     OpReturn
+     OpFunctionEnd
+)";
+
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << assembly;
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = x_1.yxxy;"));
+}
+
+TEST_F(SpvParserTest_VectorShuffle, FunctionScopeOperands_UseOne_Swizzle) {
+    // Only use the first vector operand.
+    const auto assembly = Preamble() + R"(
+     %100 = OpFunction %void None %voidfn
+     %entry = OpLabel
+     %1 = OpCopyObject %v2uint %v2uint_3_4
+     %2 = OpUndef %v2uint
+     %10 = OpVectorShuffle %v4uint %1 %2 1 0 0 1
+     OpReturn
+     OpFunctionEnd
+)";
+
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << assembly;
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = x_1.yxxy;"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, ConstantOperands_UseBoth) {
@@ -796,11 +856,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = vec4u("
-                                                                  "vec2u(4u, 3u).y, "
-                                                                  "vec2u(4u, 3u).x, "
-                                                                  "vec2u(3u, 4u).y, "
-                                                                  "vec2u(3u, 4u).x);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_10 = vec4u(vec2u(4u, 3u).yx, vec2u(3u, 4u).yx);"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, ConstantOperands_AllOnesMapToNull) {
@@ -818,7 +875,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = vec2u(0u, x_1.y);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = x_1.xy;"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, FunctionScopeOperands_MixedInputOperandSizes) {
diff --git a/src/tint/reader/spirv/function_memory_test.cc b/src/tint/reader/spirv/function_memory_test.cc
index b75a3bc..a35aa8c 100644
--- a/src/tint/reader/spirv/function_memory_test.cc
+++ b/src/tint/reader/spirv/function_memory_test.cc
@@ -499,8 +499,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody());
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("myvar[2u] = vec4f(42.0f, 42.0f, 42.0f, 42.0f);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("myvar[2u] = vec4f(42.0f);"));
 }
 
 TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Array) {
@@ -531,8 +530,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody());
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("myvar[2u] = vec4f(42.0f, 42.0f, 42.0f, 42.0f);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("myvar[2u] = vec4f(42.0f);"));
 }
 
 TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Struct) {
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 7a3089d..0c61368 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -1920,7 +1920,16 @@
                 return {original_ast_type, builder_.Expr(itr->second)};
             }
 
+            const auto* spirv_const = constant_mgr_->FindDeclaredConstant(id);
+            if (spirv_const->IsZero()) {
+                // All zeros, so just use a zero value constructor and always inline it.
+                return {original_ast_type,
+                        builder_.Call(source, original_ast_type->Build(builder_))};
+            }
+
             // Generate a composite from explicit components.
+            bool all_same = true;
+            uint32_t first_id = 0u;
             ExpressionList ast_components;
             if (!inst->WhileEachInId([&](const uint32_t* id_ref) -> bool {
                     auto component = MakeConstantExpression(*id_ref);
@@ -1929,14 +1938,30 @@
                         return false;
                     }
                     ast_components.Push(component.expr);
+
+                    // Check if this argument is different from the others.
+                    if (first_id != 0u) {
+                        if (*id_ref != first_id) {
+                            all_same = false;
+                        }
+                    } else {
+                        first_id = *id_ref;
+                    }
+
                     return true;
                 })) {
                 // We've already emitted a diagnostic.
                 return {};
             }
 
-            auto* expr = builder_.Call(source, original_ast_type->Build(builder_),
-                                       std::move(ast_components));
+            const ast::Expression* expr = nullptr;
+            if (all_same && original_ast_type->Is<Vector>()) {
+                // We're constructing a vector and all the operands were the same, so use a splat.
+                expr = builder_.Call(source, original_ast_type->Build(builder_), ast_components[0]);
+            } else {
+                expr = builder_.Call(source, original_ast_type->Build(builder_),
+                                     std::move(ast_components));
+            }
 
             if (def_use_mgr_->NumUses(id) == 1) {
                 // The constant is only used once, so just inline its use.
diff --git a/src/tint/reader/spirv/parser_impl_handle_test.cc b/src/tint/reader/spirv/parser_impl_handle_test.cc
index 206e258..4f1e1a6 100644
--- a/src/tint/reader/spirv/parser_impl_handle_test.cc
+++ b/src/tint/reader/spirv/parser_impl_handle_test.cc
@@ -4160,7 +4160,7 @@
     const auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var x_24 : vec2f;
 var x_26 : i32;
-x_24 = vec2f(0.0f, 0.0f);
+x_24 = vec2f();
 x_26 = 0i;
 loop {
   var x_27 : i32;
@@ -4171,11 +4171,11 @@
 
   continuing {
     x_27 = (x_26 + 1i);
-    x_24 = vec2f(1.0f, 1.0f);
+    x_24 = vec2f(1.0f);
     x_26 = x_27;
   }
 }
-textureStore(Output2Texture2D, vec2i(vec2u(1u, 1u)), vec4f(x_24, 0.0f, 0.0f));
+textureStore(Output2Texture2D, vec2i(vec2u(1u)), vec4f(x_24, 0.0f, 0.0f));
 return;
 )";
     ASSERT_EQ(expect, got);
@@ -4250,7 +4250,7 @@
   }
 }
 let x_21 = select(0.0f, x_14, (x_14 > 1.0f));
-x_1 = vec4f(x_21, x_21, x_21, x_21);
+x_1 = vec4f(x_21);
 return;
 )";
     ASSERT_EQ(expect, got);
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 304a545..ac42988 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -25,8 +25,8 @@
 #include <type_traits>
 #include <utility>
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/debug.h"
-#include "src/tint/number.h"
 #include "src/tint/utils/parse_num.h"
 #include "src/tint/utils/unicode.h"
 
diff --git a/src/tint/reader/wgsl/lexer_test.cc b/src/tint/reader/wgsl/lexer_test.cc
index 8d49bf8..a705de0 100644
--- a/src/tint/reader/wgsl/lexer_test.cc
+++ b/src/tint/reader/wgsl/lexer_test.cc
@@ -19,7 +19,7 @@
 #include <vector>
 
 #include "gtest/gtest.h"
-#include "src/tint/number.h"
+#include "src/tint/builtin/number.h"
 
 namespace tint::reader::wgsl {
 namespace {
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index b167f56..1151fc4 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -121,9 +121,9 @@
         /// std::unique_ptr, operator->() automatically dereferences so that the
         /// return type will always be a pointer to a non-pointer type. #errored
         /// must be false to call.
-        inline typename detail::OperatorArrow<T>::type operator->() {
+        inline typename wgsl::detail::OperatorArrow<T>::type operator->() {
             TINT_ASSERT(Reader, !errored);
-            return detail::OperatorArrow<T>::ptr(value);
+            return wgsl::detail::OperatorArrow<T>::ptr(value);
         }
 
         /// The expected value of a successful parse.
@@ -183,9 +183,9 @@
         /// std::unique_ptr, operator->() automatically dereferences so that the
         /// return type will always be a pointer to a non-pointer type. #errored
         /// must be false to call.
-        inline typename detail::OperatorArrow<T>::type operator->() {
+        inline typename wgsl::detail::OperatorArrow<T>::type operator->() {
             TINT_ASSERT(Reader, !errored);
-            return detail::OperatorArrow<T>::ptr(value);
+            return wgsl::detail::OperatorArrow<T>::ptr(value);
         }
 
         /// The value of a successful parse.
diff --git a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
index 3c63a02..621206f 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -164,7 +164,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -178,7 +178,7 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -226,7 +226,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -237,7 +237,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -249,7 +249,7 @@
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
index f3e306a..9817bec 100644
--- a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
@@ -54,7 +54,7 @@
      ^
 
 test.wgsl:4:2 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
 @_ fn -> {}
  ^
 )");
@@ -122,7 +122,7 @@
          ^^^^
 
 test.wgsl:7:6 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
     @- x : i32,
      ^
 )");
diff --git a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
index ae43a0f..52d7da1 100644
--- a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
@@ -53,7 +53,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
index 1751e98..76c27e7 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -51,7 +51,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/reflection.h b/src/tint/reflection.h
index d341f98..e965200 100644
--- a/src/tint/reflection.h
+++ b/src/tint/reflection.h
@@ -36,7 +36,7 @@
 
 /// Is true if the class T has reflected its fields with TINT_REFLECT()
 template <typename T>
-static constexpr bool HasReflection = detail::HasReflection<T>::value;
+static constexpr bool HasReflection = tint::detail::HasReflection<T>::value;
 
 /// Calls @p callback with each field of @p object
 /// @param object the object
diff --git a/src/tint/resolver/address_space_layout_validation_test.cc b/src/tint/resolver/address_space_layout_validation_test.cc
index 9a621e3..1006120 100644
--- a/src/tint/resolver/address_space_layout_validation_test.cc
+++ b/src/tint/resolver/address_space_layout_validation_test.cc
@@ -17,11 +17,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverAddressSpaceLayoutValidationTest = ResolverTest;
 
 // Detect unaligned member for storage buffers
@@ -430,7 +431,7 @@
     // @group(0) @binding(0)
     // var<uniform> a : Outer;
 
-    Alias("Inner", ty.array(ty.vec2<f32>(), 10_u));
+    Alias("Inner", ty.array<vec2<f32>, 10>());
 
     Structure(Source{{12, 34}}, "Outer",
               utils::Vector{
@@ -739,7 +740,7 @@
 
     Structure(Source{{12, 34}}, "Outer",
               utils::Vector{
-                  Member("arr", ty.array(ty.vec3<f16>(), 10_u)),
+                  Member("arr", ty.array<vec3<f16>, 10>()),
               });
 
     GlobalVar(Source{{78, 90}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
diff --git a/src/tint/resolver/address_space_validation_test.cc b/src/tint/resolver/address_space_validation_test.cc
index fae4a25..a5bd21e 100644
--- a/src/tint/resolver/address_space_validation_test.cc
+++ b/src/tint/resolver/address_space_validation_test.cc
@@ -18,11 +18,12 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/sem/struct.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ::testing::HasSubstr;
 
 using ResolverAddressSpaceValidationTest = ResolverTest;
@@ -67,8 +68,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Private_RuntimeArray) {
     // type t : ptr<private, array<i32>>;
-    Alias("t", ty.ptr(Source{{56, 78}}, builtin::AddressSpace::kPrivate,
-                      ty.array(Source{{12, 34}}, ty.i32())));
+    Alias("t", ty.ptr<private_>(Source{{56, 78}}, ty.array(Source{{12, 34}}, ty.i32())));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -93,7 +93,7 @@
     // struct S { m : array<i32> };
     // type t = ptr<private, S>;
     Structure("S", utils::Vector{Member(Source{{12, 34}}, "m", ty.array(ty.i32()))});
-    Alias("t", ty.ptr(builtin::AddressSpace::kPrivate, ty("S")));
+    Alias("t", ty.ptr<private_>(ty("S")));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -115,7 +115,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Workgroup_RuntimeArray) {
     // type t = ptr<workgroup, array<i32>>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kWorkgroup, ty.array(Source{{12, 34}}, ty.i32())));
+    Alias("t", ty.ptr<workgroup>(ty.array(Source{{12, 34}}, ty.i32())));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -140,7 +140,7 @@
     // struct S { m : array<i32> };
     // type t = ptr<workgroup, S>;
     Structure("S", utils::Vector{Member(Source{{12, 34}}, "m", ty.array(ty.i32()))});
-    Alias(Source{{56, 78}}, "t", ty.ptr(builtin::AddressSpace::kWorkgroup, ty("S")));
+    Alias(Source{{56, 78}}, "t", ty.ptr<workgroup>(ty("S")));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -164,8 +164,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_Bool) {
     // type t = ptr<storage, bool>;
-    Alias(Source{{56, 78}}, "t",
-          ty.ptr(builtin::AddressSpace::kStorage, ty.bool_(Source{{12, 34}})));
+    Alias(Source{{56, 78}}, "t", ty.ptr<storage>(ty.bool_(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -194,8 +193,7 @@
     // type a = bool;
     // type t = ptr<storage, a>;
     Alias("a", ty.bool_());
-    Alias(Source{{56, 78}}, "t",
-          ty.ptr(builtin::AddressSpace::kStorage, ty(Source{{12, 34}}, "a")));
+    Alias(Source{{56, 78}}, "t", ty.ptr<storage>(ty(Source{{12, 34}}, "a")));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -207,8 +205,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_Storage_Pointer) {
     // var<storage> g : ptr<private, f32>;
-    GlobalVar(Source{{56, 78}}, "g",
-              ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32()),
+    GlobalVar(Source{{56, 78}}, "g", ty.ptr<private_, f32>(Source{{12, 34}}),
               builtin::AddressSpace::kStorage, Binding(0_a), Group(0_a));
 
     ASSERT_FALSE(r()->Resolve());
@@ -221,8 +218,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_Pointer) {
     // type t = ptr<storage, ptr<private, f32>>;
-    Alias("t", ty.ptr(Source{{56, 78}}, builtin::AddressSpace::kStorage,
-                      ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32())));
+    Alias("t", ty.ptr<storage>(Source{{56, 78}}, ty.ptr<private_, f32>(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -241,7 +237,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_IntScalar) {
     // type t = ptr<storage, i32;
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.i32()));
+    Alias("t", ty.ptr<storage, i32>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -261,7 +257,7 @@
     // type t = ptr<storage, f16>;
     Enable(builtin::Extension::kF16);
 
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.f16()));
+    Alias("t", ty.ptr<storage, f16>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -285,7 +281,7 @@
     Enable(builtin::Extension::kF16);
 
     Alias("a", ty.f16());
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty("a")));
+    Alias("t", ty.ptr<storage>(ty("a")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -299,7 +295,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_VectorF32) {
     // type t = ptr<storage, vec4<f32>>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.vec4<f32>()));
+    Alias("t", ty.ptr<storage, vec4<f32>>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -315,7 +311,7 @@
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_VectorF16) {
     // type t = ptr<storage, vec4<f16>>;
     Enable(builtin::Extension::kF16);
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.vec(ty.f16(), 4u)));
+    Alias("t", ty.ptr<storage, vec4<f32>>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -334,7 +330,7 @@
     // struct S{ a : f32 };
     // type t = ptr<storage, array<S, 3u>>;
     Structure("S", utils::Vector{Member("a", ty.f32())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.array(ty("S"), 3_u)));
+    Alias("t", ty.ptr<storage>(ty.array(ty("S"), 3_u)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -359,8 +355,7 @@
     Enable(builtin::Extension::kF16);
 
     Structure("S", utils::Vector{Member("a", ty.f16())});
-    Alias("t",
-          ty.ptr(builtin::AddressSpace::kStorage, ty.array(ty("S"), 3_u), builtin::Access::kRead));
+    Alias("t", ty.ptr<storage, read>(ty.array(ty("S"), 3_u)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -379,7 +374,7 @@
     // struct S { x : i32 };
     // type t = ptr<storage, S, read>;
     Structure("S", utils::Vector{Member("x", ty.i32())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty("S"), builtin::Access::kRead));
+    Alias("t", ty.ptr<storage, read>(ty("S")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -404,7 +399,7 @@
     Structure("S", utils::Vector{Member("x", ty.i32())});
     Alias("a1", ty("S"));
     Alias("a2", ty("a1"));
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty("a2"), builtin::Access::kRead));
+    Alias("t", ty.ptr<storage, read>(ty("a2")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -427,7 +422,7 @@
     Enable(builtin::Extension::kF16);
 
     Structure("S", utils::Vector{Member("x", ty.f16())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty("S"), builtin::Access::kRead));
+    Alias("t", ty.ptr<storage, read>(ty("S")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -456,7 +451,7 @@
     Structure("S", utils::Vector{Member("x", ty.f16())});
     Alias("a1", ty("S"));
     Alias("a2", ty("a1"));
-    Alias("g", ty.ptr(builtin::AddressSpace::kStorage, ty("a2"), builtin::Access::kRead));
+    Alias("g", ty.ptr<storage, read>(ty("a2")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -475,8 +470,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_NotStorage_AccessMode) {
     // type t = ptr<private, i32, read>;
-    Alias("t", ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.i32(),
-                      builtin::Access::kRead));
+    Alias("t", ty.ptr<private_, i32, read>(Source{{12, 34}}));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -495,7 +489,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_ReadAccessMode) {
     // type t = ptr<storage, i32, read>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.i32(), builtin::Access::kRead));
+    Alias("t", ty.ptr<storage, i32, read>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -510,7 +504,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_ReadWriteAccessMode) {
     // type t = ptr<storage, i32, read_write>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kStorage, ty.i32(), builtin::Access::kReadWrite));
+    Alias("t", ty.ptr<storage, i32, read_write>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -527,9 +521,8 @@
 }
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_WriteAccessMode) {
-    // type t = ptr<storage, i32, read_write>;
-    Alias("t", ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kStorage, ty.i32(),
-                      builtin::Access::kWrite));
+    // type t = ptr<storage, i32, write>;
+    Alias("t", ty.ptr<storage, i32, write>(Source{{12, 34}}));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -561,7 +554,7 @@
     Structure("S",
               utils::Vector{Member(Source{{56, 78}}, "m", ty.array(Source{{12, 34}}, ty.i32()))});
 
-    Alias("t", ty.ptr(Source{{90, 12}}, builtin::AddressSpace::kUniform, ty("S")));
+    Alias("t", ty.ptr<uniform>(Source{{90, 12}}, ty("S")));
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -589,8 +582,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_UniformBufferBool) {
     // type t = ptr<uniform, bool>;
-    Alias("t",
-          ty.ptr(Source{{56, 78}}, builtin::AddressSpace::kUniform, ty.bool_(Source{{12, 34}})));
+    Alias("t", ty.ptr<uniform>(Source{{56, 78}}, ty.bool_(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -619,8 +611,7 @@
     // type a = bool;
     // type t = ptr<uniform, a>;
     Alias("a", ty.bool_());
-    Alias("t",
-          ty.ptr(Source{{56, 78}}, builtin::AddressSpace::kUniform, ty(Source{{12, 34}}, "a")));
+    Alias("t", ty.ptr<uniform>(Source{{56, 78}}, ty(Source{{12, 34}}, "a")));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -632,8 +623,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_UniformPointer) {
     // var<uniform> g : ptr<private, f32>;
-    GlobalVar(Source{{56, 78}}, "g",
-              ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32()),
+    GlobalVar(Source{{56, 78}}, "g", ty.ptr<private_, f32>(Source{{12, 34}}),
               builtin::AddressSpace::kUniform, Binding(0_a), Group(0_a));
 
     ASSERT_FALSE(r()->Resolve());
@@ -646,8 +636,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_UniformPointer) {
     // type t = ptr<uniform, ptr<private, f32>>;
-    Alias("t", ty.ptr(Source{{56, 78}}, builtin::AddressSpace::kUniform,
-                      ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32())));
+    Alias("t", ty.ptr<uniform>(Source{{56, 78}}, ty.ptr<private_, f32>(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
 
@@ -667,7 +656,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_UniformBufferIntScalar) {
     // type t = ptr<uniform, i32>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.i32()));
+    Alias("t", ty.ptr<uniform, i32>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -687,7 +676,7 @@
     // type t = ptr<uniform, f16>;
     Enable(builtin::Extension::kF16);
 
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.f16()));
+    Alias("t", ty.ptr<uniform, f16>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -701,7 +690,7 @@
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_UniformBufferVectorF32) {
     // type t = ptr<uniform, vec4<f32>>;
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.vec4<f32>()));
+    Alias("t", ty.ptr<uniform, vec4<f32>>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -721,7 +710,7 @@
     // type t = ptr<uniform, vec4<f16>>;
     Enable(builtin::Extension::kF16);
 
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.vec4<f16>()));
+    Alias("t", ty.ptr<uniform, f16>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -744,7 +733,7 @@
     // }
     // type t = ptr<uniform, array<S, 3u>>;
     Structure("S", utils::Vector{Member("a", ty.f32(), utils::Vector{MemberSize(16_a)})});
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.array(ty("S"), 3_u)));
+    Alias("t", ty.ptr<uniform>(ty.array(ty("S"), 3_u)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -773,7 +762,7 @@
     Enable(builtin::Extension::kF16);
 
     Structure("S", utils::Vector{Member("a", ty.f16(), utils::Vector{MemberSize(16_a)})});
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty.array(ty("S"), 3_u)));
+    Alias("t", ty.ptr<uniform>(ty.array(ty("S"), 3_u)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -791,7 +780,7 @@
     // struct S { x : i32 };
     // type t = ptr<uniform, S>;
     Structure("S", utils::Vector{Member("x", ty.i32())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty("S")));
+    Alias("t", ty.ptr<uniform>(ty("S")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -813,7 +802,7 @@
     // type t = ptr<uniform, a1>;
     Structure("S", utils::Vector{Member("x", ty.i32())});
     Alias("a1", ty("S"));
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty("a1")));
+    Alias("t", ty.ptr<uniform>(ty("a1")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -837,7 +826,7 @@
     Enable(builtin::Extension::kF16);
 
     Structure("S", utils::Vector{Member("x", ty.f16())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty("S")));
+    Alias("t", ty.ptr<uniform>(ty("S")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -865,7 +854,7 @@
 
     Structure("S", utils::Vector{Member("x", ty.f16())});
     Alias("a1", ty("S"));
-    Alias("t", ty.ptr(builtin::AddressSpace::kUniform, ty("a1")));
+    Alias("t", ty.ptr<uniform>(ty("a1")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -888,8 +877,7 @@
     // enable chromium_experimental_push_constant;
     // type t = ptr<push_constant, bool>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    Alias(Source{{56, 78}}, "t",
-          ty.ptr(builtin::AddressSpace::kPushConstant, ty.bool_(Source{{12, 34}})));
+    Alias(Source{{56, 78}}, "t", ty.ptr<push_constant>(ty.bool_(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -917,7 +905,7 @@
     // type t = ptr<push_constant, f16>;
     Enable(builtin::Extension::kF16);
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    Alias("t", ty.ptr(builtin::AddressSpace::kPushConstant, ty.f16(Source{{56, 78}})));
+    Alias("t", ty.ptr<push_constant>(ty.f16(Source{{56, 78}})));
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -928,8 +916,7 @@
     // enable chromium_experimental_push_constant;
     // var<push_constant> g : ptr<private, f32>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    GlobalVar(Source{{56, 78}}, "g",
-              ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32()),
+    GlobalVar(Source{{56, 78}}, "g", ty.ptr<private_, f32>(Source{{12, 34}}),
               builtin::AddressSpace::kPushConstant);
 
     ASSERT_FALSE(r()->Resolve());
@@ -943,9 +930,7 @@
     // enable chromium_experimental_push_constant;
     // type t = ptr<push_constant, ptr<private, f32>>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    Alias(Source{{56, 78}}, "t",
-          ty.ptr(builtin::AddressSpace::kPushConstant,
-                 ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kPrivate, ty.f32())));
+    Alias(Source{{56, 78}}, "t", ty.ptr<push_constant>(ty.ptr<private_, f32>(Source{{12, 34}})));
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -967,7 +952,7 @@
     // enable chromium_experimental_push_constant;
     // type t = ptr<push_constant, i32>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    Alias("t", ty.ptr(builtin::AddressSpace::kPushConstant, ty.i32()));
+    Alias("t", ty.ptr<push_constant, i32>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -985,7 +970,7 @@
     // enable chromium_experimental_push_constant;
     // var<push_constant> g : vec4<f32>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
-    Alias("t", ty.ptr(builtin::AddressSpace::kPushConstant, ty.vec4<f32>()));
+    Alias("t", ty.ptr<push_constant, vec4<f32>>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -1007,7 +992,7 @@
     // type t = ptr<push_constant, array<S, 3u>>;
     Enable(builtin::Extension::kChromiumExperimentalPushConstant);
     Structure("S", utils::Vector{Member("a", ty.f32())});
-    Alias("t", ty.ptr(builtin::AddressSpace::kPushConstant, ty.array(ty("S"), 3_u)));
+    Alias("t", ty.ptr<push_constant>(ty.array(ty("S"), 3_u)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
diff --git a/src/tint/resolver/alias_analysis_test.cc b/src/tint/resolver/alias_analysis_test.cc
index 7eab9bc..1145304 100644
--- a/src/tint/resolver/alias_analysis_test.cc
+++ b/src/tint/resolver/alias_analysis_test.cc
@@ -18,11 +18,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 struct ResolverAliasAnalysisTest : public resolver::TestHelper, public testing::Test {};
 
 // Base test harness for tests that pass two pointers to a function.
@@ -726,8 +727,8 @@
     Structure("S", utils::Vector{Member("a", ty.i32())});
     Func("f2",
          utils::Vector{
-             Param("p1", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
-             Param("p2", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
+             Param("p1", ty.ptr<function>(ty("S"))),
+             Param("p2", ty.ptr<function>(ty("S"))),
          },
          ty.void_(),
          utils::Vector{
@@ -755,8 +756,8 @@
     Structure("S", utils::Vector{Member("a", ty.i32())});
     Func("f2",
          utils::Vector{
-             Param("p1", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
-             Param("p2", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
+             Param("p1", ty.ptr<function>(ty("S"))),
+             Param("p2", ty.ptr<function>(ty("S"))),
          },
          ty.void_(),
          utils::Vector{
@@ -787,8 +788,8 @@
     Structure("S", utils::Vector{Member("a", ty.i32())});
     Func("f2",
          utils::Vector{
-             Param("p1", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
-             Param("p2", ty.ptr(builtin::AddressSpace::kFunction, ty("S"))),
+             Param("p1", ty.ptr<function>(ty("S"))),
+             Param("p2", ty.ptr<function>(ty("S"))),
          },
          ty.void_(),
          utils::Vector{
@@ -818,13 +819,13 @@
     Structure("S", utils::Vector{Member("a", ty.i32())});
     Func("f2",
          utils::Vector{
-             Param("p1", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>())),
-             Param("p2", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>())),
+             Param("p1", ty.ptr<function, vec4<f32>>()),
+             Param("p2", ty.ptr<function, vec4<f32>>()),
          },
          ty.void_(),
          utils::Vector{
              Assign(Phony(), MemberAccessor(Deref("p2"), "zy")),
-             Assign(Deref("p1"), Call(ty.vec4<f32>())),
+             Assign(Deref("p1"), Call<vec4<f32>>()),
          });
     Func("f1", utils::Empty, ty.void_(),
          utils::Vector{
diff --git a/src/tint/resolver/array_accessor_test.cc b/src/tint/resolver/array_accessor_test.cc
index 118bcff..3b12afd 100644
--- a/src/tint/resolver/array_accessor_test.cc
+++ b/src/tint/resolver/array_accessor_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/sem/index_accessor_expression.h"
 #include "src/tint/type/reference.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverIndexAccessorTest = ResolverTest;
 
 TEST_F(ResolverIndexAccessorTest, Matrix_Dynamic_F32) {
@@ -65,7 +66,7 @@
 }
 
 TEST_F(ResolverIndexAccessorTest, Matrix_Dynamic) {
-    GlobalConst("my_const", ty.mat2x3<f32>(), Call(ty.mat2x3<f32>()));
+    GlobalConst("my_const", ty.mat2x3<f32>(), Call<mat2x3<f32>>());
     auto* idx = Var("idx", ty.i32(), Call<i32>());
     auto* acc = IndexAccessor("my_const", Expr(Source{{12, 34}}, idx));
     WrapInFunction(Decl(idx), acc);
@@ -80,7 +81,7 @@
 }
 
 TEST_F(ResolverIndexAccessorTest, Matrix_XDimension_Dynamic) {
-    GlobalConst("my_const", ty.mat4x4<f32>(), Call(ty.mat4x4<f32>()));
+    GlobalConst("my_const", ty.mat4x4<f32>(), Call<mat4x4<f32>>());
     auto* idx = Var("idx", ty.u32(), Expr(3_u));
     auto* acc = IndexAccessor("my_const", Expr(Source{{12, 34}}, idx));
     WrapInFunction(Decl(idx), acc);
@@ -90,7 +91,7 @@
 }
 
 TEST_F(ResolverIndexAccessorTest, Matrix_BothDimension_Dynamic) {
-    GlobalConst("my_const", ty.mat4x4<f32>(), Call(ty.mat4x4<f32>()));
+    GlobalConst("my_const", ty.mat4x4<f32>(), Call<mat4x4<f32>>());
     auto* idx = Var("idy", ty.u32(), Expr(2_u));
     auto* acc = IndexAccessor(IndexAccessor("my_const", Expr(Source{{12, 34}}, idx)), 1_i);
     WrapInFunction(Decl(idx), acc);
@@ -158,7 +159,7 @@
 }
 
 TEST_F(ResolverIndexAccessorTest, Vector_Dynamic) {
-    GlobalConst("my_const", ty.vec3<f32>(), Call(ty.vec3<f32>()));
+    GlobalConst("my_const", ty.vec3<f32>(), Call<vec3<f32>>());
     auto* idx = Var("idx", ty.i32(), Expr(2_i));
     auto* acc = IndexAccessor("my_const", Expr(Source{{12, 34}}, idx));
     WrapInFunction(Decl(idx), acc);
@@ -242,7 +243,7 @@
 }
 
 TEST_F(ResolverIndexAccessorTest, Array_Constant) {
-    GlobalConst("my_const", ty.array<f32, 3>(), array<f32, 3>());
+    GlobalConst("my_const", ty.array<f32, 3>(), Call<array<f32, 3>>());
 
     auto* acc = IndexAccessor("my_const", 2_i);
     WrapInFunction(acc);
@@ -257,7 +258,7 @@
     // let a : array<f32, 3> = 0;
     // var idx : i32 = 0;
     // var f : f32 = a[idx];
-    auto* a = Let("a", ty.array<f32, 3>(), array<f32, 3>());
+    auto* a = Let("a", ty.array<f32, 3>(), Call<array<f32, 3>>());
     auto* idx = Var("idx", ty.i32(), Call<i32>());
     auto* acc = IndexAccessor("a", Expr(Source{{12, 34}}, idx));
     auto* f = Var("f", ty.f32(), acc);
@@ -280,7 +281,7 @@
 TEST_F(ResolverIndexAccessorTest, Array_Literal_F32) {
     // let a : array<f32, 3>;
     // var f : f32 = a[2.0f];
-    auto* a = Let("a", ty.array<f32, 3>(), array<f32, 3>());
+    auto* a = Let("a", ty.array<f32, 3>(), Call<array<f32, 3>>());
     auto* f = Var("a_2", ty.f32(), IndexAccessor("a", Expr(Source{{12, 34}}, 2_f)));
     Func("my_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -294,7 +295,7 @@
 TEST_F(ResolverIndexAccessorTest, Array_Literal_I32) {
     // let a : array<f32, 3>;
     // var f : f32 = a[2i];
-    auto* a = Let("a", ty.array<f32, 3>(), array<f32, 3>());
+    auto* a = Let("a", ty.array<f32, 3>(), Call<array<f32, 3>>());
     auto* acc = IndexAccessor("a", 2_i);
     auto* f = Var("a_2", ty.f32(), acc);
     Func("my_func", utils::Empty, ty.void_(),
@@ -316,7 +317,7 @@
     //     let x: f32 = (*p)[idx];
     //     return x;
     // }
-    auto* p = Param("p", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()));
+    auto* p = Param("p", ty.ptr<function, vec4<f32>>());
     auto* idx = Let("idx", ty.u32(), Call<u32>());
     auto* star_p = Deref(p);
     auto* acc = IndexAccessor(Source{{12, 34}}, star_p, idx);
@@ -337,7 +338,7 @@
     //     let x: f32 = *p[idx];
     //     return x;
     // }
-    auto* p = Param("p", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()));
+    auto* p = Param("p", ty.ptr<function, vec4<f32>>());
     auto* idx = Let("idx", ty.u32(), Call<u32>());
     auto* accessor_expr = IndexAccessor(Source{{12, 34}}, p, idx);
     auto* star_p = Deref(accessor_expr);
diff --git a/src/tint/resolver/assignment_validation_test.cc b/src/tint/resolver/assignment_validation_test.cc
index bd4cd71..f1251cb 100644
--- a/src/tint/resolver/assignment_validation_test.cc
+++ b/src/tint/resolver/assignment_validation_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/type/storage_texture.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverAssignmentValidationTest = ResolverTest;
 
 TEST_F(ResolverAssignmentValidationTest, ReadOnlyBuffer) {
@@ -182,9 +183,8 @@
     // var a : i32;
     // let b : ptr<function,i32> = &a;
     // *b = 2i;
-    const auto func = builtin::AddressSpace::kFunction;
-    WrapInFunction(Var("a", ty.i32(), func, Expr(2_i)),                //
-                   Let("b", ty.ptr<i32>(func), AddressOf(Expr("a"))),  //
+    WrapInFunction(Var("a", ty.i32(), builtin::AddressSpace::kFunction, Expr(2_i)),  //
+                   Let("b", ty.ptr<function, i32>(), AddressOf(Expr("a"))),          //
                    Assign(Deref("b"), 2_i));
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -194,9 +194,8 @@
     // var a : i32;
     // let b : ptr<function,i32> = &a;
     // *b = 2;
-    const auto func = builtin::AddressSpace::kFunction;
-    auto* var_a = Var("a", ty.i32(), func, Expr(2_i));
-    auto* var_b = Let("b", ty.ptr<i32>(func), AddressOf(Expr("a")));
+    auto* var_a = Var("a", ty.i32(), builtin::AddressSpace::kFunction, Expr(2_i));
+    auto* var_b = Let("b", ty.ptr<function, i32>(), AddressOf(Expr("a")));
     WrapInFunction(var_a, var_b, Assign(Deref("b"), 2_a));
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -433,9 +432,9 @@
                    Assign(Phony(), 3_f),                                    //
                    Assign(Phony(), 4_a),                                    //
                    Assign(Phony(), 5.0_a),                                  //
-                   Assign(Phony(), vec2<Infer>(6_a)),                       //
-                   Assign(Phony(), vec3<Infer>(7.0_a)),                     //
-                   Assign(Phony(), vec4<bool>()),                           //
+                   Assign(Phony(), Call<vec2<Infer>>(6_a)),                 //
+                   Assign(Phony(), Call<vec3<Infer>>(7.0_a)),               //
+                   Assign(Phony(), Call<vec4<bool>>()),                     //
                    Assign(Phony(), "tex"),                                  //
                    Assign(Phony(), "smp"),                                  //
                    Assign(Phony(), AddressOf("s")),                         //
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 97af866..5997b71 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -22,25 +22,14 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 // Helpers and typedefs
 template <typename T>
 using DataType = builder::DataType<T>;
-template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
 template <typename T, int ID = 0>
 using alias = builder::alias<T, ID>;
 template <typename T>
@@ -353,7 +342,7 @@
     auto* p = Param("a", ty.vec4<f32>(), attrs);
     Func("vertex_main", utils::Vector{p}, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
@@ -404,7 +393,7 @@
     auto& params = GetParam();
     Func("main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>(), 1_f)),
+             Return(Call<vec4<f32>>(1_f)),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kCompute),
@@ -456,7 +445,7 @@
     auto& params = GetParam();
     auto attrs = createAttributes(Source{{12, 34}}, *this, params.kind);
     attrs.Push(Location(Source{{34, 56}}, 2_a));
-    Func("frag_main", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(Call(ty.vec4<f32>()))},
+    Func("frag_main", utils::Empty, ty.vec4<f32>(), utils::Vector{Return(Call<vec4<f32>>())},
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
          },
@@ -513,7 +502,7 @@
     }
     Func("vertex_main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
@@ -1602,8 +1591,10 @@
 
     Func("F", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Var("a", ty.vec4<f32>(), Call("textureLoad", "A", vec2<i32>(1_i, 2_i), 0_i))),
-             Decl(Var("b", ty.vec4<f32>(), Call("textureLoad", "B", vec2<i32>(1_i, 2_i), 0_i))),
+             Decl(Var("a", ty.vec4<f32>(),
+                      Call("textureLoad", "A", Call<vec2<i32>>(1_i, 2_i), 0_i))),
+             Decl(Var("b", ty.vec4<f32>(),
+                      Call("textureLoad", "B", Call<vec2<i32>>(1_i, 2_i), 0_i))),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -1624,14 +1615,16 @@
 
     Func("F_A", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Var("a", ty.vec4<f32>(), Call("textureLoad", "A", vec2<i32>(1_i, 2_i), 0_i))),
+             Decl(Var("a", ty.vec4<f32>(),
+                      Call("textureLoad", "A", Call<vec2<i32>>(1_i, 2_i), 0_i))),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
          });
     Func("F_B", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Var("b", ty.vec4<f32>(), Call("textureLoad", "B", vec2<i32>(1_i, 2_i), 0_i))),
+             Decl(Var("b", ty.vec4<f32>(),
+                      Call("textureLoad", "B", Call<vec2<i32>>(1_i, 2_i), 0_i))),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -1663,7 +1656,7 @@
                         });
     Func("main", utils::Vector{param}, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -1682,7 +1675,7 @@
                         });
     Func("main", utils::Vector{param}, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -1705,7 +1698,7 @@
 TEST_F(MustUseAttributeTests, MustUse) {
     Func("main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              MustUse(Source{{12, 34}}),
@@ -1964,7 +1957,7 @@
 TEST_F(InterpolateTest, MissingLocationAttribute_ReturnType) {
     Func("main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
diff --git a/src/tint/resolver/bitcast_validation_test.cc b/src/tint/resolver/bitcast_validation_test.cc
index fb0b960..c979c55 100644
--- a/src/tint/resolver/bitcast_validation_test.cc
+++ b/src/tint/resolver/bitcast_validation_test.cc
@@ -21,6 +21,8 @@
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+
 struct Type {
     template <typename T>
     static constexpr Type Create() {
@@ -39,36 +41,36 @@
     Type::Create<u32>(),
 };
 static constexpr Type kVec2NumericScalars[] = {
-    Type::Create<builder::vec2<f32>>(),
-    Type::Create<builder::vec2<i32>>(),
-    Type::Create<builder::vec2<u32>>(),
+    Type::Create<vec2<f32>>(),
+    Type::Create<vec2<i32>>(),
+    Type::Create<vec2<u32>>(),
 };
 static constexpr Type kVec3NumericScalars[] = {
-    Type::Create<builder::vec3<f32>>(),
-    Type::Create<builder::vec3<i32>>(),
-    Type::Create<builder::vec3<u32>>(),
+    Type::Create<vec3<f32>>(),
+    Type::Create<vec3<i32>>(),
+    Type::Create<vec3<u32>>(),
 };
 static constexpr Type kVec4NumericScalars[] = {
-    Type::Create<builder::vec4<f32>>(),
-    Type::Create<builder::vec4<i32>>(),
-    Type::Create<builder::vec4<u32>>(),
+    Type::Create<vec4<f32>>(),
+    Type::Create<vec4<i32>>(),
+    Type::Create<vec4<u32>>(),
 };
 static constexpr Type kInvalid[] = {
     // A non-exhaustive selection of uncastable types
     Type::Create<bool>(),
-    Type::Create<builder::vec2<bool>>(),
-    Type::Create<builder::vec3<bool>>(),
-    Type::Create<builder::vec4<bool>>(),
-    Type::Create<builder::array<2, i32>>(),
-    Type::Create<builder::array<3, u32>>(),
-    Type::Create<builder::array<4, f32>>(),
-    Type::Create<builder::array<5, bool>>(),
-    Type::Create<builder::mat2x2<f32>>(),
-    Type::Create<builder::mat3x3<f32>>(),
-    Type::Create<builder::mat4x4<f32>>(),
-    Type::Create<builder::ptr<i32>>(),
-    Type::Create<builder::ptr<builder::array<2, i32>>>(),
-    Type::Create<builder::ptr<builder::mat2x2<f32>>>(),
+    Type::Create<vec2<bool>>(),
+    Type::Create<vec3<bool>>(),
+    Type::Create<vec4<bool>>(),
+    Type::Create<array<i32, 2>>(),
+    Type::Create<array<u32, 3>>(),
+    Type::Create<array<f32, 4>>(),
+    Type::Create<array<bool, 5>>(),
+    Type::Create<mat2x2<f32>>(),
+    Type::Create<mat3x3<f32>>(),
+    Type::Create<mat4x4<f32>>(),
+    Type::Create<ptr<private_, i32>>(),
+    Type::Create<ptr<private_, array<i32, 2>>>(),
+    Type::Create<ptr<private_, mat2x2<f32>>>(),
 };
 
 using ResolverBitcastValidationTest = ResolverTestWithParam<std::tuple<Type, Type>>;
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 5a23354..07abb21 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -43,11 +43,12 @@
 using ::testing::ElementsAre;
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ExpressionList = utils::Vector<const ast::Expression*, 8>;
 
 using ResolverBuiltinTest = ResolverTest;
@@ -156,8 +157,9 @@
 }
 
 TEST_F(ResolverBuiltinTest, Select_Error_Matrix) {
-    auto* expr = Call("select", mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)),
-                      mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)), Expr(true));
+    auto* expr =
+        Call("select", Call<mat2x2<f32>>(Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f)),
+             Call<mat2x2<f32>>(Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f)), Expr(true));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
@@ -173,7 +175,7 @@
 }
 
 TEST_F(ResolverBuiltinTest, Select_Error_MismatchTypes) {
-    auto* expr = Call("select", 1_f, vec2<f32>(2_f, 3_f), Expr(true));
+    auto* expr = Call("select", 1_f, Call<vec2<f32>>(2_f, 3_f), Expr(true));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
@@ -189,7 +191,7 @@
 }
 
 TEST_F(ResolverBuiltinTest, Select_Error_MismatchVectorSize) {
-    auto* expr = Call("select", vec2<f32>(1_f, 2_f), vec3<f32>(3_f, 4_f, 5_f), Expr(true));
+    auto* expr = Call("select", Call<vec2<f32>>(1_f, 2_f), Call<vec3<f32>>(3_f, 4_f, 5_f), true);
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
@@ -304,8 +306,8 @@
 TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, OneParam_Vector_f32) {
     auto param = GetParam();
 
-    auto val = param.name == std::string("acosh") ? vec3<f32>(1.0_f, 2.0_f, 3.0_f)
-                                                  : vec3<f32>(0.5_f, 0.5_f, 0.8_f);
+    auto val = param.name == std::string("acosh") ? Call<vec3<f32>>(1.0_f, 2.0_f, 3.0_f)
+                                                  : Call<vec3<f32>>(0.5_f, 0.5_f, 0.8_f);
 
     auto* call = Call(param.name, val);
     WrapInFunction(call);
@@ -352,7 +354,7 @@
 TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, TwoParams_Vector_f32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call(param.name, Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     if (param.args_number == 2u) {
@@ -397,8 +399,8 @@
 TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, ThreeParams_Vector_f32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<f32>(0_f, 0_f, 0_f), vec3<f32>(1_f, 1_f, 1_f),
-                      vec3<f32>(2_f, 2_f, 2_f));
+    auto* call = Call(param.name, Call<vec3<f32>>(0_f, 0_f, 0_f), Call<vec3<f32>>(1_f, 1_f, 1_f),
+                      Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(call);
 
     if (param.args_number == 3u) {
@@ -444,8 +446,8 @@
 TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, FourParams_Vector_f32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
-                      vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call(param.name, Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f),
+                      Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     if (param.args_number == 4u) {
@@ -500,8 +502,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto val = param.name == std::string("acosh") ? vec3<f16>(1.0_h, 2.0_h, 3.0_h)
-                                                  : vec3<f16>(0.5_h, 0.5_h, 0.8_h);
+    auto val = param.name == std::string("acosh") ? Call<vec3<f16>>(1.0_h, 2.0_h, 3.0_h)
+                                                  : Call<vec3<f16>>(0.5_h, 0.5_h, 0.8_h);
 
     auto* call = Call(param.name, val);
     WrapInFunction(call);
@@ -552,7 +554,7 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call(param.name, vec3<f16>(1_h, 1_h, 3_h), vec3<f16>(1_h, 1_h, 3_h));
+    auto* call = Call(param.name, Call<vec3<f16>>(1_h, 1_h, 3_h), Call<vec3<f16>>(1_h, 1_h, 3_h));
     WrapInFunction(call);
 
     if (param.args_number == 2u) {
@@ -601,8 +603,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call(param.name, vec3<f16>(0_h, 0_h, 0_h), vec3<f16>(1_h, 1_h, 1_h),
-                      vec3<f16>(2_h, 2_h, 2_h));
+    auto* call = Call(param.name, Call<vec3<f16>>(0_h, 0_h, 0_h), Call<vec3<f16>>(1_h, 1_h, 1_h),
+                      Call<vec3<f16>>(2_h, 2_h, 2_h));
     WrapInFunction(call);
 
     if (param.args_number == 3u) {
@@ -652,8 +654,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call(param.name, vec3<f16>(1_h, 1_h, 3_h), vec3<f16>(1_h, 1_h, 3_h),
-                      vec3<f16>(1_h, 1_h, 3_h), vec3<f16>(1_h, 1_h, 3_h));
+    auto* call = Call(param.name, Call<vec3<f16>>(1_h, 1_h, 3_h), Call<vec3<f16>>(1_h, 1_h, 3_h),
+                      Call<vec3<f16>>(1_h, 1_h, 3_h), Call<vec3<f16>>(1_h, 1_h, 3_h));
     WrapInFunction(call);
 
     if (param.args_number == 4u) {
@@ -732,7 +734,7 @@
 
 // cross: (vec3<T>, vec3<T>) -> vec3<T>
 TEST_F(ResolverBuiltinFloatTest, Cross_f32) {
-    auto* call = Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
+    auto* call = Call("cross", Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(1_f, 2_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -746,7 +748,7 @@
 TEST_F(ResolverBuiltinFloatTest, Cross_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("cross", vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(1_h, 2_h, 3_h));
+    auto* call = Call("cross", Call<vec3<f16>>(1_h, 2_h, 3_h), Call<vec3<f16>>(1_h, 2_h, 3_h));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -784,7 +786,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Cross_Error_Vec3Int) {
-    auto* call = Call("cross", vec3<i32>(1_i, 2_i, 3_i), vec3<i32>(1_i, 2_i, 3_i));
+    auto* call = Call("cross", Call<vec3<i32>>(1_i, 2_i, 3_i), Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -798,7 +800,8 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Cross_Error_Vec4) {
-    auto* call = Call("cross", vec4<f32>(1_f, 2_f, 3_f, 4_f), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+    auto* call =
+        Call("cross", Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f), Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
 
     WrapInFunction(call);
 
@@ -813,8 +816,8 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Cross_Error_TooManyParams) {
-    auto* call =
-        Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
+    auto* call = Call("cross", Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(1_f, 2_f, 3_f),
+                      Call<vec3<f32>>(1_f, 2_f, 3_f));
 
     WrapInFunction(call);
 
@@ -852,7 +855,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Distance_Vector_f32) {
-    auto* call = Call("distance", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call("distance", Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -864,7 +867,7 @@
 TEST_F(ResolverBuiltinFloatTest, Distance_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("distance", vec3<f16>(1_h, 1_h, 3_h), vec3<f16>(1_h, 1_h, 3_h));
+    auto* call = Call("distance", Call<vec3<f16>>(1_h, 1_h, 3_h), Call<vec3<f16>>(1_h, 1_h, 3_h));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -874,8 +877,8 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Distance_TooManyParams) {
-    auto* call = Call("distance", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
-                      vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call("distance", Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f),
+                      Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -889,7 +892,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Distance_TooFewParams) {
-    auto* call = Call("distance", vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call("distance", Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -979,7 +982,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, FrexpVector_f32) {
-    auto* call = Call("frexp", vec3<f32>());
+    auto* call = Call("frexp", Call<vec3<f32>>());
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1014,7 +1017,7 @@
 TEST_F(ResolverBuiltinFloatTest, FrexpVector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("frexp", vec3<f16>());
+    auto* call = Call("frexp", Call<vec3<f16>>());
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1086,7 +1089,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, Length_FloatVector_f32) {
-    auto* call = Call("length", vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call("length", Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1098,7 +1101,7 @@
 TEST_F(ResolverBuiltinFloatTest, Length_FloatVector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("length", vec3<f16>(1_h, 1_h, 3_h));
+    auto* call = Call("length", Call<vec3<f16>>(1_h, 1_h, 3_h));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1138,7 +1141,7 @@
 // mix(vecN<T>, vecN<T>, T) -> vecN<T>. Other overloads are tested in
 // ResolverBuiltinTest_FloatBuiltin_IdenticalType above.
 TEST_F(ResolverBuiltinFloatTest, Mix_VectorScalar_f32) {
-    auto* call = Call("mix", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f), 4_f);
+    auto* call = Call("mix", Call<vec3<f32>>(1_f, 1_f, 3_f), Call<vec3<f32>>(1_f, 1_f, 3_f), 4_f);
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1153,7 +1156,7 @@
 TEST_F(ResolverBuiltinFloatTest, Mix_VectorScalar_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("mix", vec3<f16>(1_h, 1_h, 1_h), vec3<f16>(1_h, 1_h, 1_h), 4_h);
+    auto* call = Call("mix", Call<vec3<f16>>(1_h, 1_h, 1_h), Call<vec3<f16>>(1_h, 1_h, 1_h), 4_h);
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1228,7 +1231,7 @@
 }
 
 TEST_F(ResolverBuiltinFloatTest, ModfVector_f32) {
-    auto* call = Call("modf", vec3<f32>());
+    auto* call = Call("modf", Call<vec3<f32>>());
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1263,7 +1266,7 @@
 TEST_F(ResolverBuiltinFloatTest, ModfVector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("modf", vec3<f16>());
+    auto* call = Call("modf", Call<vec3<f16>>());
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1343,7 +1346,7 @@
 
 TEST_F(ResolverBuiltinFloatTest, Modf_Error_VectorSizesDontMatch) {
     GlobalVar("whole", ty.vec4<f32>(), builtin::AddressSpace::kWorkgroup);
-    auto* call = Call("modf", vec2<f32>(1_f, 2_f), AddressOf("whole"));
+    auto* call = Call("modf", Call<vec2<f32>>(1_f, 2_f), AddressOf("whole"));
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -1359,7 +1362,7 @@
 
 // normalize: (vecN<T>) -> vecN<T>
 TEST_F(ResolverBuiltinFloatTest, Normalize_Vector_f32) {
-    auto* call = Call("normalize", vec3<f32>(1_f, 1_f, 3_f));
+    auto* call = Call("normalize", Call<vec3<f32>>(1_f, 1_f, 3_f));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1373,7 +1376,7 @@
 TEST_F(ResolverBuiltinFloatTest, Normalize_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* call = Call("normalize", vec3<f16>(1_h, 1_h, 3_h));
+    auto* call = Call("normalize", Call<vec3<f16>>(1_h, 1_h, 3_h));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1455,7 +1458,7 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Vector_i32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i));
+    auto* call = Call(param.name, Call<vec3<i32>>(1_i, 1_i, 3_i));
     WrapInFunction(call);
 
     if (param.args_number == 1u) {
@@ -1500,7 +1503,7 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Vector_u32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u));
+    auto* call = Call(param.name, Call<vec3<u32>>(1_u, 1_u, 3_u));
     WrapInFunction(call);
 
     if (param.args_number == 1u) {
@@ -1545,7 +1548,7 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Vector_i32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i));
+    auto* call = Call(param.name, Call<vec3<i32>>(1_i, 1_i, 3_i), Call<vec3<i32>>(1_i, 1_i, 3_i));
     WrapInFunction(call);
 
     if (param.args_number == 2u) {
@@ -1590,7 +1593,7 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Vector_u32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u));
+    auto* call = Call(param.name, Call<vec3<u32>>(1_u, 1_u, 3_u), Call<vec3<u32>>(1_u, 1_u, 3_u));
     WrapInFunction(call);
 
     if (param.args_number == 2u) {
@@ -1635,8 +1638,8 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Vector_i32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i),
-                      vec3<i32>(1_i, 1_i, 3_i));
+    auto* call = Call(param.name, Call<vec3<i32>>(1_i, 1_i, 3_i), Call<vec3<i32>>(1_i, 1_i, 3_i),
+                      Call<vec3<i32>>(1_i, 1_i, 3_i));
     WrapInFunction(call);
 
     if (param.args_number == 3u) {
@@ -1682,8 +1685,8 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Vector_u32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u),
-                      vec3<u32>(1_u, 1_u, 3_u));
+    auto* call = Call(param.name, Call<vec3<u32>>(1_u, 1_u, 3_u), Call<vec3<u32>>(1_u, 1_u, 3_u),
+                      Call<vec3<u32>>(1_u, 1_u, 3_u));
     WrapInFunction(call);
 
     if (param.args_number == 3u) {
@@ -1729,8 +1732,8 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Vector_i32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i),
-                      vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i));
+    auto* call = Call(param.name, Call<vec3<i32>>(1_i, 1_i, 3_i), Call<vec3<i32>>(1_i, 1_i, 3_i),
+                      Call<vec3<i32>>(1_i, 1_i, 3_i), Call<vec3<i32>>(1_i, 1_i, 3_i));
     WrapInFunction(call);
 
     if (param.args_number == 4u) {
@@ -1776,8 +1779,8 @@
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Vector_u32) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u),
-                      vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u));
+    auto* call = Call(param.name, Call<vec3<u32>>(1_u, 1_u, 3_u), Call<vec3<u32>>(1_u, 1_u, 3_u),
+                      Call<vec3<u32>>(1_u, 1_u, 3_u), Call<vec3<u32>>(1_u, 1_u, 3_u));
     WrapInFunction(call);
 
     if (param.args_number == 4u) {
@@ -2188,10 +2191,9 @@
                          ResolverBuiltinTest_Texture,
                          testing::ValuesIn(ast::test::TextureOverloadCase::ValidCases()));
 
-static std::string to_str(const std::string& function,
-                          utils::VectorRef<const sem::Parameter*> params) {
+static std::string to_str(const std::string& func, utils::VectorRef<const sem::Parameter*> params) {
     utils::StringStream out;
-    out << function << "(";
+    out << func << "(";
     bool first = true;
     for (auto* param : params) {
         if (!first) {
@@ -2556,8 +2558,8 @@
     bool pack4 = param.builtin == builtin::Function::kPack4X8Snorm ||
                  param.builtin == builtin::Function::kPack4X8Unorm;
 
-    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f))
-                       : Call(param.name, vec2<f32>(1_f, 2_f));
+    auto* call = pack4 ? Call(param.name, Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f))
+                       : Call(param.name, Call<vec2<f32>>(1_f, 2_f));
     WrapInFunction(call);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2571,8 +2573,8 @@
     bool pack4 = param.builtin == builtin::Function::kPack4X8Snorm ||
                  param.builtin == builtin::Function::kPack4X8Unorm;
 
-    auto* call = pack4 ? Call(param.name, vec4<i32>(1_i, 2_i, 3_i, 4_i))
-                       : Call(param.name, vec2<i32>(1_i, 2_i));
+    auto* call = pack4 ? Call(param.name, Call<vec4<i32>>(1_i, 2_i, 3_i, 4_i))
+                       : Call(param.name, Call<vec2<i32>>(1_i, 2_i));
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -2597,8 +2599,8 @@
     bool pack4 = param.builtin == builtin::Function::kPack4X8Snorm ||
                  param.builtin == builtin::Function::kPack4X8Unorm;
 
-    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f)
-                       : Call(param.name, vec2<f32>(1_f, 2_f), 1_f);
+    auto* call = pack4 ? Call(param.name, Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f), 1_f)
+                       : Call(param.name, Call<vec2<f32>>(1_f, 2_f), 1_f);
     WrapInFunction(call);
 
     EXPECT_FALSE(r()->Resolve());
@@ -2669,7 +2671,7 @@
 TEST_P(ResolverBuiltinTest_Barrier, Error_TooManyParams) {
     auto param = GetParam();
 
-    auto* call = Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f);
+    auto* call = Call(param.name, Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f), 1_f);
     WrapInFunction(CallStmt(call));
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/builtin_validation_test.cc b/src/tint/resolver/builtin_validation_test.cc
index fbe5a5f..3996aa9 100644
--- a/src/tint/resolver/builtin_validation_test.cc
+++ b/src/tint/resolver/builtin_validation_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/sem/value_constructor.h"
 #include "src/tint/utils/string_stream.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverBuiltinValidationTest = ResolverTest;
 
 TEST_F(ResolverBuiltinValidationTest, FunctionTypeMustMatchReturnStatementType_void_fail) {
@@ -250,10 +251,10 @@
                               i32(values[2]));
             case Kind::kVec3_Scalar_Vec2:
                 return b.Call(src, b.ty.vec3<i32>(), i32(values[0]),
-                              b.vec2<i32>(i32(values[1]), i32(values[2])));
+                              b.Call<vec2<i32>>(i32(values[1]), i32(values[2])));
             case Kind::kVec3_Vec2_Scalar:
-                return b.Call(src, b.ty.vec3<i32>(), b.vec2<i32>(i32(values[0]), i32(values[1])),
-                              i32(values[2]));
+                return b.Call(src, b.ty.vec3<i32>(),
+                              b.Call<vec2<i32>>(i32(values[0]), i32(values[1])), i32(values[2]));
             case Kind::kEmptyVec2:
                 return b.Call(src, b.ty.vec2<i32>());
             case Kind::kEmptyVec3:
diff --git a/src/tint/resolver/builtins_validation_test.cc b/src/tint/resolver/builtins_validation_test.cc
index d5f8c4f..a1cc2e2 100644
--- a/src/tint/resolver/builtins_validation_test.cc
+++ b/src/tint/resolver/builtins_validation_test.cc
@@ -17,20 +17,14 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/utils/string_stream.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 template <typename T>
 using DataType = builder::DataType<T>;
-template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-
 class ResolverBuiltinsValidationTest : public resolver::TestHelper, public testing::Test {};
 namespace StageTest {
 struct Params {
@@ -870,21 +864,21 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Length_Float_Vec2) {
-    auto* builtin = Call("length", vec2<f32>(1_f, 1_f));
+    auto* builtin = Call("length", Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Length_Float_Vec3) {
-    auto* builtin = Call("length", vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin = Call("length", Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Length_Float_Vec4) {
-    auto* builtin = Call("length", vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin = Call("length", Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -898,46 +892,50 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Distance_Float_Vec2) {
-    auto* builtin = Call("distance", vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f));
+    auto* builtin = Call("distance", Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Distance_Float_Vec3) {
-    auto* builtin = Call("distance", vec3<f32>(1_f, 1_f, 1_f), vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin =
+        Call("distance", Call<vec3<f32>>(1_f, 1_f, 1_f), Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Distance_Float_Vec4) {
-    auto* builtin = Call("distance", vec4<f32>(1_f, 1_f, 1_f, 1_f), vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin =
+        Call("distance", Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f), Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Determinant_Mat2x2) {
-    auto* builtin = Call("determinant", mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)));
+    auto* builtin = Call("determinant",
+                         Call<mat2x2<f32>>(Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f)));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Determinant_Mat3x3) {
-    auto* builtin = Call(
-        "determinant",
-        mat3x3<f32>(vec3<f32>(1_f, 1_f, 1_f), vec3<f32>(1_f, 1_f, 1_f), vec3<f32>(1_f, 1_f, 1_f)));
+    auto* builtin = Call("determinant", Call<mat3x3<f32>>(Call<vec3<f32>>(1_f, 1_f, 1_f),
+                                                          Call<vec3<f32>>(1_f, 1_f, 1_f),
+                                                          Call<vec3<f32>>(1_f, 1_f, 1_f)));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Determinant_Mat4x4) {
-    auto* builtin = Call("determinant",
-                         mat4x4<f32>(vec4<f32>(1_f, 1_f, 1_f, 1_f), vec4<f32>(1_f, 1_f, 1_f, 1_f),
-                                     vec4<f32>(1_f, 1_f, 1_f, 1_f), vec4<f32>(1_f, 1_f, 1_f, 1_f)));
+    auto* builtin = Call("determinant", Call<mat4x4<f32>>(Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f),
+                                                          Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f),
+                                                          Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f),
+                                                          Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f)));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -957,7 +955,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Frexp_Vec2) {
-    auto* builtin = Call("frexp", vec2<f32>(1_f, 1_f));
+    auto* builtin = Call("frexp", Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -974,7 +972,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Frexp_Vec3) {
-    auto* builtin = Call("frexp", vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin = Call("frexp", Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -991,7 +989,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Frexp_Vec4) {
-    auto* builtin = Call("frexp", vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin = Call("frexp", Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1021,7 +1019,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Modf_Vec2) {
-    auto* builtin = Call("modf", vec2<f32>(1_f, 1_f));
+    auto* builtin = Call("modf", Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1038,7 +1036,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Modf_Vec3) {
-    auto* builtin = Call("modf", vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin = Call("modf", Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1055,7 +1053,7 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Modf_Vec4) {
-    auto* builtin = Call("modf", vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin = Call("modf", Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1072,28 +1070,29 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Cross_Float_Vec3) {
-    auto* builtin = Call("cross", vec3<f32>(1_f, 1_f, 1_f), vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin = Call("cross", Call<vec3<f32>>(1_f, 1_f, 1_f), Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Dot_Float_Vec2) {
-    auto* builtin = Call("dot", vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f));
+    auto* builtin = Call("dot", Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Dot_Float_Vec3) {
-    auto* builtin = Call("dot", vec3<f32>(1_f, 1_f, 1_f), vec3<f32>(1_f, 1_f, 1_f));
+    auto* builtin = Call("dot", Call<vec3<f32>>(1_f, 1_f, 1_f), Call<vec3<f32>>(1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Dot_Float_Vec4) {
-    auto* builtin = Call("dot", vec4<f32>(1_f, 1_f, 1_f, 1_f), vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin =
+        Call("dot", Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f), Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1121,24 +1120,24 @@
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Select_Float_Vec2) {
-    auto* builtin =
-        Call("select", vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f), vec2<bool>(true, true));
+    auto* builtin = Call("select", Call<vec2<f32>>(1_f, 1_f), Call<vec2<f32>>(1_f, 1_f),
+                         Call<vec2<bool>>(true, true));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Select_Integer_Vec2) {
-    auto* builtin =
-        Call("select", vec2<i32>(1_i, 1_i), vec2<i32>(1_i, 1_i), vec2<bool>(true, true));
+    auto* builtin = Call("select", Call<vec2<i32>>(1_i, 1_i), Call<vec2<i32>>(1_i, 1_i),
+                         Call<vec2<bool>>(true, true));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverBuiltinsValidationTest, Select_Boolean_Vec2) {
-    auto* builtin =
-        Call("select", vec2<bool>(true, true), vec2<bool>(true, true), vec2<bool>(true, true));
+    auto* builtin = Call("select", Call<vec2<bool>>(true, true), Call<vec2<bool>>(true, true),
+                         Call<vec2<bool>>(true, true));
     WrapInFunction(builtin);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1178,7 +1177,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec2<f32>(f32(i + 1), f32(i + 1)));
+        params.Push(Call<vec2<f32>>(f32(i + 1), f32(i + 1)));
     }
     auto* builtin = Call(name, params);
     Func("func", utils::Empty, ty.void_(),
@@ -1199,7 +1198,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec3<f32>(f32(i + 1), f32(i + 1), f32(i + 1)));
+        params.Push(Call<vec3<f32>>(f32(i + 1), f32(i + 1), f32(i + 1)));
     }
     auto* builtin = Call(name, params);
     Func("func", utils::Empty, ty.void_(),
@@ -1220,7 +1219,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec4<f32>(f32(i + 1), f32(i + 1), f32(i + 1), f32(i + 1)));
+        params.Push(Call<vec4<f32>>(f32(i + 1), f32(i + 1), f32(i + 1), f32(i + 1)));
     }
     auto* builtin = Call(name, params);
     Func("func", utils::Empty, ty.void_(),
@@ -1302,7 +1301,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec2<u32>(1_u, 1_u));
+        params.Push(Call<vec2<u32>>(1_u, 1_u));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1317,7 +1316,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec3<u32>(1_u, 1_u, 1_u));
+        params.Push(Call<vec3<u32>>(1_u, 1_u, 1_u));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1332,7 +1331,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec4<u32>(1_u, 1_u, 1_u, 1_u));
+        params.Push(Call<vec4<u32>>(1_u, 1_u, 1_u, 1_u));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1362,7 +1361,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec2<i32>(1_i, 1_i));
+        params.Push(Call<vec2<i32>>(1_i, 1_i));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1377,7 +1376,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec3<i32>(1_i, 1_i, 1_i));
+        params.Push(Call<vec3<i32>>(1_i, 1_i, 1_i));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1392,7 +1391,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec4<i32>(1_i, 1_i, 1_i, 1_i));
+        params.Push(Call<vec4<i32>>(1_i, 1_i, 1_i, 1_i));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1419,7 +1418,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec2<bool>(true, true));
+        params.Push(Call<vec2<bool>>(true, true));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1433,7 +1432,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec3<bool>(true, true, true));
+        params.Push(Call<vec3<bool>>(true, true, true));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1447,7 +1446,7 @@
 
     utils::Vector<const ast::Expression*, 8> params;
     for (uint32_t i = 0; i < num_params; ++i) {
-        params.Push(vec4<bool>(true, true, true, true));
+        params.Push(Call<vec4<bool>>(true, true, true, true));
     }
     auto* builtin = Call(name, params);
     WrapInFunction(builtin);
@@ -1463,7 +1462,7 @@
 
 TEST_P(DataPacking4x8, Float_Vec4) {
     auto name = GetParam();
-    auto* builtin = Call(name, vec4<f32>(1_f, 1_f, 1_f, 1_f));
+    auto* builtin = Call(name, Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f));
     WrapInFunction(builtin);
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -1476,7 +1475,7 @@
 
 TEST_P(DataPacking2x16, Float_Vec2) {
     auto name = GetParam();
-    auto* builtin = Call(name, vec2<f32>(1_f, 1_f));
+    auto* builtin = Call(name, Call<vec2<f32>>(1_f, 1_f));
     WrapInFunction(builtin);
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
diff --git a/src/tint/resolver/call_test.cc b/src/tint/resolver/call_test.cc
index 25f4814..6ebd629 100644
--- a/src/tint/resolver/call_test.cc
+++ b/src/tint/resolver/call_test.cc
@@ -18,39 +18,21 @@
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
-// Helpers and typedefs
-template <typename T>
-using DataType = builder::DataType<T>;
-template <int N, typename T>
-using vec = builder::vec<N, T>;
-template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <int N, int M, typename T>
-using mat = builder::mat<N, M, T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat2x3 = builder::mat2x3<T>;
-template <typename T>
-using mat3x2 = builder::mat3x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
+
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 template <typename T, int ID = 0>
 using alias = builder::alias<T, ID>;
+
 template <typename T>
 using alias1 = builder::alias1<T>;
+
 template <typename T>
 using alias2 = builder::alias2<T>;
+
 template <typename T>
 using alias3 = builder::alias3<T>;
 
@@ -63,7 +45,7 @@
 
 template <typename T>
 constexpr Params ParamsFor() {
-    return Params{DataType<T>::ExprFromDouble, DataType<T>::AST};
+    return Params{builder::DataType<T>::ExprFromDouble, builder::DataType<T>::AST};
 }
 
 static constexpr Params all_param_types[] = {
diff --git a/src/tint/resolver/call_validation_test.cc b/src/tint/resolver/call_validation_test.cc
index bf533cc..100e9c4 100644
--- a/src/tint/resolver/call_validation_test.cc
+++ b/src/tint/resolver/call_validation_test.cc
@@ -18,11 +18,12 @@
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverCallValidationTest = ResolverTest;
 
 TEST_F(ResolverCallValidationTest, TooFewArgs) {
@@ -103,7 +104,7 @@
     //   var z: i32 = 1i;
     //   foo(&z);
     // }
-    auto* param = Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction));
+    auto* param = Param("p", ty.ptr<function, i32>());
     Func("foo", utils::Vector{param}, ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -120,7 +121,7 @@
     //   let z: i32 = 1i;
     //   foo(&z);
     // }
-    auto* param = Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction));
+    auto* param = Param("p", ty.ptr<function, i32>());
     Func("foo", utils::Vector{param}, ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -142,7 +143,7 @@
     auto* S = Structure("S", utils::Vector{
                                  Member("m", ty.i32()),
                              });
-    auto* param = Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction));
+    auto* param = Param("p", ty.ptr<function, i32>());
     Func("foo", utils::Vector{param}, ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -169,7 +170,7 @@
     auto* S = Structure("S", utils::Vector{
                                  Member("m", ty.i32()),
                              });
-    auto* param = Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction));
+    auto* param = Param("p", ty.ptr<function, i32>());
     Func("foo", utils::Vector{param}, ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -189,7 +190,7 @@
     auto* S = Structure("S", utils::Vector{
                                  Member("m", ty.i32()),
                              });
-    auto* param = Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction));
+    auto* param = Param("p", ty.ptr<function, i32>());
     Func("foo", utils::Vector{param}, ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -208,12 +209,12 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("bar",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(),
          utils::Vector{
@@ -235,12 +236,12 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("bar",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(),
          utils::Vector{
@@ -268,13 +269,13 @@
     // }
     Func("x",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Var("v", ty.i32())),
-             Decl(Let("p", ty.ptr(builtin::AddressSpace::kFunction, ty.i32()), AddressOf("v"))),
+             Decl(Let("p", ty.ptr<function, i32>(), AddressOf("v"))),
              CallStmt(Call("x", "p")),
          },
          utils::Vector{
@@ -293,13 +294,13 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kPrivate)),
+             Param("p", ty.ptr<private_, i32>()),
          },
          ty.void_(), utils::Empty);
     GlobalVar("v", ty.i32(), builtin::AddressSpace::kPrivate);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Let("p", ty.ptr(builtin::AddressSpace::kPrivate, ty.i32()), AddressOf("v"))),
+             Decl(Let("p", ty.ptr<private_, i32>(), AddressOf("v"))),
              CallStmt(Call("foo", Expr(Source{{12, 34}}, "p"))),
          },
          utils::Vector{
@@ -318,14 +319,13 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Var("v", ty.array<i32, 4>())),
-             Decl(Let("p", ty.ptr(builtin::AddressSpace::kFunction, ty.i32()),
-                      AddressOf(IndexAccessor("v", 0_a)))),
+             Decl(Let("p", ty.ptr<function, i32>(), AddressOf(IndexAccessor("v", 0_a)))),
              CallStmt(Call("foo", Expr(Source{{12, 34}}, "p"))),
          },
          utils::Vector{
@@ -349,14 +349,13 @@
     Enable(builtin::Extension::kChromiumExperimentalFullPtrParameters);
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Var("v", ty.array<i32, 4>())),
-             Decl(Let("p", ty.ptr(builtin::AddressSpace::kFunction, ty.i32()),
-                      AddressOf(IndexAccessor("v", 0_a)))),
+             Decl(Let("p", ty.ptr<function, i32>(), AddressOf(IndexAccessor("v", 0_a)))),
              CallStmt(Call("foo", Expr(Source{{12, 34}}, "p"))),
          },
          utils::Vector{
@@ -377,7 +376,7 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr(builtin::AddressSpace::kFunction, ty.array<i32, 4>())),
+             Param("p", ty.ptr<function, array<i32, 4>>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
@@ -406,7 +405,7 @@
     // }
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
@@ -440,7 +439,7 @@
     Enable(builtin::Extension::kChromiumExperimentalFullPtrParameters);
     Func("foo",
          utils::Vector{
-             Param("p", ty.ptr<i32>(builtin::AddressSpace::kFunction)),
+             Param("p", ty.ptr<function, i32>()),
          },
          ty.void_(), utils::Empty);
     Func("main", utils::Empty, ty.void_(),
diff --git a/src/tint/resolver/compound_assignment_validation_test.cc b/src/tint/resolver/compound_assignment_validation_test.cc
index 0343f08..0acf993 100644
--- a/src/tint/resolver/compound_assignment_validation_test.cc
+++ b/src/tint/resolver/compound_assignment_validation_test.cc
@@ -18,13 +18,13 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/type/storage_texture.h"
 
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverCompoundAssignmentValidationTest = ResolverTest;
 
 TEST_F(ResolverCompoundAssignmentValidationTest, CompatibleTypes) {
@@ -51,9 +51,8 @@
     // var a : i32;
     // let b : ptr<function,i32> = &a;
     // *b += 2;
-    const auto func = builtin::AddressSpace::kFunction;
-    auto* var_a = Var("a", ty.i32(), func, Expr(2_i));
-    auto* var_b = Let("b", ty.ptr<i32>(func), AddressOf(Expr("a")));
+    auto* var_a = Var("a", ty.i32(), builtin::AddressSpace::kFunction, Expr(2_i));
+    auto* var_b = Let("b", ty.ptr<function, i32>(), AddressOf(Expr("a")));
     WrapInFunction(var_a, var_b,
                    CompoundAssign(Source{{12, 34}}, Deref("b"), 2_i, ast::BinaryOp::kAdd));
 
@@ -116,7 +115,7 @@
 
     auto* var = Var("a", ty.f32());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", vec4<f32>(), ast::BinaryOp::kAdd);
+    auto* assign = CompoundAssign(Source{{12, 34}}, "a", Call<vec4<f32>>(), ast::BinaryOp::kAdd);
     WrapInFunction(var, assign);
 
     ASSERT_FALSE(r()->Resolve());
@@ -146,7 +145,8 @@
 
     auto* var = Var("a", ty.f32());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x4<f32>(), ast::BinaryOp::kMultiply);
+    auto* assign =
+        CompoundAssign(Source{{12, 34}}, "a", Call<mat4x4<f32>>(), ast::BinaryOp::kMultiply);
     WrapInFunction(var, assign);
 
     ASSERT_FALSE(r()->Resolve());
@@ -162,7 +162,8 @@
 
     auto* var = Var("a", ty.vec4<f32>());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x4<f32>(), ast::BinaryOp::kMultiply);
+    auto* assign =
+        CompoundAssign(Source{{12, 34}}, "a", Call<mat4x4<f32>>(), ast::BinaryOp::kMultiply);
     WrapInFunction(var, assign);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -176,7 +177,8 @@
 
     auto* var = Var("a", ty.vec4<f32>());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat4x2<f32>(), ast::BinaryOp::kMultiply);
+    auto* assign =
+        CompoundAssign(Source{{12, 34}}, "a", Call<mat4x2<f32>>(), ast::BinaryOp::kMultiply);
     WrapInFunction(var, assign);
 
     ASSERT_FALSE(r()->Resolve());
@@ -194,7 +196,8 @@
 
     auto* var = Var("a", ty.vec4<f32>());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", mat2x4<f32>(), ast::BinaryOp::kMultiply);
+    auto* assign =
+        CompoundAssign(Source{{12, 34}}, "a", Call<mat2x4<f32>>(), ast::BinaryOp::kMultiply);
     WrapInFunction(var, assign);
 
     ASSERT_FALSE(r()->Resolve());
@@ -210,7 +213,8 @@
 
     auto* var = Var("a", ty.mat4x4<f32>());
 
-    auto* assign = CompoundAssign(Source{{12, 34}}, "a", vec4<f32>(), ast::BinaryOp::kMultiply);
+    auto* assign =
+        CompoundAssign(Source{{12, 34}}, "a", Call<vec4<f32>>(), ast::BinaryOp::kMultiply);
     WrapInFunction(var, assign);
 
     ASSERT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index 3fa71cd..73e274b 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -22,11 +22,11 @@
 #include <type_traits>
 #include <utility>
 
+#include "src/tint/builtin/number.h"
 #include "src/tint/constant/composite.h"
 #include "src/tint/constant/scalar.h"
 #include "src/tint/constant/splat.h"
 #include "src/tint/constant/value.h"
-#include "src/tint/number.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/sem/member_accessor_expression.h"
 #include "src/tint/sem/value_constructor.h"
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index 9303656..0253d30 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -18,7 +18,7 @@
 #include <stddef.h>
 #include <string>
 
-#include "src/tint/number.h"
+#include "src/tint/builtin/number.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/result.h"
 #include "src/tint/utils/vector.h"
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
index e61599f..dac78ce 100644
--- a/src/tint/resolver/const_eval_binary_op_test.cc
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -17,7 +17,8 @@
 #include "src/tint/reader/wgsl/parser.h"
 #include "src/tint/utils/result.h"
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 using ::testing::HasSubstr;
 
 namespace tint::resolver {
@@ -1690,7 +1691,7 @@
     // const i = 4;
     // const result = (one == 0) && (a[i] == 0);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(4_a));
     auto* lhs = Equal("one", 0_a);
     auto* rhs = Equal(IndexAccessor("a", "i"), 0_a);
@@ -1707,7 +1708,7 @@
     // const i = 3;
     // const result = (one == 1) && (a[i] == 0);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(3_a));
     auto* lhs = Equal("one", 1_a);
     auto* rhs = Equal(IndexAccessor("a", Expr(Source{{12, 34}}, "i")), 0_a);
@@ -1724,7 +1725,7 @@
     // const i = 3;
     // const result = (one == 0) && (a[i] == 0.0f);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(3_a));
     auto* lhs = Equal("one", 0_a);
     auto* rhs = Equal(Source{{12, 34}}, IndexAccessor("a", "i"), 0.0_f);
@@ -1747,7 +1748,7 @@
     // const i = 4;
     // const result = (one == 1) || (a[i] == 0);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(4_a));
     auto* lhs = Equal("one", 1_a);
     auto* rhs = Equal(IndexAccessor("a", "i"), 0_a);
@@ -1764,7 +1765,7 @@
     // const i = 3;
     // const result = (one == 0) || (a[i] == 0);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(3_a));
     auto* lhs = Equal("one", 0_a);
     auto* rhs = Equal(IndexAccessor("a", Expr(Source{{12, 34}}, "i")), 0_a);
@@ -1781,7 +1782,7 @@
     // const i = 3;
     // const result = (one == 1) || (a[i] == 0.0f);
     GlobalConst("one", Expr(1_a));
-    GlobalConst("a", array<i32, 3>(1_i, 2_i, 3_i));
+    GlobalConst("a", Call<array<i32, 3>>(1_i, 2_i, 3_i));
     GlobalConst("i", Expr(3_a));
     auto* lhs = Equal("one", 1_a);
     auto* rhs = Equal(Source{{12, 34}}, IndexAccessor("a", "i"), 0.0_f);
@@ -1914,7 +1915,8 @@
     // const result = (one == 0) && (vec2<f32>(1.0, true).x == 0.0);
     GlobalConst("one", Expr(1_a));
     auto* lhs = Equal("one", 0_a);
-    auto* rhs = Equal(MemberAccessor(vec2<f32>(Source{{12, 34}}, 1.0_a, Expr(true)), "x"), 0.0_a);
+    auto* rhs =
+        Equal(MemberAccessor(Call<vec2<f32>>(Source{{12, 34}}, 1.0_a, Expr(true)), "x"), 0.0_a);
     GlobalConst("result", LogicalAnd(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
@@ -1942,7 +1944,8 @@
     // const result = (one == 1) || (vec2<f32>(1.0, true).x == 0.0);
     GlobalConst("one", Expr(1_a));
     auto* lhs = Equal("one", 0_a);
-    auto* rhs = Equal(MemberAccessor(vec2<f32>(Source{{12, 34}}, 1.0_a, Expr(true)), "x"), 0.0_a);
+    auto* rhs =
+        Equal(MemberAccessor(Call<vec2<f32>>(Source{{12, 34}}, 1.0_a, Expr(true)), "x"), 0.0_a);
     GlobalConst("result", LogicalOr(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
@@ -2328,7 +2331,8 @@
     // const result = (one == 0) && (vec2(1, 2).z == 0);
     GlobalConst("one", Expr(1_a));
     auto* lhs = Equal("one", 0_a);
-    auto* rhs = Equal(MemberAccessor(vec2<Infer>(1_a, 2_a), Ident(Source{{12, 34}}, "z")), 0_a);
+    auto* rhs =
+        Equal(MemberAccessor(Call<vec2<Infer>>(1_a, 2_a), Ident(Source{{12, 34}}, "z")), 0_a);
     GlobalConst("result", LogicalAnd(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
@@ -2340,7 +2344,8 @@
     // const result = (one == 1) || (vec2(1, 2).z == 0);
     GlobalConst("one", Expr(1_a));
     auto* lhs = Equal("one", 1_a);
-    auto* rhs = Equal(MemberAccessor(vec2<Infer>(1_a, 2_a), Ident(Source{{12, 34}}, "z")), 0_a);
+    auto* rhs =
+        Equal(MemberAccessor(Call<vec2<Infer>>(1_a, 2_a), Ident(Source{{12, 34}}, "z")), 0_a);
     GlobalConst("result", LogicalOr(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/const_eval_bitcast_test.cc b/src/tint/resolver/const_eval_bitcast_test.cc
index cbb09d5..c52e2e3 100644
--- a/src/tint/resolver/const_eval_bitcast_test.cc
+++ b/src/tint/resolver/const_eval_bitcast_test.cc
@@ -14,11 +14,12 @@
 
 #include "src/tint/resolver/const_eval_test.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 struct Case {
     Value input;
     struct Success {
@@ -169,24 +170,24 @@
                                      Vec(0_i, 0x3F800000_i, 0x42F60000_i)),
 
                              // Unrepresentable
-                             Failure<f32>(Val(nan_as_u32)),                          //
-                             Failure<f32>(Val(nan_as_i32)),                          //
-                             Failure<f32>(Val(inf_as_u32)),                          //
-                             Failure<f32>(Val(inf_as_i32)),                          //
-                             Failure<f32>(Val(neg_inf_as_u32)),                      //
-                             Failure<f32>(Val(neg_inf_as_i32)),                      //
-                             Failure<builder::vec2<f32>>(Vec(nan_as_u32, 0_u)),      //
-                             Failure<builder::vec2<f32>>(Vec(nan_as_i32, 0_i)),      //
-                             Failure<builder::vec2<f32>>(Vec(inf_as_u32, 0_u)),      //
-                             Failure<builder::vec2<f32>>(Vec(inf_as_i32, 0_i)),      //
-                             Failure<builder::vec2<f32>>(Vec(neg_inf_as_u32, 0_u)),  //
-                             Failure<builder::vec2<f32>>(Vec(neg_inf_as_i32, 0_i)),  //
-                             Failure<builder::vec2<f32>>(Vec(0_u, nan_as_u32)),      //
-                             Failure<builder::vec2<f32>>(Vec(0_i, nan_as_i32)),      //
-                             Failure<builder::vec2<f32>>(Vec(0_u, inf_as_u32)),      //
-                             Failure<builder::vec2<f32>>(Vec(0_i, inf_as_i32)),      //
-                             Failure<builder::vec2<f32>>(Vec(0_u, neg_inf_as_u32)),  //
-                             Failure<builder::vec2<f32>>(Vec(0_i, neg_inf_as_i32)),  //
+                             Failure<f32>(Val(nan_as_u32)),                 //
+                             Failure<f32>(Val(nan_as_i32)),                 //
+                             Failure<f32>(Val(inf_as_u32)),                 //
+                             Failure<f32>(Val(inf_as_i32)),                 //
+                             Failure<f32>(Val(neg_inf_as_u32)),             //
+                             Failure<f32>(Val(neg_inf_as_i32)),             //
+                             Failure<vec2<f32>>(Vec(nan_as_u32, 0_u)),      //
+                             Failure<vec2<f32>>(Vec(nan_as_i32, 0_i)),      //
+                             Failure<vec2<f32>>(Vec(inf_as_u32, 0_u)),      //
+                             Failure<vec2<f32>>(Vec(inf_as_i32, 0_i)),      //
+                             Failure<vec2<f32>>(Vec(neg_inf_as_u32, 0_u)),  //
+                             Failure<vec2<f32>>(Vec(neg_inf_as_i32, 0_i)),  //
+                             Failure<vec2<f32>>(Vec(0_u, nan_as_u32)),      //
+                             Failure<vec2<f32>>(Vec(0_i, nan_as_i32)),      //
+                             Failure<vec2<f32>>(Vec(0_u, inf_as_u32)),      //
+                             Failure<vec2<f32>>(Vec(0_i, inf_as_i32)),      //
+                             Failure<vec2<f32>>(Vec(0_u, neg_inf_as_u32)),  //
+                             Failure<vec2<f32>>(Vec(0_i, neg_inf_as_i32)),  //
                          }));
 
 }  // namespace
diff --git a/src/tint/resolver/const_eval_construction_test.cc b/src/tint/resolver/const_eval_construction_test.cc
index 3d59514..4ecca7f 100644
--- a/src/tint/resolver/const_eval_construction_test.cc
+++ b/src/tint/resolver/const_eval_construction_test.cc
@@ -14,11 +14,12 @@
 
 #include "src/tint/resolver/const_eval_test.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 TEST_F(ResolverConstEvalTest, Scalar_AFloat) {
     auto* expr = Expr(99.0_a);
     auto* a = Const("a", expr);
@@ -189,26 +190,26 @@
                              C<f32>(),
                              C<f16>(),
                              C<bool>(),
-                             C<builder::vec2<AInt>>(),
-                             C<builder::vec3<AInt>>(),
-                             C<builder::vec4<AInt>>(),
-                             C<builder::vec3<u32>>(),
-                             C<builder::vec3<i32>>(),
-                             C<builder::vec3<f32>>(),
-                             C<builder::vec3<f16>>(),
-                             C<builder::mat2x2<f32>>(),
-                             C<builder::mat2x2<f16>>(),
-                             C<builder::array<3, u32>>(),
-                             C<builder::array<3, i32>>(),
-                             C<builder::array<3, f32>>(),
-                             C<builder::array<3, f16>>(),
-                             C<builder::array<3, bool>>(),
+                             C<vec2<AInt>>(),
+                             C<vec3<AInt>>(),
+                             C<vec4<AInt>>(),
+                             C<vec3<u32>>(),
+                             C<vec3<i32>>(),
+                             C<vec3<f32>>(),
+                             C<vec3<f16>>(),
+                             C<mat2x2<f32>>(),
+                             C<mat2x2<f16>>(),
+                             C<array<u32, 3>>(),
+                             C<array<i32, 3>>(),
+                             C<array<f32, 3>>(),
+                             C<array<f16, 3>>(),
+                             C<array<bool, 3>>(),
                          }));
 
 }  // namespace ZeroInit
 
 TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_i32) {
-    auto* expr = vec3<i32>();
+    auto* expr = Call<vec3<i32>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -237,7 +238,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_u32) {
-    auto* expr = vec3<u32>();
+    auto* expr = Call<vec3<u32>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -266,7 +267,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f32) {
-    auto* expr = vec3<f32>();
+    auto* expr = Call<vec3<f32>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -297,7 +298,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>();
+    auto* expr = Call<vec3<f16>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -326,7 +327,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_bool) {
-    auto* expr = vec3<bool>();
+    auto* expr = Call<vec3<bool>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -355,7 +356,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Splat_i32) {
-    auto* expr = vec3<i32>(99_i);
+    auto* expr = Call<vec3<i32>>(99_i);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -384,7 +385,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Splat_u32) {
-    auto* expr = vec3<u32>(99_u);
+    auto* expr = Call<vec3<u32>>(99_u);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -413,7 +414,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Splat_f32) {
-    auto* expr = vec3<f32>(9.9_f);
+    auto* expr = Call<vec3<f32>>(9.9_f);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -444,7 +445,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_Splat_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(9.9_h);
+    auto* expr = Call<vec3<f16>>(9.9_h);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -474,7 +475,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Splat_bool) {
-    auto* expr = vec3<bool>(true);
+    auto* expr = Call<vec3<bool>>(true);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -503,7 +504,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_AInt) {
-    auto* expr = vec3<Infer>(1_a, 2_a, 3_a);
+    auto* expr = Call<vec3<Infer>>(1_a, 2_a, 3_a);
     auto* a = Const("a", expr);
     WrapInFunction(a);
 
@@ -533,7 +534,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_AFloat) {
-    auto* expr = vec3<Infer>(1.0_a, 2.0_a, 3.0_a);
+    auto* expr = Call<vec3<Infer>>(1.0_a, 2.0_a, 3.0_a);
     auto* a = Const("a", expr);
     WrapInFunction(a);
 
@@ -563,7 +564,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_i32) {
-    auto* expr = vec3<i32>(1_i, 2_i, 3_i);
+    auto* expr = Call<vec3<i32>>(1_i, 2_i, 3_i);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -592,7 +593,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_u32) {
-    auto* expr = vec3<u32>(1_u, 2_u, 3_u);
+    auto* expr = Call<vec3<u32>>(1_u, 2_u, 3_u);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -621,7 +622,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_f32) {
-    auto* expr = vec3<f32>(1_f, 2_f, 3_f);
+    auto* expr = Call<vec3<f32>>(1_f, 2_f, 3_f);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -652,7 +653,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(1_h, 2_h, 3_h);
+    auto* expr = Call<vec3<f16>>(1_h, 2_h, 3_h);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -681,7 +682,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_bool) {
-    auto* expr = vec3<bool>(true, false, true);
+    auto* expr = Call<vec3<bool>>(true, false, true);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -710,7 +711,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_i32) {
-    auto* expr = vec3<i32>(1_i, vec2<i32>(2_i, 3_i));
+    auto* expr = Call<vec3<i32>>(1_i, Call<vec2<i32>>(2_i, 3_i));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -739,7 +740,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_u32) {
-    auto* expr = vec3<u32>(vec2<u32>(1_u, 2_u), 3_u);
+    auto* expr = Call<vec3<u32>>(Call<vec2<u32>>(1_u, 2_u), 3_u);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -768,7 +769,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32) {
-    auto* expr = vec3<f32>(1_f, vec2<f32>(2_f, 3_f));
+    auto* expr = Call<vec3<f32>>(1_f, Call<vec2<f32>>(2_f, 3_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -797,7 +798,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_10) {
-    auto* expr = vec3<f32>(10_f, vec2<f32>(10_f, 10_f));
+    auto* expr = Call<vec3<f32>>(10_f, Call<vec2<f32>>(10_f, 10_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -826,7 +827,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_positive_0) {
-    auto* expr = vec3<f32>(0_f, vec2<f32>(0_f, 0_f));
+    auto* expr = Call<vec3<f32>>(0_f, Call<vec2<f32>>(0_f, 0_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -855,7 +856,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_negative_0) {
-    auto* expr = vec3<f32>(vec2<f32>(-0_f, -0_f), -0_f);
+    auto* expr = Call<vec3<f32>>(Call<vec2<f32>>(-0_f, -0_f), -0_f);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -884,7 +885,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_mixed_sign_0) {
-    auto* expr = vec3<f32>(0_f, vec2<f32>(-0_f, 0_f));
+    auto* expr = Call<vec3<f32>>(0_f, Call<vec2<f32>>(-0_f, 0_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -915,7 +916,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(1_h, vec2<f16>(2_h, 3_h));
+    auto* expr = Call<vec3<f16>>(1_h, Call<vec2<f16>>(2_h, 3_h));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -946,7 +947,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_10) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(10_h, vec2<f16>(10_h, 10_h));
+    auto* expr = Call<vec3<f16>>(10_h, Call<vec2<f16>>(10_h, 10_h));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -977,7 +978,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_positive_0) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(0_h, vec2<f16>(0_h, 0_h));
+    auto* expr = Call<vec3<f16>>(0_h, Call<vec2<f16>>(0_h, 0_h));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1008,7 +1009,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_negative_0) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(vec2<f16>(-0_h, -0_h), -0_h);
+    auto* expr = Call<vec3<f16>>(Call<vec2<f16>>(-0_h, -0_h), -0_h);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1039,7 +1040,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_mixed_sign_0) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(0_h, vec2<f16>(-0_h, 0_h));
+    auto* expr = Call<vec3<f16>>(0_h, Call<vec2<f16>>(-0_h, 0_h));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1068,7 +1069,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_bool) {
-    auto* expr = vec3<bool>(vec2<bool>(true, false), true);
+    auto* expr = Call<vec3<bool>>(Call<vec2<bool>>(true, false), true);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1097,7 +1098,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_true) {
-    auto* expr = vec3<bool>(true, vec2<bool>(true, true));
+    auto* expr = Call<vec3<bool>>(true, Call<vec2<bool>>(true, true));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1126,7 +1127,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_false) {
-    auto* expr = vec3<bool>(false, vec2<bool>(false, false));
+    auto* expr = Call<vec3<bool>>(false, Call<vec2<bool>>(false, false));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1155,7 +1156,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f32) {
-    auto* expr = mat2x3<f32>();
+    auto* expr = Call<mat2x3<f32>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1199,7 +1200,7 @@
 TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = mat2x3<f16>();
+    auto* expr = Call<mat2x3<f16>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1241,7 +1242,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Scalars_af) {
-    auto* expr = Call(ty.mat3x2<Infer>(), 1.0_a, 2.0_a, 3.0_a, 4.0_a, 5.0_a, 6.0_a);
+    auto* expr = Call<mat3x2<Infer>>(1.0_a, 2.0_a, 3.0_a, 4.0_a, 5.0_a, 6.0_a);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1283,10 +1284,10 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Columns_af) {
-    auto* expr = Call(ty.mat<Infer>(3, 2),        //
-                      vec2<Infer>(1.0_a, 2.0_a),  //
-                      vec2<Infer>(3.0_a, 4.0_a),  //
-                      vec2<Infer>(5.0_a, 6.0_a));
+    auto* expr = Call<mat<3, 2, Infer>>(  //
+        Call<vec2<Infer>>(1.0_a, 2.0_a),  //
+        Call<vec2<Infer>>(3.0_a, 4.0_a),  //
+        Call<vec2<Infer>>(5.0_a, 6.0_a));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1328,7 +1329,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_i32_Zero) {
-    auto* expr = Call(ty.array<i32, 4>());
+    auto* expr = Call<array<i32, 4>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1360,7 +1361,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_f32_Zero) {
-    auto* expr = Call(ty.array<f32, 4>());
+    auto* expr = Call<array<f32, 4>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1392,7 +1393,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_vec3_f32_Zero) {
-    auto* expr = Call(ty.array(ty.vec3<f32>(), 2_u));
+    auto* expr = Call<array<vec3<f32>, 2>>();
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1468,7 +1469,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_i32_Elements) {
-    auto* expr = Call(ty.array<i32, 4>(), 10_i, 20_i, 30_i, 40_i);
+    auto* expr = Call<array<i32, 4>>(10_i, 20_i, 30_i, 40_i);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1592,7 +1593,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_f32_Elements) {
-    auto* expr = Call(ty.array<f32, 4>(), 10_f, 20_f, 30_f, 40_f);
+    auto* expr = Call<array<f32, 4>>(10_f, 20_f, 30_f, 40_f);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -1624,8 +1625,8 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_vec3_f32_Elements) {
-    auto* expr = Call(ty.array(ty.vec3<f32>(), 2_u),  //
-                      vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
+    auto* expr =
+        Call<array<vec3<f32>, 2>>(Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2083,8 +2084,8 @@
                        Member("m4", ty.vec3<f16>()),
                        Member("m5", ty.vec2<bool>()),
                    });
-    auto* expr = Call("S", vec2<i32>(1_i), vec3<u32>(2_u), vec4<f32>(3_f), vec3<f16>(4_h),
-                      vec2<bool>(false));
+    auto* expr = Call("S", Call<vec2<i32>>(1_i), Call<vec3<u32>>(2_u), Call<vec4<f32>>(3_f),
+                      Call<vec3<f16>>(4_h), Call<vec2<bool>>(false));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2192,7 +2193,7 @@
                        Member("m2", ty.array<f32, 3>()),
                    });
     auto* expr = Call("S",  //
-                      Call(ty.array<i32, 2>(), 1_i, 2_i), Call(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
+                      Call<array<i32, 2>>(1_i, 2_i), Call<array<f32, 3>>(1_f, 2_f, 3_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/tint/resolver/const_eval_conversion_test.cc b/src/tint/resolver/const_eval_conversion_test.cc
index 468d2b4..57d6c43 100644
--- a/src/tint/resolver/const_eval_conversion_test.cc
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -16,11 +16,12 @@
 #include "src/tint/resolver/const_eval_test.h"
 #include "src/tint/sem/materialize.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 enum class Kind {
     kScalar,
     kVector,
@@ -75,7 +76,7 @@
     auto* input_val = input.Expr(*this);
     auto* expr = Call(type.ast(*this), input_val);
     if (kind == Kind::kVector) {
-        expr = Call(ty.vec<Infer>(3), expr);
+        expr = Call<vec3<Infer>>(expr);
     }
     WrapInFunction(expr);
 
@@ -225,7 +226,7 @@
                                           })));
 
 TEST_F(ResolverConstEvalTest, Vec3_Convert_f32_to_i32) {
-    auto* expr = vec3<i32>(vec3<f32>(1.1_f, 2.2_f, 3.3_f));
+    auto* expr = Call<vec3<i32>>(Call<vec3<f32>>(1.1_f, 2.2_f, 3.3_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -254,7 +255,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f32) {
-    auto* expr = vec3<f32>(vec3<u32>(10_u, 20_u, 30_u));
+    auto* expr = Call<vec3<f32>>(Call<vec3<u32>>(10_u, 20_u, 30_u));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -285,7 +286,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_Convert_f16_to_i32) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<i32>(vec3<f16>(1.1_h, 2.2_h, 3.3_h));
+    auto* expr = Call<vec3<i32>>(Call<vec3<f16>>(1.1_h, 2.2_h, 3.3_h));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -316,7 +317,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(vec3<u32>(10_u, 20_u, 30_u));
+    auto* expr = Call<vec3<f16>>(Call<vec3<u32>>(10_u, 20_u, 30_u));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -345,7 +346,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_i32) {
-    auto* expr = vec3<i32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
+    auto* expr = Call<vec3<i32>>(Call<vec3<f32>>(1e10_f, -1e20_f, 1e30_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -374,7 +375,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_u32) {
-    auto* expr = vec3<u32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
+    auto* expr = Call<vec3<u32>>(Call<vec3<f32>>(1e10_f, -1e20_f, 1e30_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -405,7 +406,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(Source{{12, 34}}, vec3<f32>(1e10_f, 0_f, 0_f));
+    auto* expr = Call<vec3<f16>>(Source{{12, 34}}, Call<vec3<f32>>(1e10_f, 0_f, 0_f));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
@@ -415,7 +416,7 @@
 TEST_F(ResolverConstEvalTest, Vec3_Convert_Small_f32_to_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = vec3<f16>(vec3<f32>(1e-20_f, -2e-30_f, 3e-40_f));
+    auto* expr = Call<vec3<f16>>(Call<vec3<f32>>(1e-20_f, -2e-30_f, 3e-40_f));
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/tint/resolver/const_eval_indexing_test.cc b/src/tint/resolver/const_eval_indexing_test.cc
index d311d30..a67843b 100644
--- a/src/tint/resolver/const_eval_indexing_test.cc
+++ b/src/tint/resolver/const_eval_indexing_test.cc
@@ -14,13 +14,14 @@
 
 #include "src/tint/resolver/const_eval_test.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 TEST_F(ResolverConstEvalTest, Vec3_Index) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), 2_i);
+    auto* expr = IndexAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), 2_i);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -35,7 +36,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_High) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, 3_i));
+    auto* expr = IndexAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, 3_i));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve()) << r()->error();
@@ -43,7 +44,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_Low) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, -3_i));
+    auto* expr = IndexAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, -3_i));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve()) << r()->error();
@@ -123,7 +124,7 @@
 }  // namespace Swizzle
 
 TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Scalar) {
-    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "y");
+    auto* expr = MemberAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), "y");
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -138,7 +139,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Vector) {
-    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "zx");
+    auto* expr = MemberAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), "zx");
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -161,7 +162,8 @@
 
 TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Chain) {
     auto* expr =  // (1, 2, 3) -> (2, 3, 1) -> (3, 2) -> 2
-        MemberAccessor(MemberAccessor(MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "gbr"), "yx"), "y");
+        MemberAccessor(MemberAccessor(MemberAccessor(Call<vec3<i32>>(1_i, 2_i, 3_i), "gbr"), "yx"),
+                       "y");
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -176,8 +178,10 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat3x2_Index) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)), 2_i);
+    auto* expr =
+        IndexAccessor(Call<mat3x2<f32>>(Call<vec2<f32>>(1._a, 2._a), Call<vec2<f32>>(3._a, 4._a),
+                                        Call<vec2<f32>>(5._a, 6._a)),
+                      2_i);
     WrapInFunction(expr);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -199,9 +203,10 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_High) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
-        Expr(Source{{12, 34}}, 3_i));
+    auto* expr =
+        IndexAccessor(Call<mat3x2<f32>>(Call<vec2<f32>>(1._a, 2._a), Call<vec2<f32>>(3._a, 4._a),
+                                        Call<vec2<f32>>(5._a, 6._a)),
+                      Expr(Source{{12, 34}}, 3_i));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve()) << r()->error();
@@ -209,9 +214,10 @@
 }
 
 TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_Low) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
-        Expr(Source{{12, 34}}, -3_i));
+    auto* expr =
+        IndexAccessor(Call<mat3x2<f32>>(Call<vec2<f32>>(1._a, 2._a), Call<vec2<f32>>(3._a, 4._a),
+                                        Call<vec2<f32>>(5._a, 6._a)),
+                      Expr(Source{{12, 34}}, -3_i));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve()) << r()->error();
@@ -219,8 +225,9 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index) {
-    auto* expr = IndexAccessor(Call(ty.array(ty.vec3<f32>(), 2_u),  //
-                                    vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+    auto* expr = IndexAccessor(Call<array<vec3<f32>, 2>>(           //
+                                   Call<vec3<f32>>(1_f, 2_f, 3_f),  //
+                                   Call<vec3<f32>>(4_f, 5_f, 6_f)),
                                1_i);
     WrapInFunction(expr);
 
@@ -248,8 +255,9 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_High) {
-    auto* expr = IndexAccessor(Call(ty.array(ty.vec3<f32>(), 2_u),  //
-                                    vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+    auto* expr = IndexAccessor(Call<array<vec3<f32>, 2>>(           //
+                                   Call<vec3<f32>>(1_f, 2_f, 3_f),  //
+                                   Call<vec3<f32>>(4_f, 5_f, 6_f)),
                                Expr(Source{{12, 34}}, 2_i));
     WrapInFunction(expr);
 
@@ -258,8 +266,9 @@
 }
 
 TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_Low) {
-    auto* expr = IndexAccessor(Call(ty.array(ty.vec3<f32>(), 2_u),  //
-                                    vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+    auto* expr = IndexAccessor(Call<array<vec3<f32>, 2>>(           //
+                                   Call<vec3<f32>>(1_f, 2_f, 3_f),  //
+                                   Call<vec3<f32>>(4_f, 5_f, 6_f)),
                                Expr(Source{{12, 34}}, -2_i));
     WrapInFunction(expr);
 
@@ -268,7 +277,7 @@
 }
 
 TEST_F(ResolverConstEvalTest, RuntimeArray_vec3_f32_Index_OOB_Low) {
-    auto* sb = GlobalVar("sb", ty.array(ty.vec3<f32>()), Group(0_a), Binding(0_a),
+    auto* sb = GlobalVar("sb", ty.array<vec3<f32>>(), Group(0_a), Binding(0_a),
                          builtin::AddressSpace::kStorage);
     auto* expr = IndexAccessor(sb, Expr(Source{{12, 34}}, -2_i));
     WrapInFunction(expr);
@@ -278,11 +287,11 @@
 }
 
 TEST_F(ResolverConstEvalTest, ChainedIndex) {
-    auto* arr_expr = Call(ty.array(ty.mat2x3<f32>(), 2_u),        // array<mat2x3<f32>, 2u>
-                          mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f),   //
-                                      vec3<f32>(4_f, 5_f, 6_f)),  //
-                          mat2x3<f32>(vec3<f32>(7_f, 0_f, 9_f),   //
-                                      vec3<f32>(10_f, 11_f, 12_f)));
+    auto* arr_expr = Call<array<mat2x3<f32>, 2>>(           //
+        Call<mat2x3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f),   //
+                          Call<vec3<f32>>(4_f, 5_f, 6_f)),  //
+        Call<mat2x3<f32>>(Call<vec3<f32>>(7_f, 0_f, 9_f),   //
+                          Call<vec3<f32>>(10_f, 11_f, 12_f)));
 
     auto* mat_expr = IndexAccessor(arr_expr, 1_i);  // arr[1]
     auto* vec_expr = IndexAccessor(mat_expr, 0_i);  // arr[1][0]
diff --git a/src/tint/resolver/const_eval_member_access_test.cc b/src/tint/resolver/const_eval_member_access_test.cc
index ee92714..3eb9644 100644
--- a/src/tint/resolver/const_eval_member_access_test.cc
+++ b/src/tint/resolver/const_eval_member_access_test.cc
@@ -14,11 +14,12 @@
 
 #include "src/tint/resolver/const_eval_test.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 TEST_F(ResolverConstEvalTest, StructMemberAccess) {
     Structure("Inner", utils::Vector{
                            Member("i1", ty.i32()),
@@ -68,9 +69,9 @@
 }
 
 TEST_F(ResolverConstEvalTest, Matrix_AFloat_Construct_From_AInt_Vectors) {
-    auto* c = Const("a", Call(ty.mat2x2<Infer>(),  //
-                              Call(ty.vec<Infer>(2), Expr(1_a), Expr(2_a)),
-                              Call(ty.vec<Infer>(2), Expr(3_a), Expr(4_a))));
+    auto* c = Const("a", Call<mat2x2<Infer>>(              //
+                             Call<vec2<Infer>>(1_a, 2_a),  //
+                             Call<vec2<Infer>>(3_a, 4_a)));
     WrapInFunction(c);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -93,18 +94,18 @@
 }
 
 TEST_F(ResolverConstEvalTest, MatrixMemberAccess_AFloat) {
-    auto* c = Const("a", Call(ty.mat2x3<Infer>(),  //
-                              Call(ty.vec3<Infer>(), Expr(1.0_a), Expr(2.0_a), Expr(3.0_a)),
-                              Call(ty.vec3<Infer>(), Expr(4.0_a), Expr(5.0_a), Expr(6.0_a))));
+    auto* c = Const("a", Call<mat2x3<Infer>>(                         //
+                             Call<vec3<Infer>>(1.0_a, 2.0_a, 3.0_a),  //
+                             Call<vec3<Infer>>(4.0_a, 5.0_a, 6.0_a)));
 
-    auto* col_0 = Const("col_0", IndexAccessor("a", Expr(0_i)));
-    auto* col_1 = Const("col_1", IndexAccessor("a", Expr(1_i)));
-    auto* e00 = Const("e00", IndexAccessor("col_0", Expr(0_i)));
-    auto* e01 = Const("e01", IndexAccessor("col_0", Expr(1_i)));
-    auto* e02 = Const("e02", IndexAccessor("col_0", Expr(2_i)));
-    auto* e10 = Const("e10", IndexAccessor("col_1", Expr(0_i)));
-    auto* e11 = Const("e11", IndexAccessor("col_1", Expr(1_i)));
-    auto* e12 = Const("e12", IndexAccessor("col_1", Expr(2_i)));
+    auto* col_0 = Const("col_0", IndexAccessor("a", 0_i));
+    auto* col_1 = Const("col_1", IndexAccessor("a", 1_i));
+    auto* e00 = Const("e00", IndexAccessor("col_0", 0_i));
+    auto* e01 = Const("e01", IndexAccessor("col_0", 1_i));
+    auto* e02 = Const("e02", IndexAccessor("col_0", 2_i));
+    auto* e10 = Const("e10", IndexAccessor("col_1", 0_i));
+    auto* e11 = Const("e11", IndexAccessor("col_1", 1_i));
+    auto* e12 = Const("e12", IndexAccessor("col_1", 2_i));
 
     (void)col_0;
     (void)col_1;
@@ -169,18 +170,18 @@
 }
 
 TEST_F(ResolverConstEvalTest, MatrixMemberAccess_f32) {
-    auto* c = Const("a", Call(ty.mat2x3<Infer>(),  //
-                              Call(ty.vec3<Infer>(), Expr(1.0_f), Expr(2.0_f), Expr(3.0_f)),
-                              Call(ty.vec3<Infer>(), Expr(4.0_f), Expr(5.0_f), Expr(6.0_f))));
+    auto* c = Const("a", Call<mat2x3<Infer>>(                         //
+                             Call<vec3<Infer>>(1.0_f, 2.0_f, 3.0_f),  //
+                             Call<vec3<Infer>>(4.0_f, 5.0_f, 6.0_f)));
 
-    auto* col_0 = Const("col_0", IndexAccessor("a", Expr(0_i)));
-    auto* col_1 = Const("col_1", IndexAccessor("a", Expr(1_i)));
-    auto* e00 = Const("e00", IndexAccessor("col_0", Expr(0_i)));
-    auto* e01 = Const("e01", IndexAccessor("col_0", Expr(1_i)));
-    auto* e02 = Const("e02", IndexAccessor("col_0", Expr(2_i)));
-    auto* e10 = Const("e10", IndexAccessor("col_1", Expr(0_i)));
-    auto* e11 = Const("e11", IndexAccessor("col_1", Expr(1_i)));
-    auto* e12 = Const("e12", IndexAccessor("col_1", Expr(2_i)));
+    auto* col_0 = Const("col_0", IndexAccessor("a", 0_i));
+    auto* col_1 = Const("col_1", IndexAccessor("a", 1_i));
+    auto* e00 = Const("e00", IndexAccessor("col_0", 0_i));
+    auto* e01 = Const("e01", IndexAccessor("col_0", 1_i));
+    auto* e02 = Const("e02", IndexAccessor("col_0", 2_i));
+    auto* e10 = Const("e10", IndexAccessor("col_1", 0_i));
+    auto* e11 = Const("e11", IndexAccessor("col_1", 1_i));
+    auto* e12 = Const("e12", IndexAccessor("col_1", 2_i));
 
     (void)col_0;
     (void)col_1;
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 504e8ac..84932ed 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -34,6 +34,7 @@
 #include "src/tint/ast/identifier.h"
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/increment_decrement_statement.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/internal_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/invariant_attribute.h"
@@ -390,6 +391,10 @@
                 TraverseExpression(id->expr);
                 return true;
             },
+            [&](const ast::IndexAttribute* index) {
+                TraverseExpression(index->expr);
+                return true;
+            },
             [&](const ast::InterpolateAttribute* interpolate) {
                 TraverseExpression(interpolate->type);
                 TraverseExpression(interpolate->sampling);
diff --git a/src/tint/resolver/dependency_graph_test.cc b/src/tint/resolver/dependency_graph_test.cc
index ee509a7..88bdc13 100644
--- a/src/tint/resolver/dependency_graph_test.cc
+++ b/src/tint/resolver/dependency_graph_test.cc
@@ -23,12 +23,12 @@
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/transform.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
 using ::testing::ElementsAre;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 template <typename T>
 class ResolverDependencyGraphTestWithParam : public ResolverTestWithParam<T> {
@@ -1732,7 +1732,7 @@
     GlobalVar(Sym(), ty.array(T, V));
     GlobalVar(Sym(), ty.vec3(T));
     GlobalVar(Sym(), ty.mat3x2(T));
-    GlobalVar(Sym(), ty.ptr(builtin::AddressSpace::kPrivate, T));
+    GlobalVar(Sym(), ty.ptr<private_>(T));
     GlobalVar(Sym(), ty.sampled_texture(type::TextureDimension::k2d, T));
     GlobalVar(Sym(), ty.depth_texture(type::TextureDimension::k2d));
     GlobalVar(Sym(), ty.depth_multisampled_texture(type::TextureDimension::k2d));
diff --git a/src/tint/resolver/dual_source_blending_extension_test.cc b/src/tint/resolver/dual_source_blending_extension_test.cc
new file mode 100644
index 0000000..ebaf61c
--- /dev/null
+++ b/src/tint/resolver/dual_source_blending_extension_test.cc
@@ -0,0 +1,88 @@
+// 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/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 {
+
+using DualSourceBlendingExtensionTest = ResolverTest;
+
+// Using the @index attribute without chromium_internal_dual_source_blending enabled should fail.
+TEST_F(DualSourceBlendingExtensionTest, UseIndexAttribWithoutExtensionError) {
+    Structure("Output", utils::Vector{
+                            Member("a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "12:34 error: use of '@index' attribute requires enabling extension "
+              "'chromium_internal_dual_source_blending'");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexF32Error) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 0_f)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexFloatValueError) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 1.0_a)}),
+                        });
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexNegativeValue) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, -1_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexValueAboveOne) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 2_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/entry_point_validation_test.cc b/src/tint/resolver/entry_point_validation_test.cc
index eb2d779..4da98db 100644
--- a/src/tint/resolver/entry_point_validation_test.cc
+++ b/src/tint/resolver/entry_point_validation_test.cc
@@ -22,27 +22,16 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 // Helpers and typedefs
 template <typename T>
 using DataType = builder::DataType<T>;
 template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
-template <typename T>
 using alias = builder::alias<T>;
 
 class ResolverEntryPointValidationTest : public TestHelper, public testing::Test {};
@@ -69,7 +58,7 @@
     // fn main() -> @builtin(position) vec4<f32> { return vec4<f32>(); }
     Func(Source{{12, 34}}, "main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
@@ -88,7 +77,7 @@
     // }
     Func(Source{{12, 34}}, "main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
@@ -105,7 +94,7 @@
     // }
     Func(Source{{12, 34}}, "main", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
@@ -793,7 +782,7 @@
 
     Func(Source{{12, 34}}, "frag_main", utils::Empty, ty.array<f32, 2>(),
          utils::Vector{
-             Return(Call(ty.array<f32, 2>())),
+             Return(Call<array<f32, 2>>()),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
diff --git a/src/tint/resolver/evaluation_stage_test.cc b/src/tint/resolver/evaluation_stage_test.cc
index 2e148e5..c9c9e97 100644
--- a/src/tint/resolver/evaluation_stage_test.cc
+++ b/src/tint/resolver/evaluation_stage_test.cc
@@ -17,11 +17,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverEvaluationStageTest = ResolverTest;
 
 TEST_F(ResolverEvaluationStageTest, Literal_i32) {
@@ -41,7 +42,7 @@
 }
 
 TEST_F(ResolverEvaluationStageTest, Vector_Init) {
-    auto* expr = vec3<f32>();
+    auto* expr = Call<vec3<f32>>();
     WrapInFunction(expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -52,7 +53,7 @@
     // const f = 1.f;
     // vec2<f32>(f, f);
     auto* f = Const("f", Expr(1_f));
-    auto* expr = vec2<f32>(f, f);
+    auto* expr = Call<vec2<f32>>(f, f);
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -64,7 +65,7 @@
     // var f = 1.f;
     // vec2<f32>(f, f);
     auto* f = Var("f", Expr(1_f));
-    auto* expr = vec2<f32>(f, f);
+    auto* expr = Call<vec2<f32>>(f, f);
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -76,7 +77,7 @@
     // const f = 1.f;
     // vec2<u32>(vec2<f32>(f));
     auto* f = Const("f", Expr(1_f));
-    auto* expr = vec2<u32>(vec2<f32>(f));
+    auto* expr = Call<vec2<u32>>(Call<vec2<f32>>(f));
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -88,7 +89,7 @@
     // var f = 1.f;
     // vec2<u32>(vec2<f32>(f));
     auto* f = Var("f", Expr(1_f));
-    auto* expr = vec2<u32>(vec2<f32>(f));
+    auto* expr = Call<vec2<u32>>(Call<vec2<f32>>(f));
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -97,7 +98,7 @@
 }
 
 TEST_F(ResolverEvaluationStageTest, Matrix_Init) {
-    auto* expr = mat2x2<f32>();
+    auto* expr = Call<mat2x2<f32>>();
     WrapInFunction(expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -105,7 +106,7 @@
 }
 
 TEST_F(ResolverEvaluationStageTest, Array_Init) {
-    auto* expr = array<f32, 3>();
+    auto* expr = Call<array<f32, 3>>();
     WrapInFunction(expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -116,7 +117,7 @@
     // const f = 1.f;
     // array<f32, 2>(f, f);
     auto* f = Const("f", Expr(1_f));
-    auto* expr = Call(ty.array<f32, 2>(), f, f);
+    auto* expr = Call<array<f32, 2>>(f, f);
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -130,7 +131,7 @@
     // array<f32, 2>(f1, f2);
     auto* f1 = Const("f1", Expr(1_f));
     auto* f2 = Override("f2", Expr(2_f));
-    auto* expr = Call(ty.array<f32, 2>(), f1, f2);
+    auto* expr = Call<array<f32, 2>>(f1, f2);
     WrapInFunction(f1, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -145,7 +146,7 @@
     // array<f32, 2>(f1, f2);
     auto* f1 = Override("f1", Expr(1_f));
     auto* f2 = Var("f2", Expr(2_f));
-    auto* expr = Call(ty.array<f32, 2>(), f1, f2);
+    auto* expr = Call<array<f32, 2>>(f1, f2);
     WrapInFunction(f2, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -160,7 +161,7 @@
     // array<f32, 2>(f1, f2);
     auto* f1 = Const("f1", Expr(1_f));
     auto* f2 = Var("f2", Expr(2_f));
-    auto* expr = Call(ty.array<f32, 2>(), f1, f2);
+    auto* expr = Call<array<f32, 2>>(f1, f2);
     WrapInFunction(f1, f2, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -173,7 +174,7 @@
     // var f = 1.f;
     // array<f32, 2>(f, f);
     auto* f = Var("f", Expr(1_f));
-    auto* expr = Call(ty.array<f32, 2>(), f, f);
+    auto* expr = Call<array<f32, 2>>(f, f);
     WrapInFunction(f, expr);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -185,7 +186,7 @@
     // const vec = vec4<f32>();
     // const idx = 1_i;
     // vec[idx]
-    auto* vec = Const("vec", vec4<f32>());
+    auto* vec = Const("vec", Call<vec4<f32>>());
     auto* idx = Const("idx", Expr(1_i));
     auto* expr = IndexAccessor(vec, idx);
     WrapInFunction(vec, idx, expr);
@@ -200,7 +201,7 @@
     // var vec = vec4<f32>();
     // const idx = 1_i;
     // vec[idx]
-    auto* vec = Var("vec", vec4<f32>());
+    auto* vec = Var("vec", Call<vec4<f32>>());
     auto* idx = Const("idx", Expr(1_i));
     auto* expr = IndexAccessor(vec, idx);
     WrapInFunction(vec, idx, expr);
@@ -215,7 +216,7 @@
     // const vec = vec4<f32>();
     // override idx = 1_i;
     // vec[idx]
-    auto* vec = Const("vec", vec4<f32>());
+    auto* vec = Const("vec", Call<vec4<f32>>());
     auto* idx = Override("idx", Expr(1_i));
     auto* expr = IndexAccessor(vec, idx);
     WrapInFunction(vec, expr);
@@ -230,7 +231,7 @@
     // const vec = vec4<f32>();
     // let idx = 1_i;
     // vec[idx]
-    auto* vec = Const("vec", vec4<f32>());
+    auto* vec = Const("vec", Call<vec4<f32>>());
     auto* idx = Let("idx", Expr(1_i));
     auto* expr = IndexAccessor(vec, idx);
     WrapInFunction(vec, idx, expr);
@@ -244,7 +245,7 @@
 TEST_F(ResolverEvaluationStageTest, Swizzle_Const) {
     // const vec = S();
     // vec.m
-    auto* vec = Const("vec", vec4<f32>());
+    auto* vec = Const("vec", Call<vec4<f32>>());
     auto* expr = MemberAccessor(vec, "xz");
     WrapInFunction(vec, expr);
 
@@ -256,7 +257,7 @@
 TEST_F(ResolverEvaluationStageTest, Swizzle_Runtime) {
     // var vec = S();
     // vec.m
-    auto* vec = Var("vec", vec4<f32>());
+    auto* vec = Var("vec", Call<vec4<f32>>());
     auto* expr = MemberAccessor(vec, "rg");
     WrapInFunction(vec, expr);
 
diff --git a/src/tint/resolver/f16_extension_test.cc b/src/tint/resolver/f16_extension_test.cc
index 0868208..8e4ad02 100644
--- a/src/tint/resolver/f16_extension_test.cc
+++ b/src/tint/resolver/f16_extension_test.cc
@@ -17,11 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverF16ExtensionTest = ResolverTest;
 
 TEST_F(ResolverF16ExtensionTest, TypeUsedWithExtension) {
@@ -65,7 +66,7 @@
     // var<private> v = vec2<f16>();
     Enable(builtin::Extension::kF16);
 
-    GlobalVar("v", vec2<f16>(), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec2<f16>>(), builtin::AddressSpace::kPrivate);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -83,14 +84,15 @@
     // var<private> v = vec2<f16>(vec2<f32>());
     Enable(builtin::Extension::kF16);
 
-    GlobalVar("v", vec2<f16>(vec2<f32>()), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec2<f16>>(Call<vec2<f32>>()), builtin::AddressSpace::kPrivate);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverF16ExtensionTest, Vec2TypeConvUsedWithoutExtension) {
     // var<private> v = vec2<f16>(vec2<f32>());
-    GlobalVar("v", vec2(ty.f16(Source{{12, 34}}), vec2<f32>()), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call(ty.vec2(ty.f16(Source{{12, 34}})), Call<vec2<f32>>()),
+              builtin::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
diff --git a/src/tint/resolver/function_validation_test.cc b/src/tint/resolver/function_validation_test.cc
index b97e7f3..1ea121d 100644
--- a/src/tint/resolver/function_validation_test.cc
+++ b/src/tint/resolver/function_validation_test.cc
@@ -22,11 +22,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 class ResolverFunctionValidationTest : public TestHelper, public testing::Test {};
 
 TEST_F(ResolverFunctionValidationTest, DuplicateParameterName) {
@@ -187,7 +188,7 @@
     Func(Source{{1, 2}}, "func", utils::Empty, ty.vec4<f32>(),
          utils::Vector{
              Discard(Source{{12, 34}}),
-             Return(Call(ty.vec4<f32>())),
+             Return(Call<vec4<f32>>()),
          },
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
@@ -926,7 +927,7 @@
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_NonPlain) {
-    auto ret_type = ty.ptr(Source{{12, 34}}, builtin::AddressSpace::kFunction, ty.i32());
+    auto ret_type = ty.ptr<function, i32>(Source{{12, 34}});
     Func("f", utils::Empty, ret_type, utils::Empty);
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/increment_decrement_validation_test.cc b/src/tint/resolver/increment_decrement_validation_test.cc
index c3a0c78..b0ce2e3 100644
--- a/src/tint/resolver/increment_decrement_validation_test.cc
+++ b/src/tint/resolver/increment_decrement_validation_test.cc
@@ -17,11 +17,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverIncrementDecrementValidationTest = ResolverTest;
 
 TEST_F(ResolverIncrementDecrementValidationTest, Increment_Signed) {
@@ -65,7 +66,7 @@
     // let b : ptr<function,i32> = &a;
     // *b++;
     auto* var_a = Var("a", ty.i32(), builtin::AddressSpace::kFunction);
-    auto* var_b = Let("b", ty.ptr<i32>(builtin::AddressSpace::kFunction), AddressOf(Expr("a")));
+    auto* var_b = Let("b", ty.ptr<function, i32>(), AddressOf(Expr("a")));
     WrapInFunction(var_a, var_b, Increment(Source{{12, 34}}, Deref("b")));
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/tint/resolver/inferred_type_test.cc b/src/tint/resolver/inferred_type_test.cc
index 4545419..b6b5481 100644
--- a/src/tint/resolver/inferred_type_test.cc
+++ b/src/tint/resolver/inferred_type_test.cc
@@ -17,27 +17,16 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 // Helpers and typedefs
 template <typename T>
 using DataType = builder::DataType<T>;
 template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
-template <typename T>
 using alias = builder::alias<T>;
 
 struct ResolverInferredTypeTest : public resolver::TestHelper, public testing::Test {};
diff --git a/src/tint/resolver/intrinsic_table_test.cc b/src/tint/resolver/intrinsic_table_test.cc
index 1d5627c..7b658ad8 100644
--- a/src/tint/resolver/intrinsic_table_test.cc
+++ b/src/tint/resolver/intrinsic_table_test.cc
@@ -36,15 +36,16 @@
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
 
 using Parameter = sem::Parameter;
 using ParameterUsage = sem::ParameterUsage;
 
-using AFloatV = builder::vec<3, AFloat>;
-using AIntV = builder::vec<3, AInt>;
-using f32V = builder::vec<3, f32>;
-using i32V = builder::vec<3, i32>;
-using u32V = builder::vec<3, u32>;
+using AFloatV = vec3<AFloat>;
+using AIntV = vec3<AInt>;
+using f32V = vec3<f32>;
+using i32V = vec3<i32>;
+using u32V = vec3<u32>;
 
 class IntrinsicTableTest : public testing::Test, public ProgramBuilder {
   public:
diff --git a/src/tint/resolver/is_storeable_test.cc b/src/tint/resolver/is_storeable_test.cc
index 65c1ab9..b7fa0e8 100644
--- a/src/tint/resolver/is_storeable_test.cc
+++ b/src/tint/resolver/is_storeable_test.cc
@@ -18,6 +18,8 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/type/atomic.h"
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+
 namespace tint::resolver {
 namespace {
 
@@ -112,7 +114,7 @@
 TEST_F(ResolverIsStorableTest, Struct_SomeMembersNonStorable) {
     Structure("S", utils::Vector{
                        Member("a", ty.i32()),
-                       Member("b", ty.ptr<i32>(builtin::AddressSpace::kPrivate)),
+                       Member("b", ty.ptr<private_, i32>()),
                    });
 
     EXPECT_FALSE(r()->Resolve());
@@ -135,11 +137,10 @@
 }
 
 TEST_F(ResolverIsStorableTest, Struct_NestedNonStorable) {
-    auto* non_storable =
-        Structure("nonstorable", utils::Vector{
-                                     Member("a", ty.i32()),
-                                     Member("b", ty.ptr<i32>(builtin::AddressSpace::kPrivate)),
-                                 });
+    auto* non_storable = Structure("nonstorable", utils::Vector{
+                                                      Member("a", ty.i32()),
+                                                      Member("b", ty.ptr<private_, i32>()),
+                                                  });
     Structure("S", utils::Vector{
                        Member("a", ty.i32()),
                        Member("b", ty.Of(non_storable)),
diff --git a/src/tint/resolver/load_test.cc b/src/tint/resolver/load_test.cc
index 2334227..48b9886 100644
--- a/src/tint/resolver/load_test.cc
+++ b/src/tint/resolver/load_test.cc
@@ -20,11 +20,12 @@
 #include "src/tint/type/reference.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverLoadTest = ResolverTest;
 
 TEST_F(ResolverLoadTest, VarInitializer) {
@@ -140,7 +141,7 @@
     // var v = array<i32, 3>(1i, 2i, 3i)[ref];
     auto* ident = Expr("ref");
     WrapInFunction(Var("ref", Expr(1_i)),  //
-                   IndexAccessor(array<i32, 3>(1_i, 2_i, 3_i), ident));
+                   IndexAccessor(Call<array<i32, 3>>(1_i, 2_i, 3_i), ident));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
     auto* load = Sem().Get<sem::Load>(ident);
@@ -154,7 +155,7 @@
     // var ref = vec4(1);
     // var v = ref.xyz;
     auto* ident = Expr("ref");
-    WrapInFunction(Var("ref", vec4<i32>(1_i)),  //
+    WrapInFunction(Var("ref", Call<vec4<i32>>(1_i)),  //
                    Var("v", MemberAccessor(ident, "xyz")));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -230,7 +231,7 @@
          },
          ty.vec4<f32>(),
          utils::Vector{
-             Return(Call("textureSampleLevel", "tp", "sp", vec2<f32>(), 0_a)),
+             Return(Call("textureSampleLevel", "tp", "sp", Call<vec2<f32>>(), 0_a)),
          });
     auto* t_ident = Expr("t");
     auto* s_ident = Expr("s");
diff --git a/src/tint/resolver/materialize_test.cc b/src/tint/resolver/materialize_test.cc
index cf67c93..7a45bf8 100644
--- a/src/tint/resolver/materialize_test.cc
+++ b/src/tint/resolver/materialize_test.cc
@@ -21,26 +21,27 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
-using AFloatV = builder::vec<3, AFloat>;
-using AFloatM = builder::mat<3, 2, AFloat>;
-using AFloatA = builder::array<3, AFloat>;
-using AIntV = builder::vec<3, AInt>;
-using AIntA = builder::array<3, AInt>;
-using f32V = builder::vec<3, f32>;
-using f16V = builder::vec<3, f16>;
-using i32V = builder::vec<3, i32>;
-using u32V = builder::vec<3, u32>;
-using f32M = builder::mat<3, 2, f32>;
-using f16M = builder::mat<3, 2, f16>;
-using f32A = builder::array<3, f32>;
-using f16A = builder::array<3, f16>;
-using i32A = builder::array<3, i32>;
-using u32A = builder::array<3, u32>;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+using AFloatV = vec3<AFloat>;
+using AFloatM = mat3x2<AFloat>;
+using AFloatA = array<AFloat, 3>;
+using AIntV = vec3<AInt>;
+using AIntA = array<AInt, 3>;
+using f32V = vec3<f32>;
+using f16V = vec3<f16>;
+using i32V = vec3<i32>;
+using u32V = vec3<u32>;
+using f32M = mat3x2<f32>;
+using f16M = mat3x2<f16>;
+using f32A = array<f32, 3>;
+using f16A = array<f16, 3>;
+using i32A = array<i32, 3>;
+using u32A = array<u32, 3>;
 
 constexpr double kTooBigF32 = static_cast<double>(3.5e+38);
 constexpr double kTooBigF16 = static_cast<double>(6.6e+4);
@@ -1276,7 +1277,7 @@
 
 TEST_F(MaterializeAbstractStructure, Modf_Vector_DefaultType) {
     // var v = modf(vec2(1));
-    auto* call = Call("modf", Call(ty.vec2<Infer>(), 1_a));
+    auto* call = Call("modf", Call<vec2<Infer>>(1_a));
     WrapInFunction(Decl(Var("v", call)));
     ASSERT_TRUE(r()->Resolve()) << r()->error();
     auto* sem = Sem().Get(call);
@@ -1316,8 +1317,8 @@
     // var v = modf(vec2(1_h)); // v is __modf_result_vec2_f16
     // v = modf(vec2(1));       // __modf_result_vec2_f16 <- __modf_result_vec2_abstract
     Enable(builtin::Extension::kF16);
-    auto* call = Call("modf", Call(ty.vec2<Infer>(), 1_a));
-    WrapInFunction(Decl(Var("v", Call("modf", Call(ty.vec2<Infer>(), 1_h)))), Assign("v", call));
+    auto* call = Call("modf", Call<vec2<Infer>>(1_a));
+    WrapInFunction(Decl(Var("v", Call("modf", Call<vec2<Infer>>(1_h)))), Assign("v", call));
     ASSERT_TRUE(r()->Resolve()) << r()->error();
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
@@ -1353,7 +1354,7 @@
 
 TEST_F(MaterializeAbstractStructure, Frexp_Vector_DefaultType) {
     // var v = frexp(vec2(1));
-    auto* call = Call("frexp", Call(ty.vec2<Infer>(), 1_a));
+    auto* call = Call("frexp", Call<vec2<Infer>>(1_a));
     WrapInFunction(Decl(Var("v", call)));
     ASSERT_TRUE(r()->Resolve()) << r()->error();
     auto* sem = Sem().Get(call);
@@ -1399,8 +1400,8 @@
     // var v = frexp(vec2(1_h)); // v is __frexp_result_vec2_f16
     // v = frexp(vec2(1));       // __frexp_result_vec2_f16 <- __frexp_result_vec2_abstract
     Enable(builtin::Extension::kF16);
-    auto* call = Call("frexp", Call(ty.vec2<Infer>(), 1_a));
-    WrapInFunction(Decl(Var("v", Call("frexp", Call(ty.vec2<Infer>(), 1_h)))), Assign("v", call));
+    auto* call = Call("frexp", Call<vec2<Infer>>(1_a));
+    WrapInFunction(Decl(Var("v", Call("frexp", Call<vec2<Infer>>(1_h)))), Assign("v", call));
     ASSERT_TRUE(r()->Resolve()) << r()->error();
     auto* sem = Sem().Get(call);
     ASSERT_TRUE(sem->Is<sem::Materialize>());
diff --git a/src/tint/resolver/ptr_ref_validation_test.cc b/src/tint/resolver/ptr_ref_validation_test.cc
index f6da233..2da5cf0 100644
--- a/src/tint/resolver/ptr_ref_validation_test.cc
+++ b/src/tint/resolver/ptr_ref_validation_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/type/reference.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 struct ResolverPtrRefValidationTest : public resolver::TestHelper, public testing::Test {};
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfLiteral) {
@@ -143,12 +144,11 @@
     // }
     auto* inner = Structure("Inner", utils::Vector{Member("arr", ty.array<i32, 4>())});
     auto* buf = Structure("S", utils::Vector{Member("inner", ty.Of(inner))});
-    auto* storage = GlobalVar("s", ty.Of(buf), builtin::AddressSpace::kStorage,
-                              builtin::Access::kReadWrite, Binding(0_a), Group(0_a));
+    auto* var = GlobalVar("s", ty.Of(buf), builtin::AddressSpace::kStorage,
+                          builtin::Access::kReadWrite, Binding(0_a), Group(0_a));
 
-    auto* expr = IndexAccessor(MemberAccessor(MemberAccessor(storage, "inner"), "arr"), 2_i);
-    auto* ptr =
-        Let(Source{{12, 34}}, "p", ty.ptr<i32>(builtin::AddressSpace::kStorage), AddressOf(expr));
+    auto* expr = IndexAccessor(MemberAccessor(MemberAccessor(var, "inner"), "arr"), 2_i);
+    auto* ptr = Let(Source{{12, 34}}, "p", ty.ptr<storage, i32>(), AddressOf(expr));
 
     WrapInFunction(ptr);
 
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 74a462a..51bf27c 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3570,6 +3570,30 @@
     return static_cast<uint32_t>(value);
 }
 
+utils::Result<uint32_t> Resolver::IndexAttribute(const ast::IndexAttribute* attr) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@index 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 && value != 1) {
+        AddError("@index value must be zero or one", attr->source);
+        return utils::Failure;
+    }
+
+    return static_cast<uint32_t>(value);
+}
+
 utils::Result<uint32_t> Resolver::BindingAttribute(const ast::BindingAttribute* attr) {
     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
@@ -4142,6 +4166,14 @@
                     attributes.location = value.Get();
                     return true;
                 },
+                [&](const ast::IndexAttribute* attr) {
+                    auto value = IndexAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.index = value.Get();
+                    return true;
+                },
                 [&](const ast::BuiltinAttribute* attr) {
                     auto value = BuiltinAttribute(attr);
                     if (!value) {
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 5d13d25..46ac83f 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -320,6 +320,10 @@
     /// @returns the location value on success.
     utils::Result<uint32_t> LocationAttribute(const ast::LocationAttribute* attr);
 
+    /// Resolves the `@index` attribute @p attr
+    /// @returns the index value on success.
+    utils::Result<uint32_t> IndexAttribute(const ast::IndexAttribute* attr);
+
     /// Resolves the `@binding` attribute @p attr
     /// @returns the binding value on success.
     utils::Result<uint32_t> BindingAttribute(const ast::BindingAttribute* attr);
diff --git a/src/tint/resolver/resolver_behavior_test.cc b/src/tint/resolver/resolver_behavior_test.cc
index bc4af42..378642b 100644
--- a/src/tint/resolver/resolver_behavior_test.cc
+++ b/src/tint/resolver/resolver_behavior_test.cc
@@ -22,11 +22,12 @@
 #include "src/tint/sem/value_expression.h"
 #include "src/tint/sem/while_statement.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 class ResolverBehaviorTest : public ResolverTest {
   protected:
     void SetUp() override {
@@ -82,7 +83,7 @@
     Func("ArrayDiscardOrNext", utils::Empty, ty.array<i32, 4>(),
          utils::Vector{
              If(true, Block(Discard())),
-             Return(array<i32, 4>()),
+             Return(Call<array<i32, 4>>()),
          });
 
     auto* stmt = Decl(Var("lhs", ty.i32(), IndexAccessor(Call("ArrayDiscardOrNext"), 1_i)));
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index eab9b86..c4d57aa 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -48,37 +48,18 @@
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/string_stream.h"
 
+namespace tint::resolver {
+namespace {
+
 using ::testing::ElementsAre;
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
-namespace tint::resolver {
-namespace {
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 // Helpers and typedefs
 template <typename T>
 using DataType = builder::DataType<T>;
-template <int N, typename T>
-using vec = builder::vec<N, T>;
-template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <int N, int M, typename T>
-using mat = builder::mat<N, M, T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat2x3 = builder::mat2x3<T>;
-template <typename T>
-using mat3x2 = builder::mat3x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
 template <typename T, int ID = 0>
 using alias = builder::alias<T, ID>;
 template <typename T>
@@ -666,7 +647,7 @@
 }
 
 TEST_F(ResolverTest, Expr_Initializer_Type_Vec2) {
-    auto* tc = vec2<f32>(1_f, 1_f);
+    auto* tc = Call<vec2<f32>>(1_f, 1_f);
     WrapInFunction(tc);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -678,7 +659,7 @@
 }
 
 TEST_F(ResolverTest, Expr_Initializer_Type_Vec3) {
-    auto* tc = vec3<f32>(1_f, 1_f, 1_f);
+    auto* tc = Call<vec3<f32>>(1_f, 1_f, 1_f);
     WrapInFunction(tc);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -690,7 +671,7 @@
 }
 
 TEST_F(ResolverTest, Expr_Initializer_Type_Vec4) {
-    auto* tc = vec4<f32>(1_f, 1_f, 1_f, 1_f);
+    auto* tc = Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f);
     WrapInFunction(tc);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -756,7 +737,7 @@
     // var a : array<bool, 10u> = 0;
     // var idx : f32 = f32();
     // var f : f32 = a[idx];
-    auto* a = Var("a", ty.array<bool, 10>(), array<bool, 10>());
+    auto* a = Var("a", ty.array<bool, 10>(), Call<array<bool, 10>>());
     auto* idx = Var("idx", ty.f32(), Call<f32>());
     auto* f = Var("f", ty.f32(), IndexAccessor("a", Expr(Source{{12, 34}}, idx)));
     Func("my_func", utils::Empty, ty.void_(),
@@ -803,7 +784,7 @@
     auto* v = Expr("v");
     auto* p = Expr("p");
     auto* v_decl = Decl(Var("v", ty.f32()));
-    auto* p_decl = Decl(Let("p", ty.ptr<f32>(builtin::AddressSpace::kFunction), AddressOf(v)));
+    auto* p_decl = Decl(Let("p", ty.ptr<function, f32>(), AddressOf(v)));
     auto* assign = Assign(Deref(p), 1.23_f);
     Func("my_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -2138,7 +2119,7 @@
               Binding(1_a));
     GlobalVar("s", ty.sampler(type::SamplerKind::kSampler), Group(1_a), Binding(2_a));
 
-    auto* call = Call("textureSample", "t", "s", vec2<f32>(1_f, 2_f));
+    auto* call = Call("textureSample", "t", "s", Call<vec2<f32>>(1_f, 2_f));
     const ast::Function* f =
         Func("test_function", utils::Empty, ty.void_(), utils::Vector{Assign(Phony(), call)},
              utils::Vector{Stage(ast::PipelineStage::kFragment)});
@@ -2157,7 +2138,7 @@
               Binding(1_a));
     GlobalVar("s", ty.sampler(type::SamplerKind::kSampler), Group(1_a), Binding(2_a));
 
-    auto* inner_call = Assign(Phony(), Call("textureSample", "t", "s", vec2<f32>(1_f, 2_f)));
+    auto* inner_call = Assign(Phony(), Call("textureSample", "t", "s", Call<vec2<f32>>(1_f, 2_f)));
     const ast::Function* inner_func =
         Func("inner_func", utils::Empty, ty.void_(), utils::Vector{inner_call});
     auto* outer_call = CallStmt(Call("inner_func"));
@@ -2183,10 +2164,12 @@
               Binding(1_a));
     GlobalVar("s", ty.sampler(type::SamplerKind::kSampler), Group(1_a), Binding(2_a));
 
-    auto* inner_call_1 = Assign(Phony(), Call("textureSample", "t", "s", vec2<f32>(1_f, 2_f)));
+    auto* inner_call_1 =
+        Assign(Phony(), Call("textureSample", "t", "s", Call<vec2<f32>>(1_f, 2_f)));
     const ast::Function* inner_func_1 =
         Func("inner_func_1", utils::Empty, ty.void_(), utils::Vector{inner_call_1});
-    auto* inner_call_2 = Assign(Phony(), Call("textureSample", "t", "s", vec2<f32>(3_f, 4_f)));
+    auto* inner_call_2 =
+        Assign(Phony(), Call("textureSample", "t", "s", Call<vec2<f32>>(3_f, 4_f)));
     const ast::Function* inner_func_2 =
         Func("inner_func_2", utils::Empty, ty.void_(), utils::Vector{inner_call_2});
     auto* outer_call_1 = CallStmt(Call("inner_func_1"));
@@ -2220,10 +2203,12 @@
               Binding(2_a));
     GlobalVar("s", ty.sampler(type::SamplerKind::kSampler), Group(1_a), Binding(3_a));
 
-    auto* inner_call_1 = Assign(Phony(), Call("textureSample", "t1", "s", vec2<f32>(1_f, 2_f)));
+    auto* inner_call_1 =
+        Assign(Phony(), Call("textureSample", "t1", "s", Call<vec2<f32>>(1_f, 2_f)));
     const ast::Function* inner_func_1 =
         Func("inner_func_1", utils::Empty, ty.void_(), utils::Vector{inner_call_1});
-    auto* inner_call_2 = Assign(Phony(), Call("textureSample", "t2", "s", vec2<f32>(3_f, 4_f)));
+    auto* inner_call_2 =
+        Assign(Phony(), Call("textureSample", "t2", "s", Call<vec2<f32>>(3_f, 4_f)));
     const ast::Function* inner_func_2 =
         Func("inner_func_2", utils::Empty, ty.void_(), utils::Vector{inner_call_2});
     auto* outer_call_1 = CallStmt(Call("inner_func_1"));
@@ -2299,10 +2284,9 @@
 
     Func("helper",
          utils::Vector{
-             Param("sl", ty.ptr(builtin::AddressSpace::kFunction,
-                                ty.sampler(type::SamplerKind::kSampler))),
-             Param("tl", ty.ptr(builtin::AddressSpace::kFunction,
-                                ty.sampled_texture(type::TextureDimension::k2d, ty.f32()))),
+             Param("sl", ty.ptr<function>(ty.sampler(type::SamplerKind::kSampler))),
+             Param("tl",
+                   ty.ptr<function>(ty.sampled_texture(type::TextureDimension::k2d, ty.f32()))),
          },
          ty.vec4<f32>(),
          utils::Vector{
@@ -2537,7 +2521,7 @@
 }
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfVector_Valid) {
-    auto a = ty.array(ty.vec3<i32>(), 10_u);
+    auto a = ty.array<vec3<i32>, 10>();
     size_t depth = 2;  // Depth of array + vector
     size_t iterations = kMaxNestDepthOfCompositeType - depth;
     for (size_t i = 0; i < iterations; ++i) {
diff --git a/src/tint/resolver/resolver_test_helper.h b/src/tint/resolver/resolver_test_helper.h
index 706ba89..4f72b9c 100644
--- a/src/tint/resolver/resolver_test_helper.h
+++ b/src/tint/resolver/resolver_test_helper.h
@@ -130,51 +130,6 @@
 
 namespace builder {
 
-template <uint32_t N, typename T>
-struct vec {};
-
-template <typename T>
-using vec2 = vec<2, T>;
-
-template <typename T>
-using vec3 = vec<3, T>;
-
-template <typename T>
-using vec4 = vec<4, T>;
-
-template <uint32_t N, uint32_t M, typename T>
-struct mat {};
-
-template <typename T>
-using mat2x2 = mat<2, 2, T>;
-
-template <typename T>
-using mat2x3 = mat<2, 3, T>;
-
-template <typename T>
-using mat2x4 = mat<2, 4, T>;
-
-template <typename T>
-using mat3x2 = mat<3, 2, T>;
-
-template <typename T>
-using mat3x3 = mat<3, 3, T>;
-
-template <typename T>
-using mat3x4 = mat<3, 4, T>;
-
-template <typename T>
-using mat4x2 = mat<4, 2, T>;
-
-template <typename T>
-using mat4x3 = mat<4, 3, T>;
-
-template <typename T>
-using mat4x4 = mat<4, 4, T>;
-
-template <uint32_t N, typename T>
-struct array {};
-
 template <typename TO, int ID = 0>
 struct alias {};
 
@@ -187,9 +142,6 @@
 template <typename TO>
 using alias3 = alias<TO, 3>;
 
-template <typename TO>
-struct ptr {};
-
 /// A scalar value
 using Scalar = std::variant<i32, u32, f32, f16, AInt, AFloat, bool>;
 
@@ -446,7 +398,7 @@
 
 /// Helper for building vector types and expressions
 template <uint32_t N, typename T>
-struct DataType<vec<N, T>> {
+struct DataType<builtin::fluent_types::vec<N, T>> {
     /// The element type
     using ElementType = T;
 
@@ -457,7 +409,7 @@
     /// @return a new AST vector type
     static inline ast::Type AST(ProgramBuilder& b) {
         if (IsInferOrAbstract<T>) {
-            return b.ty.vec<Infer, N>();
+            return b.ty.vec<builtin::fluent_types::Infer, N>();
         } else {
             return b.ty.vec(DataType<T>::AST(b), N);
         }
@@ -498,7 +450,7 @@
 
 /// Helper for building matrix types and expressions
 template <uint32_t N, uint32_t M, typename T>
-struct DataType<mat<N, M, T>> {
+struct DataType<builtin::fluent_types::mat<N, M, T>> {
     /// The element type
     using ElementType = T;
 
@@ -509,7 +461,7 @@
     /// @return a new AST matrix type
     static inline ast::Type AST(ProgramBuilder& b) {
         if (IsInferOrAbstract<T>) {
-            return b.ty.mat<Infer, N, M>();
+            return b.ty.mat<builtin::fluent_types::Infer, N, M>();
         } else {
             return b.ty.mat(DataType<T>::AST(b), N, M);
         }
@@ -535,13 +487,14 @@
         utils::Vector<const ast::Expression*, N> r;
         for (uint32_t i = 0; i < N; ++i) {
             if (one_value) {
-                r.Push(DataType<vec<M, T>>::Expr(b, utils::Vector<Scalar, 1>{args[0]}));
+                r.Push(DataType<builtin::fluent_types::vec<M, T>>::Expr(
+                    b, utils::Vector<Scalar, 1>{args[0]}));
             } else {
                 utils::Vector<Scalar, M> v;
                 for (size_t j = 0; j < M; ++j) {
                     v.Push(args[next++]);
                 }
-                r.Push(DataType<vec<M, T>>::Expr(b, std::move(v)));
+                r.Push(DataType<builtin::fluent_types::vec<M, T>>::Expr(b, std::move(v)));
             }
         }
         return r;
@@ -618,7 +571,8 @@
 
 /// Helper for building pointer types and expressions
 template <typename T>
-struct DataType<ptr<T>> {
+struct DataType<
+    builtin::fluent_types::ptr<builtin::AddressSpace::kPrivate, T, builtin::Access::kUndefined>> {
     /// The element type
     using ElementType = typename DataType<T>::ElementType;
 
@@ -628,9 +582,9 @@
     /// @param b the ProgramBuilder
     /// @return a new AST alias type
     static inline ast::Type AST(ProgramBuilder& b) {
-        return b.ty.ptr(builtin::AddressSpace::kPrivate, DataType<T>::AST(b),
-                        builtin::Access::kUndefined);
+        return b.ty.ptr<builtin::AddressSpace::kPrivate, T>();
     }
+
     /// @param b the ProgramBuilder
     /// @return the semantic aliased type
     static inline const type::Type* Sem(ProgramBuilder& b) {
@@ -659,8 +613,8 @@
 };
 
 /// Helper for building array types and expressions
-template <uint32_t N, typename T>
-struct DataType<array<N, T>> {
+template <typename T, uint32_t N>
+struct DataType<builtin::fluent_types::array<T, N>> {
     /// The element type
     using ElementType = typename DataType<T>::ElementType;
 
@@ -673,7 +627,7 @@
         if (auto ast = DataType<T>::AST(b)) {
             return b.ty.array(ast, u32(N));
         }
-        return b.ty.array<Infer>();
+        return b.ty.array<builtin::fluent_types::Infer>();
     }
     /// @param b the ProgramBuilder
     /// @return the semantic array type
@@ -832,7 +786,7 @@
                   "Vector args must all be the same type");
     constexpr size_t N = sizeof...(args);
     utils::Vector<Scalar, sizeof...(args)> v{args...};
-    return Value::Create<vec<N, FirstT>>(std::move(v));
+    return Value::Create<builtin::fluent_types::vec<N, FirstT>>(std::move(v));
 }
 
 /// Creates a Value of DataType<array<N, T>> from N scalar `args`
@@ -843,7 +797,7 @@
                   "Array args must all be the same type");
     constexpr size_t N = sizeof...(args);
     utils::Vector<Scalar, sizeof...(args)> v{args...};
-    return Value::Create<array<N, FirstT>>(std::move(v));
+    return Value::Create<builtin::fluent_types::array<FirstT, N>>(std::move(v));
 }
 
 /// Creates a Value of DataType<mat<C,R,T> from C*R scalar `args`
@@ -855,7 +809,7 @@
             m.Push(m_in[i][j]);
         }
     }
-    return Value::Create<mat<C, R, T>>(std::move(m));
+    return Value::Create<builtin::fluent_types::mat<C, R, T>>(std::move(m));
 }
 
 /// Creates a Value of DataType<mat<2,R,T> from column vectors `c0` and `c1`
@@ -869,7 +823,7 @@
     for (auto v : c1) {
         m.Push(v);
     }
-    return Value::Create<mat<C, R, T>>(std::move(m));
+    return Value::Create<builtin::fluent_types::mat<C, R, T>>(std::move(m));
 }
 
 /// Creates a Value of DataType<mat<3,R,T> from column vectors `c0`, `c1`, and `c2`
@@ -886,7 +840,7 @@
     for (auto v : c2) {
         m.Push(v);
     }
-    return Value::Create<mat<C, R, T>>(std::move(m));
+    return Value::Create<builtin::fluent_types::mat<C, R, T>>(std::move(m));
 }
 
 /// Creates a Value of DataType<mat<4,R,T> from column vectors `c0`, `c1`, `c2`, and `c3`
@@ -906,7 +860,7 @@
     for (auto v : c3) {
         m.Push(v);
     }
-    return Value::Create<mat<C, R, T>>(std::move(m));
+    return Value::Create<builtin::fluent_types::mat<C, R, T>>(std::move(m));
 }
 }  // namespace builder
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/root_identifier_test.cc b/src/tint/resolver/root_identifier_test.cc
index 4ff43cb..f6d88f8 100644
--- a/src/tint/resolver/root_identifier_test.cc
+++ b/src/tint/resolver/root_identifier_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/sem/member_accessor_expression.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 class ResolverRootIdentifierTest : public ResolverTest {};
 
 TEST_F(ResolverRootIdentifierTest, GlobalPrivateVar) {
@@ -142,7 +143,7 @@
     // {
     //   let b = a;
     // }
-    auto* param = Param("a", ty.ptr(builtin::AddressSpace::kFunction, ty.f32()));
+    auto* param = Param("a", ty.ptr<function, f32>());
     auto* expr_param = Expr(param);
     auto* let = Let("b", expr_param);
     auto* expr_let = Expr("b");
diff --git a/src/tint/resolver/struct_pipeline_stage_use_test.cc b/src/tint/resolver/struct_pipeline_stage_use_test.cc
index b4857f0..70e6046 100644
--- a/src/tint/resolver/struct_pipeline_stage_use_test.cc
+++ b/src/tint/resolver/struct_pipeline_stage_use_test.cc
@@ -20,13 +20,13 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/sem/struct.h"
 
-using ::testing::UnorderedElementsAre;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using ::testing::UnorderedElementsAre;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverPipelineStageUseTest = ResolverTest;
 
 TEST_F(ResolverPipelineStageUseTest, UnusedStruct) {
@@ -68,7 +68,7 @@
     auto* s = Structure("S", utils::Vector{Member("a", ty.f32(), utils::Vector{Location(0_a)})});
 
     Func("main", utils::Vector{Param("param", ty.Of(s))}, ty.vec4<f32>(),
-         utils::Vector{Return(Call(ty.vec4<f32>()))},
+         utils::Vector{Return(Call<vec4<f32>>())},
          utils::Vector{Stage(ast::PipelineStage::kVertex)},
          utils::Vector{Builtin(builtin::BuiltinValue::kPosition)});
 
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index de79f85..117b2ad 100644
--- a/src/tint/resolver/type_validation_test.cc
+++ b/src/tint/resolver/type_validation_test.cc
@@ -23,41 +23,16 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 // Helpers and typedefs
 template <typename T>
 using DataType = builder::DataType<T>;
 template <typename T>
-using vec2 = builder::vec2<T>;
-template <typename T>
-using vec3 = builder::vec3<T>;
-template <typename T>
-using vec4 = builder::vec4<T>;
-template <typename T>
-using mat2x2 = builder::mat2x2<T>;
-template <typename T>
-using mat2x3 = builder::mat2x3<T>;
-template <typename T>
-using mat2x4 = builder::mat2x4<T>;
-template <typename T>
-using mat3x2 = builder::mat3x2<T>;
-template <typename T>
-using mat3x3 = builder::mat3x3<T>;
-template <typename T>
-using mat3x4 = builder::mat3x4<T>;
-template <typename T>
-using mat4x2 = builder::mat4x2<T>;
-template <typename T>
-using mat4x3 = builder::mat4x3<T>;
-template <typename T>
-using mat4x4 = builder::mat4x4<T>;
-template <int N, typename T>
-using array = builder::array<N, T>;
-template <typename T>
 using alias = builder::alias<T>;
 template <typename T>
 using alias1 = builder::alias1<T>;
@@ -331,7 +306,7 @@
 TEST_F(ResolverTypeValidationTest, ArraySize_IVecConst) {
     // const size = vec2<i32>(100, 100);
     // var<private> a : array<f32, size>;
-    GlobalConst("size", Call(ty.vec2<i32>(), 100_i, 100_i));
+    GlobalConst("size", Call<vec2<i32>>(100_i, 100_i));
     GlobalVar("a", ty.array(ty.f32(), Expr(Source{{12, 34}}, "size")),
               builtin::AddressSpace::kPrivate);
     EXPECT_FALSE(r()->Resolve());
@@ -881,7 +856,7 @@
 }
 
 TEST_F(ResolverTypeValidationTest, ArrayOfNonStorableTypeWithStride) {
-    auto ptr_ty = ty.ptr<u32>(Source{{12, 34}}, builtin::AddressSpace::kUniform);
+    auto ptr_ty = ty.ptr<uniform, u32>(Source{{12, 34}});
     GlobalVar("arr", ty.array(ptr_ty, 4_i, utils::Vector{Stride(16)}),
               builtin::AddressSpace::kPrivate);
 
@@ -1305,8 +1280,8 @@
                                          ParamsFor<mat2x2<f16>>(3, 2),
                                          ParamsFor<mat3x3<f16>>(3, 3),
                                          ParamsFor<mat4x4<f16>>(3, 4),
-                                         ParamsFor<array<2, f32>>(4, 2),
-                                         ParamsFor<array<2, f16>>(4, 2)));
+                                         ParamsFor<array<f32, 2>>(4, 2),
+                                         ParamsFor<array<f16, 2>>(4, 2)));
 }  // namespace MatrixTests
 
 namespace VectorTests {
@@ -1366,8 +1341,7 @@
               builtin::AddressSpace::kPrivate);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: vector element type must be 'bool', 'f32', 'f16', 'i32' "
-              "or 'u32'");
+              "12:34 error: vector element type must be 'bool', 'f32', 'f16', 'i32' or 'u32'");
 }
 INSTANTIATE_TEST_SUITE_P(ResolverTypeValidationTest,
                          InvalidVectorElementTypes,
@@ -1377,7 +1351,7 @@
                                          ParamsFor<mat2x2<f32>>(2),
                                          ParamsFor<mat3x3<f16>>(2),
                                          ParamsFor<mat4x4<f32>>(2),
-                                         ParamsFor<array<2, f32>>(2)));
+                                         ParamsFor<array<f32, 2>>(2)));
 }  // namespace VectorTests
 
 namespace BuiltinTypeAliasTests {
diff --git a/src/tint/resolver/uniformity_test.cc b/src/tint/resolver/uniformity_test.cc
index e890bcb..1cb9142 100644
--- a/src/tint/resolver/uniformity_test.cc
+++ b/src/tint/resolver/uniformity_test.cc
@@ -25,11 +25,12 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 class UniformityAnalysisTestBase {
   protected:
     /// Build and resolve a program from a ProgramBuilder object.
@@ -5301,8 +5302,7 @@
     }
     foo_body.Push(b.Decl(b.Let("rhs", rhs_init)));
     for (int i = 0; i < 255; i++) {
-        params.Push(
-            b.Param("p" + std::to_string(i), ty.ptr(builtin::AddressSpace::kFunction, ty.i32())));
+        params.Push(b.Param("p" + std::to_string(i), ty.ptr<function, i32>()));
         if (i > 0) {
             foo_body.Push(b.Assign(b.Deref("p" + std::to_string(i)), "rhs"));
         }
diff --git a/src/tint/resolver/validation_test.cc b/src/tint/resolver/validation_test.cc
index ca958ad..23fb56a 100644
--- a/src/tint/resolver/validation_test.cc
+++ b/src/tint/resolver/validation_test.cc
@@ -40,14 +40,14 @@
 #include "src/tint/type/sampled_texture.h"
 #include "src/tint/type/texture_dimension.h"
 
-using ::testing::ElementsAre;
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ResolverValidationTest = ResolverTest;
 
 class FakeStmt final : public utils::Castable<FakeStmt, ast::Statement> {
@@ -380,7 +380,7 @@
     //     let x: f32 = (*p).z;
     //     return x;
     // }
-    auto* p = Param("p", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()));
+    auto* p = Param("p", ty.ptr<function, vec4<f32>>());
     auto* star_p = Deref(p);
     auto* accessor_expr = MemberAccessor(star_p, "z");
     auto* x = Var("x", ty.f32(), accessor_expr);
@@ -397,7 +397,7 @@
     //     let x: f32 = *p.z;
     //     return x;
     // }
-    auto* p = Param("p", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()));
+    auto* p = Param("p", ty.ptr<function, vec4<f32>>());
     auto* accessor_expr = MemberAccessor(p, Ident(Source{{12, 34}}, "z"));
     auto* star_p = Deref(accessor_expr);
     auto* x = Var("x", ty.f32(), star_p);
@@ -1234,8 +1234,8 @@
 
 TEST_F(ResolverTest, Expr_Initializer_Cast_Pointer) {
     auto* vf = Var("vf", ty.f32());
-    auto* c = Call(Source{{12, 34}}, ty.ptr<i32>(builtin::AddressSpace::kFunction), ExprList(vf));
-    auto* ip = Let("ip", ty.ptr<i32>(builtin::AddressSpace::kFunction), c);
+    auto* c = Call(Source{{12, 34}}, ty.ptr<function, i32>(), ExprList(vf));
+    auto* ip = Let("ip", ty.ptr<function, i32>(), c);
     WrapInFunction(Decl(vf), Decl(ip));
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index eb009c2..17b75e1 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -29,6 +29,7 @@
 #include "src/tint/ast/for_loop_statement.h"
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/internal_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/loop_statement.h"
@@ -1115,6 +1116,8 @@
                                        is_input)) {
                     return false;
                 }
+            } else if (auto* index_attr = attr->As<ast::IndexAttribute>()) {
+                return IndexAttribute(index_attr);
             } else if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
                 if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
                     is_invalid_compute_shader_attribute = true;
@@ -2113,6 +2116,7 @@
                     }
                     return true;
                 },
+                [&](const ast::IndexAttribute* index) { return IndexAttribute(index); },
                 [&](const ast::BuiltinAttribute* builtin_attr) {
                     if (!BuiltinAttribute(builtin_attr, member->Type(), stage,
                                           /* is_input */ false)) {
@@ -2197,6 +2201,18 @@
     return true;
 }
 
+bool Validator::IndexAttribute(const ast::IndexAttribute* index_attr) const {
+    if (!enabled_extensions_.Contains(builtin::Extension::kChromiumInternalDualSourceBlending)) {
+        AddError(
+            "use of '@index' attribute requires enabling extension "
+            "'chromium_internal_dual_source_blending'",
+            index_attr->source);
+        return false;
+    }
+
+    return false;
+}
+
 bool Validator::Return(const ast::ReturnStatement* ret,
                        const type::Type* func_type,
                        const type::Type* ret_type,
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 999bc49..19ceb5e 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -329,6 +329,11 @@
                            const Source& source,
                            const bool is_input = false) const;
 
+    /// Validates a index attribute
+    /// @param index_attr the index attribute to validate
+    /// @returns true on success, false otherwise.
+    bool IndexAttribute(const ast::IndexAttribute* index_attr) const;
+
     /// Validates a loop statement
     /// @param stmt the loop statement
     /// @returns true on success, false otherwise.
diff --git a/src/tint/resolver/value_constructor_validation_test.cc b/src/tint/resolver/value_constructor_validation_test.cc
index 3b7ad64..4e41d25 100644
--- a/src/tint/resolver/value_constructor_validation_test.cc
+++ b/src/tint/resolver/value_constructor_validation_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/type/reference.h"
 #include "src/tint/utils/string_stream.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ::testing::HasSubstr;
 
 // Helpers and typedefs
@@ -34,14 +35,6 @@
 using builder::CreatePtrs;
 using builder::CreatePtrsFor;
 using builder::DataType;
-using builder::mat2x2;
-using builder::mat2x3;
-using builder::mat3x2;
-using builder::mat3x3;
-using builder::mat4x4;
-using builder::vec2;
-using builder::vec3;
-using builder::vec4;
 
 class ResolverValueConstructorValidationTest : public resolver::TestHelper, public testing::Test {};
 
@@ -478,7 +471,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, ConversionConstructorInvalid_InvalidConstructor) {
-    auto* a = Var("a", ty.f32(), Call(Source{{12, 34}}, ty.f32(), Call(ty.array<f32, 4>())));
+    auto* a = Var("a", ty.f32(), Call(Source{{12, 34}}, ty.f32(), Call<array<f32, 4>>()));
     WrapInFunction(a);
 
     ASSERT_FALSE(r()->Resolve());
@@ -492,7 +485,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, Array_ZeroValue_Pass) {
     // array<u32, 10u>();
-    auto* tc = array<u32, 10>();
+    auto* tc = Call<array<u32, 10>>();
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -508,7 +501,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, Array_U32U32U32) {
     // array<u32, 3u>(0u, 10u, 20u);
-    auto* tc = array<u32, 3>(0_u, 10_u, 20_u);
+    auto* tc = Call<array<u32, 3>>(0_u, 10_u, 20_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -527,7 +520,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArray_U32U32U32) {
     // array(0u, 10u, 20u);
-    auto* tc = array<Infer>(Source{{12, 34}}, 0_u, 10_u, 20_u);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 0_u, 10_u, 20_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -546,7 +539,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, Array_U32AIU32) {
     // array<u32, 3u>(0u, 10, 20u);
-    auto* tc = array<u32, 3>(0_u, 10_a, 20_u);
+    auto* tc = Call<array<u32, 3>>(0_u, 10_a, 20_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -565,7 +558,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArray_U32AIU32) {
     // array(0u, 10u, 20u);
-    auto* tc = array<Infer>(Source{{12, 34}}, 0_u, 10_a, 20_u);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 0_u, 10_a, 20_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -584,7 +577,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayU32_AIAIAI) {
     // array<u32, 3u>(0, 10, 20);
-    auto* tc = array<u32, 3>(0_a, 10_a, 20_a);
+    auto* tc = Call<array<u32, 3>>(0_a, 10_a, 20_a);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -603,7 +596,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArray_AIAIAI) {
     // const c = array(0, 10, 20);
-    auto* tc = array<Infer>(Source{{12, 34}}, 0_a, 10_a, 20_a);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 0_a, 10_a, 20_a);
     WrapInFunction(Decl(Const("C", tc)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -622,9 +615,9 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayU32_VecI32_VecAI) {
     // array(vec2(10i), vec2(20));
-    auto* tc = array<Infer>(Source{{12, 34}},              //
-                            Call(ty.vec<Infer>(2), 20_i),  //
-                            Call(ty.vec<Infer>(2), 20_a));
+    auto* tc = Call<array<Infer>>(Source{{12, 34}},         //
+                                  Call<vec2<Infer>>(20_i),  //
+                                  Call<vec2<Infer>>(20_a));
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -644,9 +637,9 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayU32_VecAI_VecF32) {
     // array(vec2(20), vec2(10f));
-    auto* tc = array<Infer>(Source{{12, 34}},              //
-                            Call(ty.vec<Infer>(2), 20_a),  //
-                            Call(ty.vec<Infer>(2), 20_f));
+    auto* tc = Call<array<Infer>>(Source{{12, 34}},         //
+                                  Call<vec2<Infer>>(20_a),  //
+                                  Call<vec2<Infer>>(20_f));
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -666,7 +659,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_U32F32) {
     // array<u32, 3u>(0u, 1.0f, 20u);
-    auto* tc = array<u32, 3>(0_u, Expr(Source{{12, 34}}, 1_f), 20_u);
+    auto* tc = Call<array<u32, 3>>(0_u, Expr(Source{{12, 34}}, 1_f), 20_u);
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -675,7 +668,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_U32F32) {
     // array(0u, 1.0f, 20u);
-    auto* tc = array<Infer>(Source{{12, 34}}, 0_u, 1_f, 20_u);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 0_u, 1_f, 20_u);
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -687,7 +680,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_F32I32) {
     // array<f32, 1u>(1i);
-    auto* tc = array<f32, 1>(Expr(Source{{12, 34}}, 1_i));
+    auto* tc = Call<array<f32, 1>>(Expr(Source{{12, 34}}, 1_i));
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -696,7 +689,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_F32I32) {
     // array(1f, 1i);
-    auto* tc = array<Infer>(Source{{12, 34}}, 1_f, 1_i);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 1_f, 1_i);
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -708,7 +701,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_U32I32) {
     // array<u32, 1u>(1i, 0u, 0u, 0u, 0u, 0u);
-    auto* tc = array<u32, 1>(Expr(Source{{12, 34}}, 1_i), 0_u, 0_u, 0_u, 0_u);
+    auto* tc = Call<array<u32, 1>>(Expr(Source{{12, 34}}, 1_i), 0_u, 0_u, 0_u, 0_u);
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -717,7 +710,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_U32I32) {
     // array(1i, 0u, 0u, 0u, 0u, 0u);
-    auto* tc = array<Infer>(Source{{12, 34}}, 1_i, 0_u, 0_u, 0_u, 0_u);
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 1_i, 0_u, 0_u, 0_u, 0_u);
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -729,7 +722,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_I32Vec2) {
     // array<i32, 3u>(1i, vec2<i32>());
-    auto* tc = array<i32, 3>(1_i, vec2<i32>(Source{{12, 34}}));
+    auto* tc = Call<array<i32, 3>>(1_i, Call<vec2<i32>>(Source{{12, 34}}));
     WrapInFunction(tc);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -738,7 +731,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_I32Vec2) {
     // array(1i, vec2<i32>());
-    auto* tc = array<Infer>(Source{{12, 34}}, 1_i, vec2<i32>());
+    auto* tc = Call<array<Infer>>(Source{{12, 34}}, 1_i, Call<vec2<i32>>());
     WrapInFunction(tc);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -749,7 +742,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_Vec3i32_Vec3u32) {
     // array<vec3<i32>, 2u>(vec3<u32>(), vec3<u32>());
-    auto* t = array(ty.vec3<i32>(), 2_u, vec3<u32>(Source{{12, 34}}), vec3<u32>());
+    auto* t = Call<array<vec3<i32>, 2>>(Call<vec3<u32>>(Source{{12, 34}}), Call<vec3<u32>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -759,7 +752,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_Vec3i32_Vec3u32) {
     // array(vec3<i32>(), vec3<u32>());
-    auto* t = array<Infer>(Source{{12, 34}}, vec3<i32>(), vec3<u32>());
+    auto* t = Call<array<Infer>>(Source{{12, 34}}, Call<vec3<i32>>(), Call<vec3<u32>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -771,7 +764,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_Vec3i32_Vec3AF) {
     // array(vec3<i32>(), vec3(1.0));
-    auto* t = array<Infer>(Source{{12, 34}}, vec3<i32>(), Call("vec3", 1._a));
+    auto* t = Call<array<Infer>>(Source{{12, 34}}, Call<vec3<i32>>(), Call("vec3", 1._a));
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -783,7 +776,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayArgumentTypeMismatch_Vec3i32_Vec3bool) {
     // array<vec3<i32>, 2u>(vec3<i32>(), vec3<bool>());
-    auto* t = array(ty.vec3<i32>(), 2_u, vec3<i32>(), vec3<bool>());
+    auto* t = Call<array<vec3<i32>, 2>>(Call<vec3<i32>>(), Call<vec3<bool>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -793,7 +786,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayArgumentTypeMismatch_Vec3i32_Vec3bool) {
     // array(vec3<i32>(), vec3<bool>());
-    auto* t = array<Infer>(Source{{12, 34}}, vec3<i32>(), vec3<bool>());
+    auto* t = Call<array<Infer>>(Source{{12, 34}}, Call<vec3<i32>>(), Call<vec3<bool>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -805,7 +798,8 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayOfArray_SubElemSizeMismatch) {
     // array<array<i32, 2u>, 2u>(array<i32, 3u>(), array<i32, 2u>());
-    auto* t = array(Source{{12, 34}}, ty.array<i32, 2>(), 2_i, array<i32, 3>(), array<i32, 2>());
+    auto* t = Call<array<array<i32, 2>, 2>>(Source{{12, 34}}, Call<array<i32, 3>>(),
+                                            Call<array<i32, 2>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -815,7 +809,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayOfArray_SubElemSizeMismatch) {
     // array<array<i32, 2u>, 2u>(array<i32, 3u>(), array<i32, 2u>());
-    auto* t = array<Infer>(Source{{12, 34}}, array<i32, 3>(), array<i32, 2>());
+    auto* t = Call<array<Infer>>(Source{{12, 34}}, Call<array<i32, 3>>(), Call<array<i32, 2>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -827,7 +821,8 @@
 
 TEST_F(ResolverValueConstructorValidationTest, ArrayOfArray_SubElemTypeMismatch) {
     // array<array<i32, 2u>, 2u>(array<i32, 2u>(), array<u32, 2u>());
-    auto* t = array(Source{{12, 34}}, ty.array<i32, 2>(), 2_i, array<i32, 2>(), array<u32, 2>());
+    auto* t = Call<array<array<i32, 2>, 2>>(Source{{12, 34}}, Call<array<i32, 2>>(),
+                                            Call<array<u32, 2>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -837,7 +832,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, InferredArrayOfArray_SubElemTypeMismatch) {
     // array<array<i32, 2u>, 2u>(array<i32, 2u>(), array<u32, 2u>());
-    auto* t = array<Infer>(Source{{12, 34}}, array<i32, 2>(), array<u32, 2>());
+    auto* t = Call<array<Infer>>(Source{{12, 34}}, Call<array<i32, 2>>(), Call<array<u32, 2>>());
     WrapInFunction(t);
 
     EXPECT_FALSE(r()->Resolve());
@@ -850,7 +845,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Array_TooFewElements) {
     // array<i32, 4u>(1i, 2i, 3i);
     SetSource(Source::Location({12, 34}));
-    auto* tc = array<i32, 4>(Expr(1_i), Expr(2_i), Expr(3_i));
+    auto* tc = Call<array<i32, 4>>(Expr(1_i), Expr(2_i), Expr(3_i));
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -861,7 +856,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Array_TooManyElements) {
     // array<i32, 4u>(1i, 2i, 3i, 4i, 5i);
     SetSource(Source::Location({12, 34}));
-    auto* tc = array<i32, 4>(Expr(1_i), Expr(2_i), Expr(3_i), Expr(4_i), Expr(5_i));
+    auto* tc = Call<array<i32, 4>>(Expr(1_i), Expr(2_i), Expr(3_i), Expr(4_i), Expr(5_i));
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -873,7 +868,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, Array_Runtime) {
     // array<i32>(1i);
-    auto* tc = array<i32>(Source{{12, 34}}, Expr(1_i));
+    auto* tc = Call<array<i32>>(Source{{12, 34}}, Expr(1_i));
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -882,7 +877,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, Array_RuntimeZeroValue) {
     // array<i32>();
-    auto* tc = array<i32>(Source{{12, 34}});
+    auto* tc = Call<array<i32>>(Source{{12, 34}});
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -1048,7 +1043,7 @@
 namespace VectorConstructor {
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2F32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, 1_i, 2_f));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, 1_i, 2_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1058,7 +1053,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec2F16_Error_ScalarArgumentTypeMismatch) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec2<f16>(Source{{12, 34}}, 1_h, 2_f));
+    WrapInFunction(Call<vec2<f16>>(Source{{12, 34}}, 1_h, 2_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1066,7 +1061,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2U32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec2<u32>(Source{{12, 34}}, 1_u, 2_i));
+    WrapInFunction(Call<vec2<u32>>(Source{{12, 34}}, 1_u, 2_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1074,7 +1069,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2I32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec2<i32>(Source{{12, 34}}, 1_u, 2_i));
+    WrapInFunction(Call<vec2<i32>>(Source{{12, 34}}, 1_u, 2_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1082,7 +1077,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2Bool_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec2<bool>(Source{{12, 34}}, true, 1_i));
+    WrapInFunction(Call<vec2<bool>>(Source{{12, 34}}, true, 1_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1090,7 +1085,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_Vec3ArgumentCardinalityTooLarge) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, vec3<f32>()));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1098,7 +1093,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_Vec4ArgumentCardinalityTooLarge) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, vec4<f32>()));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, Call<vec4<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1106,7 +1101,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsScalar) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, 1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, 1_f, 2_f, 3_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1114,7 +1109,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsVector) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, vec2<f32>(), vec2<f32>()));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1123,7 +1118,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsVectorAndScalar) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, vec2<f32>(), 1_f));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1131,7 +1126,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_InvalidArgumentType) {
-    WrapInFunction(vec2<f32>(Source{{12, 34}}, mat2x2<f32>()));
+    WrapInFunction(Call<vec2<f32>>(Source{{12, 34}}, Call<mat2x2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1139,7 +1134,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Success_ZeroValue) {
-    auto* tc = vec2<f32>();
+    auto* tc = Call<vec2<f32>>();
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1158,7 +1153,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2F32_Success_Scalar) {
-    auto* tc = vec2<f32>(1_f, 1_f);
+    auto* tc = Call<vec2<f32>>(1_f, 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1181,7 +1176,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec2F16_Success_Scalar) {
     Enable(builtin::Extension::kF16);
 
-    auto* tc = vec2<f16>(1_h, 1_h);
+    auto* tc = Call<vec2<f16>>(1_h, 1_h);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1202,7 +1197,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2U32_Success_Scalar) {
-    auto* tc = vec2<u32>(1_u, 1_u);
+    auto* tc = Call<vec2<u32>>(1_u, 1_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1223,7 +1218,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2I32_Success_Scalar) {
-    auto* tc = vec2<i32>(1_i, 1_i);
+    auto* tc = Call<vec2<i32>>(1_i, 1_i);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1244,7 +1239,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2Bool_Success_Scalar) {
-    auto* tc = vec2<bool>(true, false);
+    auto* tc = Call<vec2<bool>>(true, false);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1265,7 +1260,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Success_Identity) {
-    auto* tc = vec2<f32>(vec2<f32>());
+    auto* tc = Call<vec2<f32>>(Call<vec2<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1285,7 +1280,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Success_Vec2TypeConversion) {
-    auto* tc = vec2<f32>(vec2<i32>());
+    auto* tc = Call<vec2<f32>>(Call<vec2<i32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1305,7 +1300,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3F32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, 1_f, 2_f, 3_i));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, 1_f, 2_f, 3_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1315,7 +1310,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec3F16_Error_ScalarArgumentTypeMismatch) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(Source{{12, 34}}, 1_h, 2_h, 3_f));
+    WrapInFunction(Call<vec3<f16>>(Source{{12, 34}}, 1_h, 2_h, 3_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1323,7 +1318,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3U32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec3<u32>(Source{{12, 34}}, 1_u, 2_i, 3_u));
+    WrapInFunction(Call<vec3<u32>>(Source{{12, 34}}, 1_u, 2_i, 3_u));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1331,7 +1326,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3I32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec3<i32>(Source{{12, 34}}, 1_i, 2_u, 3_i));
+    WrapInFunction(Call<vec3<i32>>(Source{{12, 34}}, 1_i, 2_u, 3_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1339,7 +1334,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3Bool_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec3<bool>(Source{{12, 34}}, false, 1_i, true));
+    WrapInFunction(Call<vec3<bool>>(Source{{12, 34}}, false, 1_i, true));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1347,7 +1342,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_Vec4ArgumentCardinalityTooLarge) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, vec4<f32>()));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<vec4<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1355,7 +1350,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooFewArgumentsScalar) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, 1_f, 2_f));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, 1_f, 2_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1363,7 +1358,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsScalar) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, 1_f, 2_f, 3_f, 4_f));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, 1_f, 2_f, 3_f, 4_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1372,7 +1367,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooFewArgumentsVec2) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, vec2<f32>()));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1380,7 +1375,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec2) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, vec2<f32>(), vec2<f32>()));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1389,7 +1384,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec2AndScalar) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, vec2<f32>(), 1_f, 1_f));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), 1_f, 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1398,7 +1393,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec3) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, vec3<f32>(), 1_f));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<vec3<f32>>(), 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1406,7 +1401,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_InvalidArgumentType) {
-    WrapInFunction(vec3<f32>(Source{{12, 34}}, mat2x2<f32>()));
+    WrapInFunction(Call<vec3<f32>>(Source{{12, 34}}, Call<mat2x2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1414,7 +1409,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_ZeroValue) {
-    auto* tc = vec3<f32>();
+    auto* tc = Call<vec3<f32>>();
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1433,7 +1428,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3F32_Success_Scalar) {
-    auto* tc = vec3<f32>(1_f, 1_f, 1_f);
+    auto* tc = Call<vec3<f32>>(1_f, 1_f, 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1457,7 +1452,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec3F16_Success_Scalar) {
     Enable(builtin::Extension::kF16);
 
-    auto* tc = vec3<f16>(1_h, 1_h, 1_h);
+    auto* tc = Call<vec3<f16>>(1_h, 1_h, 1_h);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1479,7 +1474,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3U32_Success_Scalar) {
-    auto* tc = vec3<u32>(1_u, 1_u, 1_u);
+    auto* tc = Call<vec3<u32>>(1_u, 1_u, 1_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1501,7 +1496,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3I32_Success_Scalar) {
-    auto* tc = vec3<i32>(1_i, 1_i, 1_i);
+    auto* tc = Call<vec3<i32>>(1_i, 1_i, 1_i);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1523,7 +1518,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3Bool_Success_Scalar) {
-    auto* tc = vec3<bool>(true, false, true);
+    auto* tc = Call<vec3<bool>>(true, false, true);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1545,7 +1540,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_Vec2AndScalar) {
-    auto* tc = vec3<f32>(vec2<f32>(), 1_f);
+    auto* tc = Call<vec3<f32>>(Call<vec2<f32>>(), 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1566,7 +1561,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_ScalarAndVec2) {
-    auto* tc = vec3<f32>(1_f, vec2<f32>());
+    auto* tc = Call<vec3<f32>>(1_f, Call<vec2<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1587,7 +1582,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_Identity) {
-    auto* tc = vec3<f32>(vec3<f32>());
+    auto* tc = Call<vec3<f32>>(Call<vec3<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1607,7 +1602,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_Vec3TypeConversion) {
-    auto* tc = vec3<f32>(vec3<i32>());
+    auto* tc = Call<vec3<f32>>(Call<vec3<i32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1627,7 +1622,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4F32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, 1_f, 1_f, 1_i, 1_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, 1_f, 1_f, 1_i, 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1638,7 +1633,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec4F16_Error_ScalarArgumentTypeMismatch) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec4<f16>(Source{{12, 34}}, 1_h, 1_h, 1_f, 1_h));
+    WrapInFunction(Call<vec4<f16>>(Source{{12, 34}}, 1_h, 1_h, 1_f, 1_h));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1647,7 +1642,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4U32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec4<u32>(Source{{12, 34}}, 1_u, 1_u, 1_i, 1_u));
+    WrapInFunction(Call<vec4<u32>>(Source{{12, 34}}, 1_u, 1_u, 1_i, 1_u));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1656,7 +1651,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4I32_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec4<i32>(Source{{12, 34}}, 1_i, 1_i, 1_u, 1_i));
+    WrapInFunction(Call<vec4<i32>>(Source{{12, 34}}, 1_i, 1_i, 1_u, 1_i));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1665,7 +1660,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4Bool_Error_ScalarArgumentTypeMismatch) {
-    WrapInFunction(vec4<bool>(Source{{12, 34}}, true, false, 1_i, true));
+    WrapInFunction(Call<vec4<bool>>(Source{{12, 34}}, true, false, 1_i, true));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1674,7 +1669,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsScalar) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, 1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, 1_f, 2_f, 3_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1682,7 +1677,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsScalar) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, 1_f, 2_f, 3_f, 4_f, 5_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, 1_f, 2_f, 3_f, 4_f, 5_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1691,7 +1686,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsVec2AndScalar) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec2<f32>(), 1_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1699,7 +1694,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2AndScalars) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec2<f32>(), 1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), 1_f, 2_f, 3_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1708,7 +1703,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2Vec2Scalar) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec2<f32>(), vec2<f32>(), 1_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec2<f32>>(), 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1717,7 +1712,8 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2Vec2Vec2) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec2<f32>(), vec2<f32>(), vec2<f32>()));
+    WrapInFunction(
+        Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec2<f32>>(), Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1727,7 +1723,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsVec3) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec3<f32>()));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1735,7 +1731,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndScalars) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec3<f32>(), 1_f, 2_f));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec3<f32>>(), 1_f, 2_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1744,7 +1740,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndVec2) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec3<f32>(), vec2<f32>()));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec3<f32>>(), Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1753,7 +1749,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2AndVec3) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec2<f32>(), vec3<f32>()));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1762,7 +1758,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndVec3) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, vec3<f32>(), vec3<f32>()));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<vec3<f32>>(), Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
@@ -1771,7 +1767,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_InvalidArgumentType) {
-    WrapInFunction(vec4<f32>(Source{{12, 34}}, mat2x2<f32>()));
+    WrapInFunction(Call<vec4<f32>>(Source{{12, 34}}, Call<mat2x2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1779,7 +1775,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_ZeroValue) {
-    auto* tc = vec4<f32>();
+    auto* tc = Call<vec4<f32>>();
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1791,7 +1787,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4F32_Success_Scalar) {
-    auto* tc = vec4<f32>(1_f, 1_f, 1_f, 1_f);
+    auto* tc = Call<vec4<f32>>(1_f, 1_f, 1_f, 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1805,7 +1801,7 @@
 TEST_F(ResolverValueConstructorValidationTest, Vec4F16_Success_Scalar) {
     Enable(builtin::Extension::kF16);
 
-    auto* tc = vec4<f16>(1_h, 1_h, 1_h, 1_h);
+    auto* tc = Call<vec4<f16>>(1_h, 1_h, 1_h, 1_h);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1817,7 +1813,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4U32_Success_Scalar) {
-    auto* tc = vec4<u32>(1_u, 1_u, 1_u, 1_u);
+    auto* tc = Call<vec4<u32>>(1_u, 1_u, 1_u, 1_u);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1829,7 +1825,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4I32_Success_Scalar) {
-    auto* tc = vec4<i32>(1_i, 1_i, 1_i, 1_i);
+    auto* tc = Call<vec4<i32>>(1_i, 1_i, 1_i, 1_i);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1841,7 +1837,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4Bool_Success_Scalar) {
-    auto* tc = vec4<bool>(true, false, true, false);
+    auto* tc = Call<vec4<bool>>(true, false, true, false);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1853,7 +1849,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_Vec2ScalarScalar) {
-    auto* tc = vec4<f32>(vec2<f32>(), 1_f, 1_f);
+    auto* tc = Call<vec4<f32>>(Call<vec2<f32>>(), 1_f, 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1865,7 +1861,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_ScalarVec2Scalar) {
-    auto* tc = vec4<f32>(1_f, vec2<f32>(), 1_f);
+    auto* tc = Call<vec4<f32>>(1_f, Call<vec2<f32>>(), 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1877,7 +1873,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_ScalarScalarVec2) {
-    auto* tc = vec4<f32>(1_f, 1_f, vec2<f32>());
+    auto* tc = Call<vec4<f32>>(1_f, 1_f, Call<vec2<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1889,7 +1885,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_Vec2AndVec2) {
-    auto* tc = vec4<f32>(vec2<f32>(), vec2<f32>());
+    auto* tc = Call<vec4<f32>>(Call<vec2<f32>>(), Call<vec2<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1901,7 +1897,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_Vec3AndScalar) {
-    auto* tc = vec4<f32>(vec3<f32>(), 1_f);
+    auto* tc = Call<vec4<f32>>(Call<vec3<f32>>(), 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1913,7 +1909,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_ScalarAndVec3) {
-    auto* tc = vec4<f32>(1_f, vec3<f32>());
+    auto* tc = Call<vec4<f32>>(1_f, Call<vec3<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1925,7 +1921,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_Identity) {
-    auto* tc = vec4<f32>(vec4<f32>());
+    auto* tc = Call<vec4<f32>>(Call<vec4<f32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1937,7 +1933,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_Vec4TypeConversion) {
-    auto* tc = vec4<f32>(vec4<i32>());
+    auto* tc = Call<vec4<f32>>(Call<vec4<i32>>());
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1949,9 +1945,9 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, NestedVectorConstructors_InnerError) {
-    WrapInFunction(vec4<f32>(vec4<f32>(1_f, 1_f,  //
-                                       vec3<f32>(Source{{12, 34}}, 1_f, 1_f)),
-                             1_f));
+    WrapInFunction(Call<vec4<f32>>(Call<vec4<f32>>(1_f, 1_f,  //
+                                                   Call<vec3<f32>>(Source{{12, 34}}, 1_f, 1_f)),
+                                   1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -1959,7 +1955,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, NestedVectorConstructors_Success) {
-    auto* tc = vec4<f32>(vec3<f32>(vec2<f32>(1_f, 1_f), 1_f), 1_f);
+    auto* tc = Call<vec4<f32>>(Call<vec3<f32>>(Call<vec2<f32>>(1_f, 1_f), 1_f), 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1974,7 +1970,7 @@
     auto* alias = Alias("UnsignedInt", ty.u32());
     GlobalVar("uint_var", ty.Of(alias), builtin::AddressSpace::kPrivate);
 
-    auto* tc = vec2<f32>(Source{{12, 34}}, "uint_var");
+    auto* tc = Call<vec2<f32>>(Source{{12, 34}}, "uint_var");
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -1987,7 +1983,7 @@
     GlobalVar("my_f32", ty.Of(f32_alias), builtin::AddressSpace::kPrivate);
     GlobalVar("my_vec2", ty.Of(vec2_alias), builtin::AddressSpace::kPrivate);
 
-    auto* tc = vec3<f32>("my_vec2", "my_f32");
+    auto* tc = Call<vec3<f32>>("my_vec2", "my_f32");
     WrapInFunction(tc);
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
@@ -2020,7 +2016,7 @@
 
     // vec3<u32>(vec<Float32>(), 1.0f)
     auto vec_type = ty.vec(ty.Of(f32_alias), 2);
-    WrapInFunction(vec3<u32>(Source{{12, 34}}, Call(vec_type), 1_f));
+    WrapInFunction(Call<vec3<u32>>(Source{{12, 34}}, Call(vec_type), 1_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
@@ -2032,7 +2028,7 @@
 
     // vec3<f32>(vec<Float32>(), 1.0f)
     auto vec_type = ty.vec(ty.Of(f32_alias), 2);
-    auto* tc = vec3<f32>(Call(Source{{12, 34}}, vec_type), 1_f);
+    auto* tc = Call<vec3<f32>>(Call(Source{{12, 34}}, vec_type), 1_f);
     WrapInFunction(tc);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2041,11 +2037,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec2ElementTypeFromScalars) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec2_bool = vec2<Infer>(true, false);
-    auto* vec2_i32 = vec2<Infer>(1_i, 2_i);
-    auto* vec2_u32 = vec2<Infer>(1_u, 2_u);
-    auto* vec2_f32 = vec2<Infer>(1_f, 2_f);
-    auto* vec2_f16 = vec2<Infer>(1_h, 2_h);
+    auto* vec2_bool = Call<vec2<Infer>>(true, false);
+    auto* vec2_i32 = Call<vec2<Infer>>(1_i, 2_i);
+    auto* vec2_u32 = Call<vec2<Infer>>(1_u, 2_u);
+    auto* vec2_f32 = Call<vec2<Infer>>(1_f, 2_f);
+    auto* vec2_f16 = Call<vec2<Infer>>(1_h, 2_h);
     WrapInFunction(vec2_bool, vec2_i32, vec2_u32, vec2_f32, vec2_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2070,11 +2066,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec2ElementTypeFromVec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec2_bool = vec2<Infer>(vec2<bool>(true, false));
-    auto* vec2_i32 = vec2<Infer>(vec2<i32>(1_i, 2_i));
-    auto* vec2_u32 = vec2<Infer>(vec2<u32>(1_u, 2_u));
-    auto* vec2_f32 = vec2<Infer>(vec2<f32>(1_f, 2_f));
-    auto* vec2_f16 = vec2<Infer>(vec2<f16>(1_h, 2_h));
+    auto* vec2_bool = Call<vec2<Infer>>(Call<vec2<bool>>(true, false));
+    auto* vec2_i32 = Call<vec2<Infer>>(Call<vec2<i32>>(1_i, 2_i));
+    auto* vec2_u32 = Call<vec2<Infer>>(Call<vec2<u32>>(1_u, 2_u));
+    auto* vec2_f32 = Call<vec2<Infer>>(Call<vec2<f32>>(1_f, 2_f));
+    auto* vec2_f16 = Call<vec2<Infer>>(Call<vec2<f16>>(1_h, 2_h));
     WrapInFunction(vec2_bool, vec2_i32, vec2_u32, vec2_f32, vec2_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2099,11 +2095,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec3ElementTypeFromScalars) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec3_bool = vec3<Infer>(Expr(true), Expr(false), Expr(true));
-    auto* vec3_i32 = vec3<Infer>(Expr(1_i), Expr(2_i), Expr(3_i));
-    auto* vec3_u32 = vec3<Infer>(Expr(1_u), Expr(2_u), Expr(3_u));
-    auto* vec3_f32 = vec3<Infer>(Expr(1_f), Expr(2_f), Expr(3_f));
-    auto* vec3_f16 = vec3<Infer>(Expr(1_h), Expr(2_h), Expr(3_h));
+    auto* vec3_bool = Call<vec3<Infer>>(true, false, true);
+    auto* vec3_i32 = Call<vec3<Infer>>(1_i, 2_i, 3_i);
+    auto* vec3_u32 = Call<vec3<Infer>>(1_u, 2_u, 3_u);
+    auto* vec3_f32 = Call<vec3<Infer>>(1_f, 2_f, 3_f);
+    auto* vec3_f16 = Call<vec3<Infer>>(1_h, 2_h, 3_h);
     WrapInFunction(vec3_bool, vec3_i32, vec3_u32, vec3_f32, vec3_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2128,11 +2124,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec3ElementTypeFromVec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec3_bool = vec3<Infer>(vec3<bool>(true, false, true));
-    auto* vec3_i32 = vec3<Infer>(vec3<i32>(1_i, 2_i, 3_i));
-    auto* vec3_u32 = vec3<Infer>(vec3<u32>(1_u, 2_u, 3_u));
-    auto* vec3_f32 = vec3<Infer>(vec3<f32>(1_f, 2_f, 3_f));
-    auto* vec3_f16 = vec3<Infer>(vec3<f16>(1_h, 2_h, 3_h));
+    auto* vec3_bool = Call<vec3<Infer>>(Call<vec3<bool>>(true, false, true));
+    auto* vec3_i32 = Call<vec3<Infer>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
+    auto* vec3_u32 = Call<vec3<Infer>>(Call<vec3<u32>>(1_u, 2_u, 3_u));
+    auto* vec3_f32 = Call<vec3<Infer>>(Call<vec3<f32>>(1_f, 2_f, 3_f));
+    auto* vec3_f16 = Call<vec3<Infer>>(Call<vec3<f16>>(1_h, 2_h, 3_h));
     WrapInFunction(vec3_bool, vec3_i32, vec3_u32, vec3_f32, vec3_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2157,11 +2153,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec3ElementTypeFromScalarAndVec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec3_bool = vec3<Infer>(Expr(true), vec2<bool>(false, true));
-    auto* vec3_i32 = vec3<Infer>(Expr(1_i), vec2<i32>(2_i, 3_i));
-    auto* vec3_u32 = vec3<Infer>(Expr(1_u), vec2<u32>(2_u, 3_u));
-    auto* vec3_f32 = vec3<Infer>(Expr(1_f), vec2<f32>(2_f, 3_f));
-    auto* vec3_f16 = vec3<Infer>(Expr(1_h), vec2<f16>(2_h, 3_h));
+    auto* vec3_bool = Call<vec3<Infer>>(true, Call<vec2<bool>>(false, true));
+    auto* vec3_i32 = Call<vec3<Infer>>(1_i, Call<vec2<i32>>(2_i, 3_i));
+    auto* vec3_u32 = Call<vec3<Infer>>(1_u, Call<vec2<u32>>(2_u, 3_u));
+    auto* vec3_f32 = Call<vec3<Infer>>(1_f, Call<vec2<f32>>(2_f, 3_f));
+    auto* vec3_f16 = Call<vec3<Infer>>(1_h, Call<vec2<f16>>(2_h, 3_h));
     WrapInFunction(vec3_bool, vec3_i32, vec3_u32, vec3_f32, vec3_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2186,11 +2182,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec4ElementTypeFromScalars) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec4_bool = vec4<Infer>(Expr(true), Expr(false), Expr(true), Expr(false));
-    auto* vec4_i32 = vec4<Infer>(Expr(1_i), Expr(2_i), Expr(3_i), Expr(4_i));
-    auto* vec4_u32 = vec4<Infer>(Expr(1_u), Expr(2_u), Expr(3_u), Expr(4_u));
-    auto* vec4_f32 = vec4<Infer>(Expr(1_f), Expr(2_f), Expr(3_f), Expr(4_f));
-    auto* vec4_f16 = vec4<Infer>(Expr(1_h), Expr(2_h), Expr(3_h), Expr(4_h));
+    auto* vec4_bool = Call<vec4<Infer>>(true, false, true, false);
+    auto* vec4_i32 = Call<vec4<Infer>>(1_i, 2_i, 3_i, 4_i);
+    auto* vec4_u32 = Call<vec4<Infer>>(1_u, 2_u, 3_u, 4_u);
+    auto* vec4_f32 = Call<vec4<Infer>>(1_f, 2_f, 3_f, 4_f);
+    auto* vec4_f16 = Call<vec4<Infer>>(1_h, 2_h, 3_h, 4_h);
     WrapInFunction(vec4_bool, vec4_i32, vec4_u32, vec4_f32, vec4_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2215,11 +2211,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec4ElementTypeFromVec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec4_bool = vec4<Infer>(vec4<bool>(true, false, true, false));
-    auto* vec4_i32 = vec4<Infer>(vec4<i32>(1_i, 2_i, 3_i, 4_i));
-    auto* vec4_u32 = vec4<Infer>(vec4<u32>(1_u, 2_u, 3_u, 4_u));
-    auto* vec4_f32 = vec4<Infer>(vec4<f32>(1_f, 2_f, 3_f, 4_f));
-    auto* vec4_f16 = vec4<Infer>(vec4<f16>(1_h, 2_h, 3_h, 4_h));
+    auto* vec4_bool = Call<vec4<Infer>>(Call<vec4<bool>>(true, false, true, false));
+    auto* vec4_i32 = Call<vec4<Infer>>(Call<vec4<i32>>(1_i, 2_i, 3_i, 4_i));
+    auto* vec4_u32 = Call<vec4<Infer>>(Call<vec4<u32>>(1_u, 2_u, 3_u, 4_u));
+    auto* vec4_f32 = Call<vec4<Infer>>(Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
+    auto* vec4_f16 = Call<vec4<Infer>>(Call<vec4<f16>>(1_h, 2_h, 3_h, 4_h));
     WrapInFunction(vec4_bool, vec4_i32, vec4_u32, vec4_f32, vec4_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2244,11 +2240,11 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec4ElementTypeFromScalarAndVec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec4_bool = vec4<Infer>(Expr(true), vec3<bool>(false, true, false));
-    auto* vec4_i32 = vec4<Infer>(Expr(1_i), vec3<i32>(2_i, 3_i, 4_i));
-    auto* vec4_u32 = vec4<Infer>(Expr(1_u), vec3<u32>(2_u, 3_u, 4_u));
-    auto* vec4_f32 = vec4<Infer>(Expr(1_f), vec3<f32>(2_f, 3_f, 4_f));
-    auto* vec4_f16 = vec4<Infer>(Expr(1_h), vec3<f16>(2_h, 3_h, 4_h));
+    auto* vec4_bool = Call<vec4<Infer>>(true, Call<vec3<bool>>(false, true, false));
+    auto* vec4_i32 = Call<vec4<Infer>>(1_i, Call<vec3<i32>>(2_i, 3_i, 4_i));
+    auto* vec4_u32 = Call<vec4<Infer>>(1_u, Call<vec3<u32>>(2_u, 3_u, 4_u));
+    auto* vec4_f32 = Call<vec4<Infer>>(1_f, Call<vec3<f32>>(2_f, 3_f, 4_f));
+    auto* vec4_f16 = Call<vec4<Infer>>(1_h, Call<vec3<f16>>(2_h, 3_h, 4_h));
     WrapInFunction(vec4_bool, vec4_i32, vec4_u32, vec4_f32, vec4_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2273,11 +2269,12 @@
 TEST_F(ResolverValueConstructorValidationTest, InferVec4ElementTypeFromVec2AndVec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec4_bool = vec4<Infer>(vec2<bool>(true, false), vec2<bool>(true, false));
-    auto* vec4_i32 = vec4<Infer>(vec2<i32>(1_i, 2_i), vec2<i32>(3_i, 4_i));
-    auto* vec4_u32 = vec4<Infer>(vec2<u32>(1_u, 2_u), vec2<u32>(3_u, 4_u));
-    auto* vec4_f32 = vec4<Infer>(vec2<f32>(1_f, 2_f), vec2<f32>(3_f, 4_f));
-    auto* vec4_f16 = vec4<Infer>(vec2<f16>(1_h, 2_h), vec2<f16>(3_h, 4_h));
+    auto* vec4_bool =
+        Call<vec4<Infer>>(Call<vec2<bool>>(true, false), Call<vec2<bool>>(true, false));
+    auto* vec4_i32 = Call<vec4<Infer>>(Call<vec2<i32>>(1_i, 2_i), Call<vec2<i32>>(3_i, 4_i));
+    auto* vec4_u32 = Call<vec4<Infer>>(Call<vec2<u32>>(1_u, 2_u), Call<vec2<u32>>(3_u, 4_u));
+    auto* vec4_f32 = Call<vec4<Infer>>(Call<vec2<f32>>(1_f, 2_f), Call<vec2<f32>>(3_f, 4_f));
+    auto* vec4_f16 = Call<vec4<Infer>>(Call<vec2<f16>>(1_h, 2_h), Call<vec2<f16>>(3_h, 4_h));
     WrapInFunction(vec4_bool, vec4_i32, vec4_u32, vec4_f32, vec4_f16);
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -2300,9 +2297,9 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, InferVecNoArgs) {
-    auto* v2 = vec2<Infer>();
-    auto* v3 = vec3<Infer>();
-    auto* v4 = vec4<Infer>();
+    auto* v2 = Call<vec2<Infer>>();
+    auto* v3 = Call<vec3<Infer>>();
+    auto* v4 = Call<vec4<Infer>>();
 
     GlobalConst("v2", v2);
     GlobalConst("v3", v3);
@@ -2409,8 +2406,8 @@
         DataType<T>::Name,
         DataType<T>::AST,
         DataType<T>::ExprFromDouble,
-        DataType<tint::resolver::builder::vec<R, T>>::AST,
-        DataType<tint::resolver::builder::mat<C, R, T>>::AST,
+        DataType<vec<R, T>>::AST,
+        DataType<mat<C, R, T>>::AST,
     };
 }
 
@@ -2776,7 +2773,7 @@
 
 TEST_F(ResolverValueConstructorValidationTest, MatrixConstructor_ArgumentTypeAlias_Error) {
     auto* alias = Alias("VectorUnsigned2", ty.vec2<u32>());
-    auto* tc = Call(Source{{12, 34}}, ty.mat2x2<f32>(), Call(ty.Of(alias)), vec2<f32>());
+    auto* tc = Call(Source{{12, 34}}, ty.mat2x2<f32>(), Call(ty.Of(alias)), Call<vec2<f32>>());
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
@@ -2973,12 +2970,6 @@
 namespace StructConstructor {
 using builder::CreatePtrs;
 using builder::CreatePtrsFor;
-using builder::mat2x2;
-using builder::mat3x3;
-using builder::mat4x4;
-using builder::vec2;
-using builder::vec3;
-using builder::vec4;
 
 constexpr CreatePtrs all_types[] = {
     CreatePtrsFor<bool>(),         //
@@ -3170,7 +3161,7 @@
 }
 
 TEST_F(ResolverValueConstructorValidationTest, BuilinTypeConstructorAsStatement) {
-    WrapInFunction(CallStmt(vec2<f32>(Source{{12, 34}}, 1_f, 2_f)));
+    WrapInFunction(CallStmt(Call<vec2<f32>>(Source{{12, 34}}, 1_f, 2_f)));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: value constructor evaluated but not used");
diff --git a/src/tint/resolver/variable_test.cc b/src/tint/resolver/variable_test.cc
index 7095cfe..2054885 100644
--- a/src/tint/resolver/variable_test.cc
+++ b/src/tint/resolver/variable_test.cc
@@ -21,7 +21,8 @@
 namespace tint::resolver {
 namespace {
 
-using namespace tint::number_suffixes;  // NOLINT
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 struct ResolverVariableTest : public resolver::TestHelper, public testing::Test {};
 
@@ -421,7 +422,7 @@
     auto* b = Let("b", ty.bool_(), b_c);
     auto* s = Let("s", ty.Of(S), s_c);
     auto* a = Let("a", ty.Of(A), a_c);
-    auto* p = Let("p", ty.ptr<i32>(builtin::AddressSpace::kFunction), p_c);
+    auto* p = Let("p", ty.ptr<function, i32>(), p_c);
 
     Func("F", utils::Empty, ty.void_(),
          utils::Vector{
@@ -898,10 +899,10 @@
     auto* c_i32 = Const("a", ty.i32(), Expr(0_i));
     auto* c_u32 = Const("b", ty.u32(), Expr(0_u));
     auto* c_f32 = Const("c", ty.f32(), Expr(0_f));
-    auto* c_vi32 = Const("d", ty.vec3<i32>(), vec3<i32>());
-    auto* c_vu32 = Const("e", ty.vec3<u32>(), vec3<u32>());
-    auto* c_vf32 = Const("f", ty.vec3<f32>(), vec3<f32>());
-    auto* c_mf32 = Const("g", ty.mat3x3<f32>(), mat3x3<f32>());
+    auto* c_vi32 = Const("d", ty.vec3<i32>(), Call<vec3<i32>>());
+    auto* c_vu32 = Const("e", ty.vec3<u32>(), Call<vec3<u32>>());
+    auto* c_vf32 = Const("f", ty.vec3<f32>(), Call<vec3<f32>>());
+    auto* c_mf32 = Const("g", ty.mat3x3<f32>(), Call<mat3x3<f32>>());
     auto* c_s = Const("h", ty("S"), Call("S"));
 
     WrapInFunction(c_i32, c_u32, c_f32, c_vi32, c_vu32, c_vf32, c_mf32, c_s);
@@ -944,16 +945,16 @@
     auto* c_f32 = Const("c", Expr(0_f));
     auto* c_ai = Const("d", Expr(0_a));
     auto* c_af = Const("e", Expr(0._a));
-    auto* c_vi32 = Const("f", vec3<i32>());
-    auto* c_vu32 = Const("g", vec3<u32>());
-    auto* c_vf32 = Const("h", vec3<f32>());
-    auto* c_vai = Const("i", Call(ty.vec<Infer>(3), Expr(0_a)));
-    auto* c_vaf = Const("j", Call(ty.vec<Infer>(3), Expr(0._a)));
-    auto* c_mf32 = Const("k", mat3x3<f32>());
-    auto* c_maf32 =
-        Const("l", Call(ty.mat3x3<Infer>(),  //
-                        Call(ty.vec<Infer>(3), Expr(0._a)), Call(ty.vec<Infer>(3), Expr(0._a)),
-                        Call(ty.vec<Infer>(3), Expr(0._a))));
+    auto* c_vi32 = Const("f", Call<vec3<i32>>());
+    auto* c_vu32 = Const("g", Call<vec3<u32>>());
+    auto* c_vf32 = Const("h", Call<vec3<f32>>());
+    auto* c_vai = Const("i", Call<vec3<Infer>>(0_a));
+    auto* c_vaf = Const("j", Call<vec3<Infer>>(0._a));
+    auto* c_mf32 = Const("k", Call<mat3x3<f32>>());
+    auto* c_maf32 = Const("l", Call<mat3x3<Infer>>(          //
+                                   Call<vec3<Infer>>(0._a),  //
+                                   Call<vec3<Infer>>(0._a),  //
+                                   Call<vec3<Infer>>(0._a)));
     auto* c_s = Const("m", Call("S"));
 
     WrapInFunction(c_i32, c_u32, c_f32, c_ai, c_af, c_vi32, c_vu32, c_vf32, c_vai, c_vaf, c_mf32,
@@ -1082,10 +1083,10 @@
     auto* c_i32 = GlobalConst("a", ty.i32(), Expr(0_i));
     auto* c_u32 = GlobalConst("b", ty.u32(), Expr(0_u));
     auto* c_f32 = GlobalConst("c", ty.f32(), Expr(0_f));
-    auto* c_vi32 = GlobalConst("d", ty.vec3<i32>(), vec3<i32>());
-    auto* c_vu32 = GlobalConst("e", ty.vec3<u32>(), vec3<u32>());
-    auto* c_vf32 = GlobalConst("f", ty.vec3<f32>(), vec3<f32>());
-    auto* c_mf32 = GlobalConst("g", ty.mat3x3<f32>(), mat3x3<f32>());
+    auto* c_vi32 = GlobalConst("d", ty.vec3<i32>(), Call<vec3<i32>>());
+    auto* c_vu32 = GlobalConst("e", ty.vec3<u32>(), Call<vec3<u32>>());
+    auto* c_vf32 = GlobalConst("f", ty.vec3<f32>(), Call<vec3<f32>>());
+    auto* c_mf32 = GlobalConst("g", ty.mat3x3<f32>(), Call<mat3x3<f32>>());
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
@@ -1120,16 +1121,16 @@
     auto* c_f32 = GlobalConst("c", Expr(0_f));
     auto* c_ai = GlobalConst("d", Expr(0_a));
     auto* c_af = GlobalConst("e", Expr(0._a));
-    auto* c_vi32 = GlobalConst("f", vec3<i32>());
-    auto* c_vu32 = GlobalConst("g", vec3<u32>());
-    auto* c_vf32 = GlobalConst("h", vec3<f32>());
-    auto* c_vai = GlobalConst("i", Call(ty.vec<Infer>(3), Expr(0_a)));
-    auto* c_vaf = GlobalConst("j", Call(ty.vec<Infer>(3), Expr(0._a)));
-    auto* c_mf32 = GlobalConst("k", mat3x3<f32>());
-    auto* c_maf32 = GlobalConst(
-        "l", Call(ty.mat3x3<Infer>(),  //
-                  Call(ty.vec<Infer>(3), Expr(0._a)), Call(ty.vec<Infer>(3), Expr(0._a)),
-                  Call(ty.vec<Infer>(3), Expr(0._a))));
+    auto* c_vi32 = GlobalConst("f", Call<vec3<i32>>());
+    auto* c_vu32 = GlobalConst("g", Call<vec3<u32>>());
+    auto* c_vf32 = GlobalConst("h", Call<vec3<f32>>());
+    auto* c_vai = GlobalConst("i", Call<vec3<Infer>>(0_a));
+    auto* c_vaf = GlobalConst("j", Call<vec3<Infer>>(0._a));
+    auto* c_mf32 = GlobalConst("k", Call<mat3x3<f32>>());
+    auto* c_maf32 = GlobalConst("l", Call<mat3x3<Infer>>(          //
+                                         Call<vec3<Infer>>(0._a),  //
+                                         Call<vec3<Infer>>(0._a),  //
+                                         Call<vec3<Infer>>(0._a)));
 
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 
diff --git a/src/tint/resolver/variable_validation_test.cc b/src/tint/resolver/variable_validation_test.cc
index 9656574..44cc973 100644
--- a/src/tint/resolver/variable_validation_test.cc
+++ b/src/tint/resolver/variable_validation_test.cc
@@ -17,11 +17,12 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/type/texture_dimension.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::resolver {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 struct ResolverVariableValidationTest : public resolver::TestHelper, public testing::Test {};
 
 TEST_F(ResolverVariableValidationTest, VarNoInitializerNoType) {
@@ -132,8 +133,8 @@
     // var i : i32;
     // var p : pointer<function, i32> = &v;
     auto* i = Var("i", ty.i32());
-    auto* p = Var("a", ty.ptr<i32>(Source{{56, 78}}, builtin::AddressSpace::kFunction),
-                  builtin::AddressSpace::kUndefined, AddressOf(Source{{12, 34}}, "i"));
+    auto* p = Var("a", ty.ptr<function, i32>(Source{{56, 78}}), builtin::AddressSpace::kUndefined,
+                  AddressOf(Source{{12, 34}}, "i"));
     WrapInFunction(i, p);
 
     EXPECT_FALSE(r()->Resolve());
@@ -162,7 +163,7 @@
 
 TEST_F(ResolverVariableValidationTest, OverrideInferedTypeNotScalar) {
     // override o = vec3(1.0f);
-    Override(Source{{56, 78}}, "o", vec3<f32>(1.0_f));
+    Override(Source{{56, 78}}, "o", Call<vec3<f32>>(1.0_f));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "56:78 error: vec3<f32> cannot be used as the type of a 'override'");
@@ -314,13 +315,11 @@
     auto* buf = Structure("S", utils::Vector{
                                    Member("inner", ty.Of(inner)),
                                });
-    auto* storage =
+    auto* var =
         GlobalVar("s", ty.Of(buf), builtin::AddressSpace::kStorage, Binding(0_a), Group(0_a));
 
-    auto* expr = IndexAccessor(MemberAccessor(MemberAccessor(storage, "inner"), "arr"), 2_i);
-    auto* ptr = Let(Source{{12, 34}}, "p",
-                    ty.ptr<i32>(builtin::AddressSpace::kStorage, builtin::Access::kReadWrite),
-                    AddressOf(expr));
+    auto* expr = IndexAccessor(MemberAccessor(MemberAccessor(var, "inner"), "arr"), 2_i);
+    auto* ptr = Let(Source{{12, 34}}, "p", ty.ptr<storage, i32, read_write>(), AddressOf(expr));
 
     WrapInFunction(ptr);
 
@@ -390,7 +389,7 @@
 
 TEST_F(ResolverVariableValidationTest, VectorConstNoType) {
     // const a vec3 = vec3<f32>();
-    WrapInFunction(Const("a", ty.vec3<Infer>(Source{{12, 34}}), vec3<f32>()));
+    WrapInFunction(Const("a", ty.vec3<Infer>(Source{{12, 34}}), Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'vec3'");
@@ -398,7 +397,7 @@
 
 TEST_F(ResolverVariableValidationTest, VectorLetNoType) {
     // let a : vec3 = vec3<f32>();
-    WrapInFunction(Let("a", ty.vec3<Infer>(Source{{12, 34}}), vec3<f32>()));
+    WrapInFunction(Let("a", ty.vec3<Infer>(Source{{12, 34}}), Call<vec3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'vec3'");
@@ -414,7 +413,7 @@
 
 TEST_F(ResolverVariableValidationTest, MatrixConstNoType) {
     // const a : mat3x3 = mat3x3<f32>();
-    WrapInFunction(Const("a", ty.mat3x3<Infer>(Source{{12, 34}}), mat3x3<f32>()));
+    WrapInFunction(Const("a", ty.mat3x3<Infer>(Source{{12, 34}}), Call<mat3x3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'mat3x3'");
@@ -422,7 +421,7 @@
 
 TEST_F(ResolverVariableValidationTest, MatrixLetNoType) {
     // let a : mat3x3 = mat3x3<f32>();
-    WrapInFunction(Let("a", ty.mat3x3<Infer>(Source{{12, 34}}), mat3x3<f32>()));
+    WrapInFunction(Let("a", ty.mat3x3<Infer>(Source{{12, 34}}), Call<mat3x3<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'mat3x3'");
diff --git a/src/tint/sem/statement.h b/src/tint/sem/statement.h
index 87214a2..1d9b6cb 100644
--- a/src/tint/sem/statement.h
+++ b/src/tint/sem/statement.h
@@ -88,7 +88,7 @@
     /// pointer to that template argument type, otherwise a CompoundStatement
     /// pointer is returned.
     template <typename... TYPES>
-    const detail::FindFirstParentReturnType<TYPES...>* FindFirstParent() const;
+    const sem::detail::FindFirstParentReturnType<TYPES...>* FindFirstParent() const;
 
     /// @return the closest enclosing block for this statement
     const BlockStatement* Block() const;
@@ -181,8 +181,8 @@
 }
 
 template <typename... TYPES>
-const detail::FindFirstParentReturnType<TYPES...>* Statement::FindFirstParent() const {
-    using ReturnType = detail::FindFirstParentReturnType<TYPES...>;
+const sem::detail::FindFirstParentReturnType<TYPES...>* Statement::FindFirstParent() const {
+    using ReturnType = sem::detail::FindFirstParentReturnType<TYPES...>;
     if (sizeof...(TYPES) == 1) {
         if (auto* p = As<ReturnType>()) {
             return p;
diff --git a/src/tint/switch.h b/src/tint/switch.h
index df5bd95..4073bb8 100644
--- a/src/tint/switch.h
+++ b/src/tint/switch.h
@@ -110,8 +110,8 @@
 template <typename... CASE_RETURN_TYPES>
 struct SwitchReturnTypeImpl</*IS_CASTABLE*/ true, utils::detail::Infer, CASE_RETURN_TYPES...> {
   private:
-    using InferredType = utils::CastableCommonBase<
-        detail::NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>;
+    using InferredType =
+        utils::CastableCommonBase<NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>;
 
   public:
     /// `const T*` or `T*`, where T is the common base type for all the castable case types.
@@ -166,8 +166,9 @@
           typename T = utils::CastableBase,
           typename... CASES>
 inline auto Switch(T* object, CASES&&... cases) {
-    using ReturnType = detail::SwitchReturnType<RETURN_TYPE, utils::traits::ReturnType<CASES>...>;
-    static constexpr int kDefaultIndex = detail::IndexOfDefaultCase<std::tuple<CASES...>>();
+    using ReturnType =
+        tint::detail::SwitchReturnType<RETURN_TYPE, utils::traits::ReturnType<CASES>...>;
+    static constexpr int kDefaultIndex = tint::detail::IndexOfDefaultCase<std::tuple<CASES...>>();
     static constexpr bool kHasDefaultCase = kDefaultIndex >= 0;
     static constexpr bool kHasReturnType = !std::is_same_v<ReturnType, void>;
 
@@ -202,8 +203,8 @@
     struct alignas(alignof(ReturnTypeOrU8)) ReturnStorage {
         uint8_t data[sizeof(ReturnTypeOrU8)];
     };
-    ReturnStorage storage;
-    auto* result = utils::Bitcast<ReturnTypeOrU8*>(&storage);
+    ReturnStorage return_storage;
+    auto* result = utils::Bitcast<ReturnTypeOrU8*>(&return_storage);
 
     const utils::TypeInfo& type_info = object->TypeInfo();
 
@@ -217,7 +218,7 @@
     // `result` pointer.
     auto try_case = [&](auto&& case_fn) {
         using CaseFunc = std::decay_t<decltype(case_fn)>;
-        using CaseType = detail::SwitchCaseType<CaseFunc>;
+        using CaseType = tint::detail::SwitchCaseType<CaseFunc>;
         bool success = false;
         if constexpr (std::is_same_v<CaseType, Default>) {
             if constexpr (kHasReturnType) {
diff --git a/src/tint/transform/manager_test.cc b/src/tint/transform/manager_test.cc
index a41fa87..c1083b2 100644
--- a/src/tint/transform/manager_test.cc
+++ b/src/tint/transform/manager_test.cc
@@ -51,7 +51,7 @@
     void Run(ir::Module* mod, const DataMap&, DataMap&) const override {
         ir::Builder builder(*mod);
         auto* func = builder.Function("ir_func", mod->Types().Get<type::Void>());
-        func->StartTarget()->SetInstructions(utils::Vector{builder.Return(func)});
+        func->StartTarget()->Append(builder.Return(func));
         mod->functions.Push(func);
     }
 };
@@ -68,7 +68,7 @@
     ir::Module mod;
     ir::Builder builder(mod);
     auto* func = builder.Function("main", mod.Types().Get<type::Void>());
-    func->StartTarget()->SetInstructions(utils::Vector{builder.Return(func)});
+    func->StartTarget()->Append(builder.Return(func));
     builder.ir.functions.Push(func);
     return mod;
 }
diff --git a/src/tint/type/manager.cc b/src/tint/type/manager.cc
index f5f382b..89a0c0a 100644
--- a/src/tint/type/manager.cc
+++ b/src/tint/type/manager.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/type/manager.h"
 
+#include <algorithm>
+
 #include "src/tint/type/abstract_float.h"
 #include "src/tint/type/abstract_int.h"
 #include "src/tint/type/array.h"
@@ -158,8 +160,24 @@
 
 const type::Pointer* Manager::ptr(builtin::AddressSpace address_space,
                                   const type::Type* subtype,
-                                  builtin::Access access) {
+                                  builtin::Access access /* = builtin::Access::kReadWrite */) {
     return Get<type::Pointer>(address_space, subtype, access);
 }
 
+const type::Struct* Manager::Struct(Symbol name, utils::VectorRef<StructMemberDesc> md) {
+    utils::Vector<const type::StructMember*, 4> members;
+    uint32_t current_size = 0u;
+    uint32_t max_align = 0u;
+    for (const auto& m : md) {
+        uint32_t index = static_cast<uint32_t>(members.Length());
+        uint32_t offset = utils::RoundUp(m.type->Align(), current_size);
+        members.Push(Get<type::StructMember>(m.name, m.type, index, offset, m.type->Align(),
+                                             m.type->Size(), std::move(m.attributes)));
+        current_size = offset + m.type->Size();
+        max_align = std::max(max_align, m.type->Align());
+    }
+    return Get<type::Struct>(name, members, max_align, utils::RoundUp(max_align, current_size),
+                             current_size);
+}
+
 }  // namespace tint::type
diff --git a/src/tint/type/manager.h b/src/tint/type/manager.h
index a9bdbeb..99ffda3 100644
--- a/src/tint/type/manager.h
+++ b/src/tint/type/manager.h
@@ -19,7 +19,10 @@
 
 #include "src/tint/builtin/access.h"
 #include "src/tint/builtin/address_space.h"
-#include "src/tint/number.h"
+#include "src/tint/builtin/fluent_types.h"
+#include "src/tint/builtin/number.h"
+#include "src/tint/symbol.h"
+#include "src/tint/type/struct.h"
 #include "src/tint/type/type.h"
 #include "src/tint/type/unique_node.h"
 #include "src/tint/utils/hash.h"
@@ -43,9 +46,6 @@
 
 namespace tint::type {
 
-template <typename T>
-struct CppToType;
-
 /// The type manager holds all the pointers to the known types.
 class Manager final {
   public:
@@ -92,10 +92,31 @@
     /// the same pointer is returned.
     template <typename T, typename... ARGS>
     auto* Get(ARGS&&... args) {
-        using N = ToType<T>;
-        if constexpr (utils::traits::IsTypeOrDerived<N, Type>) {
-            return types_.Get<N>(std::forward<ARGS>(args)...);
-        } else if constexpr (utils::traits::IsTypeOrDerived<N, UniqueNode>) {
+        if constexpr (std::is_same_v<T, tint::AInt>) {
+            return Get<type::AbstractInt>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, tint::AFloat>) {
+            return Get<type::AbstractFloat>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, tint::i32>) {
+            return Get<type::I32>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, tint::u32>) {
+            return Get<type::U32>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, tint::f32>) {
+            return Get<type::F32>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, tint::f16>) {
+            return Get<type::F16>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, bool>) {
+            return Get<type::Bool>(std::forward<ARGS>(args)...);
+        } else if constexpr (builtin::fluent_types::IsVector<T>) {
+            return vec<typename T::type, T::width>(std::forward<ARGS>(args)...);
+        } else if constexpr (builtin::fluent_types::IsMatrix<T>) {
+            return mat<T::columns, T::rows, typename T::type>(std::forward<ARGS>(args)...);
+        } else if constexpr (builtin::fluent_types::IsPointer<T>) {
+            return ptr<T::address, typename T::type, T::access>(std::forward<ARGS>(args)...);
+        } else if constexpr (builtin::fluent_types::IsArray<T>) {
+            return array<typename T::type, T::length>(std::forward<ARGS>(args)...);
+        } else if constexpr (utils::traits::IsTypeOrDerived<T, Type>) {
+            return types_.Get<T>(std::forward<ARGS>(args)...);
+        } else if constexpr (utils::traits::IsTypeOrDerived<T, UniqueNode>) {
             return unique_nodes_.Get<T>(std::forward<ARGS>(args)...);
         } else {
             return nodes_.Create<T>(std::forward<ARGS>(args)...);
@@ -158,6 +179,7 @@
     /// @returns the vector type
     template <typename T, size_t N>
     const type::Vector* vec() {
+        TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
         static_assert(N >= 2 && N <= 4);
         switch (N) {
             case 2:
@@ -167,6 +189,8 @@
             case 4:
                 return vec4<T>();
         }
+        return nullptr;  // unreachable
+        TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
     }
 
     /// @tparam T the element type
@@ -295,6 +319,24 @@
         return mat4x4(Get<T>());
     }
 
+    /// @param columns the number of columns of the matrix
+    /// @param rows the number of rows of the matrix
+    /// @tparam T the element type
+    /// @returns a matrix with the given number of columns and rows
+    template <typename T>
+    const type::Matrix* mat(uint32_t columns, uint32_t rows) {
+        return mat(Get<T>(), columns, rows);
+    }
+
+    /// @tparam C the number of columns in the matrix
+    /// @tparam R the number of rows in the matrix
+    /// @tparam T the element type
+    /// @returns a matrix with the given number of columns and rows
+    template <uint32_t C, uint32_t R, typename T>
+    const type::Matrix* mat() {
+        return mat(Get<T>(), C, R);
+    }
+
     /// @param elem_ty the array element type
     /// @param count the array element count
     /// @param stride the optional array element stride
@@ -325,7 +367,7 @@
     /// @returns the pointer type
     const type::Pointer* ptr(builtin::AddressSpace address_space,
                              const type::Type* subtype,
-                             builtin::Access access);
+                             builtin::Access access = builtin::Access::kReadWrite);
 
     /// @tparam SPACE the address space
     /// @tparam T the storage type
@@ -338,6 +380,39 @@
         return ptr(SPACE, Get<T>(), ACCESS);
     }
 
+    /// @param subtype the pointer subtype
+    /// @tparam SPACE the address space
+    /// @tparam ACCESS the access mode
+    /// @returns the pointer type with the templated address space, storage type and access.
+    template <builtin::AddressSpace SPACE, builtin::Access ACCESS = builtin::Access::kReadWrite>
+    const type::Pointer* ptr(const type::Type* subtype) {
+        return ptr(SPACE, subtype, ACCESS);
+    }
+
+    /// A structure member descriptor.
+    struct StructMemberDesc {
+        /// The name of the struct member.
+        Symbol name;
+        /// The type of the struct member.
+        const type::Type* type = nullptr;
+        /// The optional struct member attributes.
+        type::StructMemberAttributes attributes = {};
+    };
+
+    /// Create a new structure declaration.
+    /// @param name the name of the structure
+    /// @param members the list of structure member descriptors
+    /// @returns the structure type
+    const type::Struct* Struct(Symbol name, utils::VectorRef<StructMemberDesc> members);
+
+    /// Create a new structure declaration.
+    /// @param name the name of the structure
+    /// @param members the list of structure member descriptors
+    /// @returns the structure type
+    const type::Struct* Struct(Symbol name, std::initializer_list<StructMemberDesc> members) {
+        return Struct(name, utils::Vector<StructMemberDesc, 4>(members));
+    }
+
     /// @returns an iterator to the beginning of the types
     TypeIterator begin() const { return types_.begin(); }
     /// @returns an iterator to the end of the types
@@ -362,46 +437,6 @@
     utils::BlockAllocator<Node> nodes_;
 };
 
-//! @cond Doxygen_Suppress
-// Various template specializations for Manager::ToTypeImpl.
-template <>
-struct Manager::ToTypeImpl<AInt> {
-    using type = type::AbstractInt;
-};
-template <>
-struct Manager::ToTypeImpl<AFloat> {
-    using type = type::AbstractFloat;
-};
-template <>
-struct Manager::ToTypeImpl<i32> {
-    using type = type::I32;
-};
-template <>
-struct Manager::ToTypeImpl<u32> {
-    using type = type::U32;
-};
-template <>
-struct Manager::ToTypeImpl<f32> {
-    using type = type::F32;
-};
-template <>
-struct Manager::ToTypeImpl<f16> {
-    using type = type::F16;
-};
-template <>
-struct Manager::ToTypeImpl<bool> {
-    using type = type::Bool;
-};
-template <typename T>
-struct Manager::ToTypeImpl<const T> {
-    using type = const Manager::ToType<T>;
-};
-template <typename T>
-struct Manager::ToTypeImpl<T*> {
-    using type = Manager::ToType<T>*;
-};
-//! @endcond
-
 }  // namespace tint::type
 
 #endif  // SRC_TINT_TYPE_MANAGER_H_
diff --git a/src/tint/type/struct.h b/src/tint/type/struct.h
index 720beca..907485c 100644
--- a/src/tint/type/struct.h
+++ b/src/tint/type/struct.h
@@ -185,6 +185,8 @@
 struct StructMemberAttributes {
     /// The value of a `@location` attribute
     std::optional<uint32_t> location;
+    /// The value of a `@index` attribute
+    std::optional<uint32_t> index;
     /// The value of a `@builtin` attribute
     std::optional<builtin::BuiltinValue> builtin;
     /// The values of a `@interpolate` attribute
diff --git a/src/tint/utils/castable.cc b/src/tint/utils/castable.cc
index b7756b3..484014f 100644
--- a/src/tint/utils/castable.cc
+++ b/src/tint/utils/castable.cc
@@ -19,7 +19,7 @@
 /// The unique TypeInfo for the CastableBase type
 /// @return doxygen-thinks-this-static-field-is-a-function :(
 template <>
-const TypeInfo detail::TypeInfoOf<CastableBase>::info{
+const TypeInfo utils::detail::TypeInfoOf<CastableBase>::info{
     nullptr,
     "CastableBase",
     tint::utils::TypeInfo::HashCodeOf<CastableBase>(),
diff --git a/src/tint/utils/castable.h b/src/tint/utils/castable.h
index f402c1c..176b107 100644
--- a/src/tint/utils/castable.h
+++ b/src/tint/utils/castable.h
@@ -176,7 +176,7 @@
     /// @returns the static TypeInfo for the type T
     template <typename T>
     static const TypeInfo& Of() {
-        return detail::TypeInfoOf<std::remove_cv_t<T>>::info;
+        return utils::detail::TypeInfoOf<std::remove_cv_t<T>>::info;
     }
 
     /// @returns a compile-time hashcode for the type `T`.
@@ -281,7 +281,7 @@
 /// @returns true if `obj` is a valid pointer, and is of, or derives from the class `TO`
 /// @param obj the object to test from
 /// @see CastFlags
-template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+template <typename TO, int FLAGS = 0, typename FROM = utils::detail::Infer>
 inline bool Is(FROM* obj) {
     if (obj == nullptr) {
         return false;
@@ -295,7 +295,10 @@
 /// @param pred predicate function with signature `bool(const TYPE*)` called iff object is of, or
 /// derives from the class `TYPE`.
 /// @see CastFlags
-template <typename TYPE, int FLAGS = 0, typename OBJ = detail::Infer, typename Pred = detail::Infer>
+template <typename TYPE,
+          int FLAGS = 0,
+          typename OBJ = utils::detail::Infer,
+          typename Pred = utils::detail::Infer>
 inline bool Is(OBJ* obj, Pred&& pred) {
     return Is<TYPE, FLAGS, OBJ>(obj) && pred(static_cast<std::add_const_t<TYPE>*>(obj));
 }
@@ -315,7 +318,7 @@
 /// `TO`.
 /// @param obj the object to cast from
 /// @see CastFlags
-template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+template <typename TO, int FLAGS = 0, typename FROM = utils::detail::Infer>
 inline TO* As(FROM* obj) {
     auto* as_castable = static_cast<CastableBase*>(obj);
     return Is<TO, FLAGS>(obj) ? static_cast<TO*>(as_castable) : nullptr;
@@ -325,7 +328,7 @@
 /// `TO`.
 /// @param obj the object to cast from
 /// @see CastFlags
-template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+template <typename TO, int FLAGS = 0, typename FROM = utils::detail::Infer>
 inline const TO* As(const FROM* obj) {
     auto* as_castable = static_cast<const CastableBase*>(obj);
     return Is<TO, FLAGS>(obj) ? static_cast<const TO*>(as_castable) : nullptr;
@@ -361,7 +364,7 @@
     /// returns true
     /// @param pred predicate function with signature `bool(const TO*)` called iff object is of, or
     /// derives from the class `TO`.
-    template <typename TO, int FLAGS = 0, typename Pred = detail::Infer>
+    template <typename TO, int FLAGS = 0, typename Pred = utils::detail::Infer>
     inline bool Is(Pred&& pred) const {
         return tint::utils::Is<TO, FLAGS>(this, std::forward<Pred>(pred));
     }
@@ -445,7 +448,7 @@
     /// pred(const TO*) returns true
     /// @param pred predicate function with signature `bool(const TO*)` called iff
     /// object is of, or derives from the class `TO`.
-    template <int FLAGS = 0, typename Pred = detail::Infer>
+    template <int FLAGS = 0, typename Pred = utils::detail::Infer>
     inline bool Is(Pred&& pred) const {
         using TO = typename std::remove_pointer<utils::traits::ParameterType<Pred, 0>>::type;
         return tint::utils::Is<TO, FLAGS>(static_cast<const CLASS*>(this),
@@ -534,7 +537,7 @@
 
 /// Resolves to the common most derived type that each of the types in `TYPES` derives from.
 template <typename... TYPES>
-using CastableCommonBase = detail::CastableCommonBase<TYPES...>;
+using CastableCommonBase = utils::detail::CastableCommonBase<TYPES...>;
 
 }  // namespace tint::utils
 
diff --git a/src/tint/utils/hash.h b/src/tint/utils/hash.h
index fe1614e..f02c777 100644
--- a/src/tint/utils/hash.h
+++ b/src/tint/utils/hash.h
@@ -209,7 +209,7 @@
 ///          The returned hash is dependent on the order of the arguments.
 template <typename... ARGS>
 size_t HashCombine(size_t hash, const ARGS&... values) {
-    constexpr size_t offset = detail::HashCombineOffset<sizeof(size_t)>::value();
+    constexpr size_t offset = utils::detail::HashCombineOffset<sizeof(size_t)>::value();
     ((hash ^= Hash(values) + (offset ^ (hash >> 2))), ...);
     return hash;
 }
diff --git a/src/tint/utils/hashset.h b/src/tint/utils/hashset.h
index 2d427a1..5339565 100644
--- a/src/tint/utils/hashset.h
+++ b/src/tint/utils/hashset.h
@@ -35,6 +35,17 @@
     using PutMode = typename Base::PutMode;
 
   public:
+    using Base::Base;
+
+    /// Constructor with initializer list of items
+    /// @param items the items to place into the set
+    Hashset(std::initializer_list<KEY> items) {
+        this->Reserve(items.size());
+        for (auto item : items) {
+            this->Add(item);
+        }
+    }
+
     /// Adds a value to the set, if the set does not already contain an entry equal to `value`.
     /// @param value the value to add to the set.
     /// @returns true if the value was added, false if there was an existing value in the set.
@@ -55,6 +66,30 @@
         }
         return out;
     }
+
+    /// @returns true if the predicate function returns true for any of the elements of the set
+    /// @param pred a function-like with the signature `bool(T)`
+    template <typename PREDICATE>
+    bool Any(PREDICATE&& pred) const {
+        for (const auto& it : *this) {
+            if (pred(it)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /// @returns false if the predicate function returns false for any of the elements of the set
+    /// @param pred a function-like with the signature `bool(T)`
+    template <typename PREDICATE>
+    bool All(PREDICATE&& pred) const {
+        for (const auto& it : *this) {
+            if (!pred(it)) {
+                return false;
+            }
+        }
+        return true;
+    }
 };
 
 }  // namespace tint::utils
diff --git a/src/tint/utils/hashset_test.cc b/src/tint/utils/hashset_test.cc
index c541fb5..31a025a 100644
--- a/src/tint/utils/hashset_test.cc
+++ b/src/tint/utils/hashset_test.cc
@@ -21,6 +21,7 @@
 #include <unordered_set>
 
 #include "gmock/gmock.h"
+#include "src/tint/utils/predicates.h"
 
 namespace tint::utils {
 namespace {
@@ -38,6 +39,16 @@
     EXPECT_EQ(set.Count(), 0u);
 }
 
+TEST(Hashset, InitializerConstructor) {
+    Hashset<int, 8> set{1, 5, 7};
+    EXPECT_EQ(set.Count(), 3u);
+    EXPECT_TRUE(set.Contains(1u));
+    EXPECT_FALSE(set.Contains(3u));
+    EXPECT_TRUE(set.Contains(5u));
+    EXPECT_TRUE(set.Contains(7u));
+    EXPECT_FALSE(set.Contains(9u));
+}
+
 TEST(Hashset, AddRemove) {
     Hashset<std::string, 8> set;
     EXPECT_TRUE(set.Add("hello"));
@@ -140,5 +151,31 @@
     }
 }
 
+TEST(HashsetTest, Any) {
+    Hashset<int, 8> set{1, 7, 5, 9};
+    EXPECT_TRUE(set.Any(Eq(1)));
+    EXPECT_FALSE(set.Any(Eq(2)));
+    EXPECT_FALSE(set.Any(Eq(3)));
+    EXPECT_FALSE(set.Any(Eq(4)));
+    EXPECT_TRUE(set.Any(Eq(5)));
+    EXPECT_FALSE(set.Any(Eq(6)));
+    EXPECT_TRUE(set.Any(Eq(7)));
+    EXPECT_FALSE(set.Any(Eq(8)));
+    EXPECT_TRUE(set.Any(Eq(9)));
+}
+
+TEST(HashsetTest, All) {
+    Hashset<int, 8> set{1, 7, 5, 9};
+    EXPECT_FALSE(set.All(Ne(1)));
+    EXPECT_TRUE(set.All(Ne(2)));
+    EXPECT_TRUE(set.All(Ne(3)));
+    EXPECT_TRUE(set.All(Ne(4)));
+    EXPECT_FALSE(set.All(Ne(5)));
+    EXPECT_TRUE(set.All(Ne(6)));
+    EXPECT_FALSE(set.All(Ne(7)));
+    EXPECT_TRUE(set.All(Ne(8)));
+    EXPECT_FALSE(set.All(Ne(9)));
+}
+
 }  // namespace
 }  // namespace tint::utils
diff --git a/src/tint/utils/slice.h b/src/tint/utils/slice.h
index ca25949..49483ec 100644
--- a/src/tint/utils/slice.h
+++ b/src/tint/utils/slice.h
@@ -94,7 +94,8 @@
 ///  * `FROM` and `TO` are pointers to CastableBase (or derived), and the pointee type of `TO` is of
 ///     the same type as, or is an ancestor of the pointee type of `FROM`.
 template <ReinterpretMode MODE, typename TO, typename FROM>
-static constexpr bool CanReinterpretSlice = detail::CanReinterpretSlice<MODE, TO, FROM>::value;
+static constexpr bool CanReinterpretSlice =
+    utils::detail::CanReinterpretSlice<MODE, TO, FROM>::value;
 
 /// A slice represents a contigious array of elements of type T.
 template <typename T>
diff --git a/src/tint/utils/traits.h b/src/tint/utils/traits.h
index 711ea36..f37b073 100644
--- a/src/tint/utils/traits.h
+++ b/src/tint/utils/traits.h
@@ -148,14 +148,14 @@
 /// `[OFFSET..OFFSET+COUNT)`
 template <std::size_t OFFSET, std::size_t COUNT, typename TUPLE>
 constexpr auto Slice(TUPLE&& t) {
-    return detail::Swizzle<TUPLE>(std::forward<TUPLE>(t), Range<OFFSET, COUNT>());
+    return traits::detail::Swizzle<TUPLE>(std::forward<TUPLE>(t), Range<OFFSET, COUNT>());
 }
 
 /// Resolves to the slice of the tuple `t` with the tuple elements
 /// `[OFFSET..OFFSET+COUNT)`
 template <std::size_t OFFSET, std::size_t COUNT, typename TUPLE>
 using SliceTuple =
-    std::remove_pointer_t<decltype(detail::SwizzlePtrTy<TUPLE>(Range<OFFSET, COUNT>()))>;
+    std::remove_pointer_t<decltype(traits::detail::SwizzlePtrTy<TUPLE>(Range<OFFSET, COUNT>()))>;
 
 namespace detail {
 /// Base template for IsTypeIn
@@ -171,7 +171,7 @@
 /// Works for std::variant, std::tuple, std::pair, or any typename template where all parameters are
 /// types.
 template <typename T, typename TypeContainer>
-static constexpr bool IsTypeIn = detail::IsTypeIn<T, TypeContainer>::value;
+static constexpr bool IsTypeIn = traits::detail::IsTypeIn<T, TypeContainer>::value;
 
 /// Evaluates to the decayed pointer element type, or the decayed type T if T is not a pointer.
 template <typename T>
@@ -207,7 +207,7 @@
 /// Evaluates to `char*` or `const char*` if `T` is `char[N]` or `const char[N]`, respectively,
 /// otherwise T.
 template <typename T>
-using CharArrayToCharPtr = typename detail::CharArrayToCharPtrImpl<T>::type;
+using CharArrayToCharPtr = typename traits::detail::CharArrayToCharPtrImpl<T>::type;
 
 }  // namespace tint::utils::traits
 
diff --git a/src/tint/utils/vector.h b/src/tint/utils/vector.h
index 8b8fc0f..78cddbd 100644
--- a/src/tint/utils/vector.h
+++ b/src/tint/utils/vector.h
@@ -571,7 +571,7 @@
 /// Helper for determining the Vector element type (`T`) from the vector's constuctor arguments
 template <typename... Ts>
 using VectorCommonType =
-    typename detail::VectorCommonType<IsCastable<std::remove_pointer_t<Ts>...>, Ts...>::type;
+    typename utils::detail::VectorCommonType<IsCastable<std::remove_pointer_t<Ts>...>, Ts...>::type;
 
 /// Deduction guide for Vector
 template <typename... Ts>
@@ -792,7 +792,7 @@
 
 /// True if T is a Vector<T, N> or VectorRef<T>
 template <typename T>
-static constexpr bool IsVectorLike = detail::IsVectorLike<T>::value;
+static constexpr bool IsVectorLike = utils::detail::IsVectorLike<T>::value;
 
 }  // namespace tint::utils
 
diff --git a/src/tint/writer/append_vector_test.cc b/src/tint/writer/append_vector_test.cc
index ff58ba3..b9e7857 100644
--- a/src/tint/writer/append_vector_test.cc
+++ b/src/tint/writer/append_vector_test.cc
@@ -20,11 +20,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 class AppendVectorTest : public ::testing::Test, public ProgramBuilder {};
 
 // AppendVector(vec2<i32>(1, 2), 3) -> vec3<i32>(1, 2, 3)
@@ -32,7 +33,7 @@
     auto* scalar_1 = Expr(1_i);
     auto* scalar_2 = Expr(2_i);
     auto* scalar_3 = Expr(3_i);
-    auto* vec_12 = vec2<i32>(scalar_1, scalar_2);
+    auto* vec_12 = Call<vec2<i32>>(scalar_1, scalar_2);
     WrapInFunction(vec_12, scalar_3);
 
     resolver::Resolver resolver(this);
@@ -73,7 +74,7 @@
     auto* scalar_1 = Expr(1_i);
     auto* scalar_2 = Expr(2_i);
     auto* scalar_3 = Expr(3_u);
-    auto* vec_12 = vec2<i32>(scalar_1, scalar_2);
+    auto* vec_12 = Call<vec2<i32>>(scalar_1, scalar_2);
     WrapInFunction(vec_12, scalar_3);
 
     resolver::Resolver resolver(this);
@@ -120,8 +121,8 @@
     auto* scalar_1 = Expr(1_u);
     auto* scalar_2 = Expr(2_u);
     auto* scalar_3 = Expr(3_u);
-    auto* uvec_12 = vec2<u32>(scalar_1, scalar_2);
-    auto* vec_12 = vec2<i32>(uvec_12);
+    auto* uvec_12 = Call<vec2<u32>>(scalar_1, scalar_2);
+    auto* vec_12 = Call<vec2<i32>>(uvec_12);
     WrapInFunction(vec_12, scalar_3);
 
     resolver::Resolver resolver(this);
@@ -170,7 +171,7 @@
     auto* scalar_1 = Expr(1_i);
     auto* scalar_2 = Expr(2_i);
     auto* scalar_3 = Expr(3_f);
-    auto* vec_12 = vec2<i32>(scalar_1, scalar_2);
+    auto* vec_12 = Call<vec2<i32>>(scalar_1, scalar_2);
     WrapInFunction(vec_12, scalar_3);
 
     resolver::Resolver resolver(this);
@@ -216,7 +217,7 @@
     auto* scalar_2 = Expr(2_i);
     auto* scalar_3 = Expr(3_i);
     auto* scalar_4 = Expr(4_i);
-    auto* vec_123 = vec3<i32>(scalar_1, scalar_2, scalar_3);
+    auto* vec_123 = Call<vec3<i32>>(scalar_1, scalar_2, scalar_3);
     WrapInFunction(vec_123, scalar_4);
 
     resolver::Resolver resolver(this);
@@ -298,7 +299,7 @@
     auto* scalar_1 = Expr(1_i);
     auto* scalar_2 = Expr(2_i);
     auto* scalar_3 = Expr("scalar_3");
-    auto* vec_12 = vec2<i32>(scalar_1, scalar_2);
+    auto* vec_12 = Call<vec2<i32>>(scalar_1, scalar_2);
     WrapInFunction(vec_12, scalar_3);
 
     resolver::Resolver resolver(this);
@@ -455,7 +456,7 @@
 // AppendVector(vec3<i32>(), 4) -> vec3<bool>(0, 0, 0, 4)
 TEST_F(AppendVectorTest, ZeroVec3i32_i32) {
     auto* scalar = Expr(4_i);
-    auto* vec000 = vec3<i32>();
+    auto* vec000 = Call<vec3<i32>>();
     WrapInFunction(vec000, scalar);
 
     resolver::Resolver resolver(this);
diff --git a/src/tint/writer/glsl/generator_impl_binary_test.cc b/src/tint/writer/glsl/generator_impl_binary_test.cc
index 9be3fa4..7ca2b09 100644
--- a/src/tint/writer/glsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/glsl/generator_impl_binary_test.cc
@@ -19,11 +19,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_Binary = TestHelper;
 
 struct BinaryData {
@@ -160,7 +161,7 @@
                     BinaryData{"(left % right)", ast::BinaryOp::kModulo}));
 
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_VectorScalar_f32) {
-    GlobalVar("a", vec3<f32>(1_f, 1_f, 1_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("a", Call<vec3<f32>>(1_f, 1_f, 1_f), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("a");
     auto* rhs = Expr(1_f);
 
@@ -179,7 +180,7 @@
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_VectorScalar_f16) {
     Enable(builtin::Extension::kF16);
 
-    GlobalVar("a", vec3<f16>(1_h, 1_h, 1_h), builtin::AddressSpace::kPrivate);
+    GlobalVar("a", Call<vec3<f16>>(1_h, 1_h, 1_h), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("a");
     auto* rhs = Expr(1_h);
 
@@ -196,7 +197,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_ScalarVector_f32) {
-    GlobalVar("a", vec3<f32>(1_f, 1_f, 1_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("a", Call<vec3<f32>>(1_f, 1_f, 1_f), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr(1_f);
     auto* rhs = Expr("a");
 
@@ -215,7 +216,7 @@
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_ScalarVector_f16) {
     Enable(builtin::Extension::kF16);
 
-    GlobalVar("a", vec3<f16>(1_h, 1_h, 1_h), builtin::AddressSpace::kPrivate);
+    GlobalVar("a", Call<vec3<f16>>(1_h, 1_h, 1_h), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr(1_h);
     auto* rhs = Expr("a");
 
@@ -302,7 +303,7 @@
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_MatrixVector_f32) {
     GlobalVar("mat", ty.mat3x3<f32>(), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("mat");
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
     WrapInFunction(expr);
@@ -320,7 +321,7 @@
 
     GlobalVar("mat", ty.mat3x3<f16>(), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("mat");
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
     WrapInFunction(expr);
@@ -335,7 +336,7 @@
 
 TEST_F(GlslGeneratorImplTest_Binary, Multiply_VectorMatrix_f32) {
     GlobalVar("mat", ty.mat3x3<f32>(), builtin::AddressSpace::kPrivate);
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* rhs = Expr("mat");
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -353,7 +354,7 @@
     Enable(builtin::Extension::kF16);
 
     GlobalVar("mat", ty.mat3x3<f16>(), builtin::AddressSpace::kPrivate);
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
     auto* rhs = Expr("mat");
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
diff --git a/src/tint/writer/glsl/generator_impl_builtin_test.cc b/src/tint/writer/glsl/generator_impl_builtin_test.cc
index e7d1a61..78765ef 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_test.cc
@@ -19,13 +19,13 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_Builtin = TestHelper;
 
 enum class CallParamType {
@@ -371,9 +371,9 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, Select_Vector) {
-    GlobalVar("a", vec2<i32>(1_i, 2_i), builtin::AddressSpace::kPrivate);
-    GlobalVar("b", vec2<i32>(3_i, 4_i), builtin::AddressSpace::kPrivate);
-    auto* call = Call("select", "a", "b", vec2<bool>(true, false));
+    GlobalVar("a", Call<vec2<i32>>(1_i, 2_i), builtin::AddressSpace::kPrivate);
+    GlobalVar("b", Call<vec2<i32>>(3_i, 4_i), builtin::AddressSpace::kPrivate);
+    auto* call = Call("select", "a", "b", Call<vec2<bool>>(true, false));
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
@@ -495,7 +495,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, Runtime_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("f", vec3<f32>(1.5_f, 2.5_f, 3.5_f))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -532,7 +532,7 @@
 TEST_F(GlslGeneratorImplTest_Builtin, Runtime_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("f", vec3<f16>(1.5_h, 2.5_h, 3.5_h))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -625,7 +625,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, Const_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f32>(1.5_f, 2.5_f, 3.5_f)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -654,7 +654,7 @@
 TEST_F(GlslGeneratorImplTest_Builtin, Const_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f16>(1.5_h, 2.5_h, 3.5_h)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -755,7 +755,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, Runtime_Frexp_Vector_f32) {
-    WrapInFunction(Var("f", Expr(vec3<f32>())),  //
+    WrapInFunction(Var("f", Call<vec3<f32>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -792,7 +792,7 @@
 TEST_F(GlslGeneratorImplTest_Builtin, Runtime_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Var("f", Expr(vec3<f16>())),  //
+    WrapInFunction(Var("f", Call<vec3<f16>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -885,7 +885,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, Const_Frexp_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f32>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f32>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -914,7 +914,7 @@
 TEST_F(GlslGeneratorImplTest_Builtin, Const_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f16>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f16>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -1576,7 +1576,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, QuantizeToF16_Vec2) {
-    GlobalVar("v", vec2<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec2<f32>>(2_f), builtin::AddressSpace::kPrivate);
     WrapInFunction(Call("quantizeToF16", "v"));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -1604,7 +1604,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, QuantizeToF16_Vec3) {
-    GlobalVar("v", vec3<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec3<f32>>(2_f), builtin::AddressSpace::kPrivate);
     WrapInFunction(Call("quantizeToF16", "v"));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -1634,7 +1634,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Builtin, QuantizeToF16_Vec4) {
-    GlobalVar("v", vec4<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec4<f32>>(2_f), builtin::AddressSpace::kPrivate);
     WrapInFunction(Call("quantizeToF16", "v"));
 
     GeneratorImpl& gen = SanitizeAndBuild();
diff --git a/src/tint/writer/glsl/generator_impl_cast_test.cc b/src/tint/writer/glsl/generator_impl_cast_test.cc
index 4773a4b..870e46e 100644
--- a/src/tint/writer/glsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/glsl/generator_impl_cast_test.cc
@@ -17,11 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_Cast = TestHelper;
 
 TEST_F(GlslGeneratorImplTest_Cast, EmitExpression_Cast_Scalar) {
@@ -37,7 +38,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Cast, EmitExpression_Cast_Vector) {
-    auto* cast = vec3<f32>(vec3<i32>(1_i, 2_i, 3_i));
+    auto* cast = Call<vec3<f32>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(cast);
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/glsl/generator_impl_constructor_test.cc b/src/tint/writer/glsl/generator_impl_constructor_test.cc
index 2983382..8a18dc7 100644
--- a/src/tint/writer/glsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_constructor_test.cc
@@ -15,12 +15,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using GlslGeneratorImplTest_Constructor = TestHelper;
 
@@ -121,7 +121,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_F32) {
-    WrapInFunction(vec3<f32>(1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec3<f32>>(1_f, 2_f, 3_f));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -132,7 +132,7 @@
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(1_h, 2_h, 3_h));
+    WrapInFunction(Call<vec3<f16>>(1_h, 2_h, 3_h));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -141,7 +141,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_Empty_F32) {
-    WrapInFunction(vec3<f32>());
+    WrapInFunction(Call<vec3<f32>>());
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -152,7 +152,7 @@
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>());
+    WrapInFunction(Call<vec3<f16>>());
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -161,7 +161,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Literal) {
-    WrapInFunction(vec3<f32>(2_f));
+    WrapInFunction(Call<vec3<f32>>(2_f));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -172,7 +172,7 @@
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F16_Literal) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(2_h));
+    WrapInFunction(Call<vec3<f16>>(2_h));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -182,7 +182,7 @@
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Var) {
     auto* var = Var("v", Expr(2_f));
-    auto* cast = vec3<f32>(var);
+    auto* cast = Call<vec3<f32>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -196,7 +196,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Var("v", Expr(2_h));
-    auto* cast = vec3<f16>(var);
+    auto* cast = Call<vec3<f16>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -207,7 +207,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Bool) {
-    WrapInFunction(vec3<bool>(true));
+    WrapInFunction(Call<vec3<bool>>(true));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -216,7 +216,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Int) {
-    WrapInFunction(vec3<i32>(2_i));
+    WrapInFunction(Call<vec3<i32>>(2_i));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -225,7 +225,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_UInt) {
-    WrapInFunction(vec3<u32>(2_u));
+    WrapInFunction(Call<vec3<u32>>(2_u));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -234,7 +234,8 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Mat_F32) {
-    WrapInFunction(mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(3_f, 4_f, 5_f)));
+    WrapInFunction(
+        Call<mat2x3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(3_f, 4_f, 5_f)));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -245,7 +246,8 @@
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Mat_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>(vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(3_h, 4_h, 5_h)));
+    WrapInFunction(
+        Call<mat2x3<f16>>(Call<vec3<f16>>(1_h, 2_h, 3_h), Call<vec3<f16>>(3_h, 4_h, 5_h)));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -262,14 +264,14 @@
     //     vec4<f32>(vec4<f32>(42.0f, 21.0f, 6.0f, -5.0f)),
     //   );
     auto* vector_literal =
-        vec4<f32>(Expr(f32(2.0)), Expr(f32(3.0)), Expr(f32(4.0)), Expr(f32(8.0)));
-    auto* vector_zero_init = vec4<f32>();
-    auto* vector_single_scalar_init = vec4<f32>(Expr(f32(7.0)));
-    auto* vector_identical_init =
-        vec4<f32>(vec4<f32>(Expr(f32(42.0)), Expr(f32(21.0)), Expr(f32(6.0)), Expr(f32(-5.0))));
+        Call<vec4<f32>>(Expr(f32(2.0)), Expr(f32(3.0)), Expr(f32(4.0)), Expr(f32(8.0)));
+    auto* vector_zero_init = Call<vec4<f32>>();
+    auto* vector_single_scalar_init = Call<vec4<f32>>(Expr(f32(7.0)));
+    auto* vector_identical_init = Call<vec4<f32>>(
+        Call<vec4<f32>>(Expr(f32(42.0)), Expr(f32(21.0)), Expr(f32(6.0)), Expr(f32(-5.0))));
 
-    auto* ctor = mat4x4<f32>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                             vector_identical_init);
+    auto* ctor = Call<mat4x4<f32>>(vector_literal, vector_zero_init, vector_single_scalar_init,
+                                   vector_identical_init);
 
     WrapInFunction(ctor);
 
@@ -290,14 +292,14 @@
     Enable(builtin::Extension::kF16);
 
     auto* vector_literal =
-        vec4<f16>(Expr(f16(2.0)), Expr(f16(3.0)), Expr(f16(4.0)), Expr(f16(8.0)));
-    auto* vector_zero_init = vec4<f16>();
-    auto* vector_single_scalar_init = vec4<f16>(Expr(f16(7.0)));
-    auto* vector_identical_init =
-        vec4<f16>(vec4<f16>(Expr(f16(42.0)), Expr(f16(21.0)), Expr(f16(6.0)), Expr(f16(-5.0))));
+        Call<vec4<f16>>(Expr(f16(2.0)), Expr(f16(3.0)), Expr(f16(4.0)), Expr(f16(8.0)));
+    auto* vector_zero_init = Call<vec4<f16>>();
+    auto* vector_single_scalar_init = Call<vec4<f16>>(Expr(f16(7.0)));
+    auto* vector_identical_init = Call<vec4<f16>>(
+        Call<vec4<f16>>(Expr(f16(42.0)), Expr(f16(21.0)), Expr(f16(6.0)), Expr(f16(-5.0))));
 
-    auto* ctor = mat4x4<f16>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                             vector_identical_init);
+    auto* ctor = Call<mat4x4<f16>>(vector_literal, vector_zero_init, vector_single_scalar_init,
+                                   vector_identical_init);
 
     WrapInFunction(ctor);
 
@@ -310,7 +312,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Mat_Empty_F32) {
-    WrapInFunction(mat2x3<f32>());
+    WrapInFunction(Call<mat2x3<f32>>());
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -321,7 +323,7 @@
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Mat_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>());
+    WrapInFunction(Call<mat2x3<f16>>());
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -336,8 +338,8 @@
     //     var m_2: mat4x4<f32> = mat4x4<f32>(m_1);
     // }
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), mat4x4<f32>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), mat4x4<f32>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -355,8 +357,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), mat4x4<f16>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), mat4x4<f16>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -367,8 +369,9 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Array) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u), vec3<f32>(1_f, 2_f, 3_f),
-                        vec3<f32>(4_f, 5_f, 6_f), vec3<f32>(7_f, 8_f, 9_f)));
+    WrapInFunction(Call<array<vec3<f32>, 3>>(Call<vec3<f32>>(1_f, 2_f, 3_f),
+                                             Call<vec3<f32>>(4_f, 5_f, 6_f),
+                                             Call<vec3<f32>>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -379,7 +382,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Array_Empty) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u)));
+    WrapInFunction(Call<array<vec3<f32>, 3>>());
 
     GeneratorImpl& gen = Build();
     gen.Generate();
@@ -394,7 +397,7 @@
                                    Member("c", ty.vec3<i32>()),
                                });
 
-    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, vec3<i32>(3_i, 4_i, 5_i)));
+    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, Call<vec3<i32>>(3_i, 4_i, 5_i)));
 
     GeneratorImpl& gen = SanitizeAndBuild();
     gen.Generate();
diff --git a/src/tint/writer/glsl/generator_impl_function_test.cc b/src/tint/writer/glsl/generator_impl_function_test.cc
index 34375f7..43f8773 100644
--- a/src/tint/writer/glsl/generator_impl_function_test.cc
+++ b/src/tint/writer/glsl/generator_impl_function_test.cc
@@ -20,11 +20,12 @@
 
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_Function = TestHelper;
 
 TEST_F(GlslGeneratorImplTest_Function, Emit_Function) {
@@ -110,7 +111,7 @@
     // fn f(foo : ptr<function, f32>) -> f32 {
     //   return *foo;
     // }
-    Func("f", utils::Vector{Param("foo", ty.ptr<f32>(builtin::AddressSpace::kFunction))}, ty.f32(),
+    Func("f", utils::Vector{Param("foo", ty.ptr<function, f32>())}, ty.f32(),
          utils::Vector{Return(Deref("foo"))});
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -207,7 +208,7 @@
     //   @location(2) col2 : f32;
     // };
     // fn vert_main() -> Interface {
-    //   return Interface(vec4<f32>(), 0.4, 0.6);
+    //   return Interface(vec4<f32>(), 0.5, 0.25);
     // }
     // fn frag_main(inputs : Interface) {
     //   const r = inputs.col1;
@@ -223,8 +224,7 @@
         });
 
     Func("vert_main", utils::Empty, ty.Of(interface_struct),
-         utils::Vector{Return(
-             Call(ty.Of(interface_struct), Call(ty.vec4<f32>()), Expr(0.5_f), Expr(0.25_f)))},
+         utils::Vector{Return(Call(ty.Of(interface_struct), Call<vec4<f32>>(), 0.5_f, 0.25_f))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)});
 
     Func("frag_main", utils::Vector{Param("inputs", ty.Of(interface_struct))}, ty.void_(),
@@ -301,7 +301,7 @@
 
   Func("foo", utils::Vector{Param("x", ty.f32())}, ty.Of(vertex_output_struct),
        {Return(Call(ty.Of(vertex_output_struct),
-                         Call(ty.vec4<f32>(), "x", "x", "x", Expr(1_f))))},
+                         Call<vec4<f32>>( "x", "x", "x", 1_f)))},
        {});
 
   Func("vert_main1", utils::Empty, ty.Of(vertex_output_struct),
@@ -845,7 +845,7 @@
 TEST_F(GlslGeneratorImplTest_Function, Emit_Function_WithArrayReturn) {
     Func("my_func", utils::Empty, ty.array<f32, 5>(),
          utils::Vector{
-             Return(Call(ty.array<f32, 5>())),
+             Return(Call<array<f32, 5>>()),
          });
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/glsl/generator_impl_import_test.cc b/src/tint/writer/glsl/generator_impl_import_test.cc
index 6f772ea..0f27e4b 100644
--- a/src/tint/writer/glsl/generator_impl_import_test.cc
+++ b/src/tint/writer/glsl/generator_impl_import_test.cc
@@ -17,11 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_Import = TestHelper;
 
 struct GlslImportData {
@@ -95,7 +96,7 @@
 TEST_P(GlslImportData_SingleVectorParamTest, FloatVector) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(0.1_f, 0.2_f, 0.3_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(0.1_f, 0.2_f, 0.3_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -162,7 +163,7 @@
 TEST_P(GlslImportData_DualParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -227,8 +228,8 @@
 TEST_P(GlslImportData_TripleParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f),
-                      vec3<f32>(7_f, 8_f, 9_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f),
+                      Call<vec3<f32>>(7_f, 8_f, 9_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/glsl/generator_impl_member_accessor_test.cc b/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
index 041e72c..7f252b1 100644
--- a/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
@@ -17,12 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using create_type_func_ptr = ast::Type (*)(const ProgramBuilder::TypesBuilder& ty);
 
@@ -275,7 +275,7 @@
     });
 
     SetupFunction(utils::Vector{
-        Assign(MemberAccessor("data", "b"), Call(ty.mat2x3<f32>())),
+        Assign(MemberAccessor("data", "b"), Call<mat2x3<f32>>()),
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -769,7 +769,7 @@
 
     SetupFunction(utils::Vector{
         Assign(MemberAccessor(IndexAccessor(MemberAccessor("data", "c"), 2_i), "b"),
-               vec3<f32>(1_f, 2_f, 3_f)),
+               Call<vec3<f32>>(1_f, 2_f, 3_f)),
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -868,7 +868,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_MemberAccessor, Swizzle_xyz) {
-    auto* var = Var("my_vec", ty.vec4<f32>(), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+    auto* var = Var("my_vec", ty.vec4<f32>(), Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
     auto* expr = MemberAccessor("my_vec", "xyz");
     WrapInFunction(var, expr);
 
@@ -879,7 +879,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_MemberAccessor, Swizzle_gbr) {
-    auto* var = Var("my_vec", ty.vec4<f32>(), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+    auto* var = Var("my_vec", ty.vec4<f32>(), Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
     auto* expr = MemberAccessor("my_vec", "gbr");
     WrapInFunction(var, expr);
 
diff --git a/src/tint/writer/glsl/generator_impl_module_constant_test.cc b/src/tint/writer/glsl/generator_impl_module_constant_test.cc
index 8de0dd0..9a80721 100644
--- a/src/tint/writer/glsl/generator_impl_module_constant_test.cc
+++ b/src/tint/writer/glsl/generator_impl_module_constant_test.cc
@@ -17,15 +17,16 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslGeneratorImplTest_ModuleConstant = TestHelper;
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalLet) {
-    auto* var = Let("pos", ty.array<f32, 3>(), array<f32, 3>(1_f, 2_f, 3_f));
+    auto* var = Let("pos", ty.array<f32, 3>(), Call<array<f32, 3>>(1_f, 2_f, 3_f));
     WrapInFunction(Decl(var));
 
     GeneratorImpl& gen = Build();
@@ -152,7 +153,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_AInt) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -171,7 +172,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -190,7 +191,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_f32) {
-    auto* var = GlobalConst("G", vec3<f32>(1_f, 2_f, 3_f));
+    auto* var = GlobalConst("G", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -211,7 +212,7 @@
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", vec3<f16>(1_h, 2_h, 3_h));
+    auto* var = GlobalConst("G", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -231,7 +232,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* var = GlobalConst("G", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -250,7 +251,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_f32) {
-    auto* var = GlobalConst("G", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* var = GlobalConst("G", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -271,7 +272,7 @@
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* var = GlobalConst("G", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -291,7 +292,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_arr_f32) {
-    auto* var = GlobalConst("G", Call(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
+    auto* var = GlobalConst("G", Call<array<f32, 3>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
@@ -310,10 +311,10 @@
 }
 
 TEST_F(GlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_arr_vec2_bool) {
-    auto* var = GlobalConst("G", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                                      vec2<bool>(true, false),         //
-                                      vec2<bool>(false, true),         //
-                                      vec2<bool>(true, true)));
+    auto* var = GlobalConst("G", Call<array<vec2<bool>, 3>>(         //
+                                     Call<vec2<bool>>(true, false),  //
+                                     Call<vec2<bool>>(false, true),  //
+                                     Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", Expr(var))),
diff --git a/src/tint/writer/glsl/generator_impl_sanitizer_test.cc b/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
index fdd216f..ca60d36 100644
--- a/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
+++ b/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
@@ -19,11 +19,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using GlslSanitizerTest = TestHelper;
 
 TEST_F(GlslSanitizerTest, Call_ArrayLength) {
@@ -149,7 +150,7 @@
 }
 
 TEST_F(GlslSanitizerTest, PromoteArrayInitializerToConstVar) {
-    auto* array_init = array<i32, 4>(1_i, 2_i, 3_i, 4_i);
+    auto* array_init = Call<array<i32, 4>>(1_i, 2_i, 3_i, 4_i);
 
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -189,7 +190,7 @@
                                    Member("c", ty.i32()),
                                });
     auto* runtime_value = Var("runtime_value", Expr(3_f));
-    auto* struct_init = Call(ty.Of(str), 1_i, vec3<f32>(2_f, runtime_value, 4_f), 4_i);
+    auto* struct_init = Call(ty.Of(str), 1_i, Call<vec3<f32>>(2_f, runtime_value, 4_f), 4_i);
     auto* struct_access = MemberAccessor(struct_init, "b");
     auto* pos = Var("pos", ty.vec3<f32>(), struct_access);
 
@@ -235,7 +236,7 @@
     // let p : ptr<function, i32> = &v;
     // let x : i32 = *p;
     auto* v = Var("v", ty.i32());
-    auto* p = Let("p", ty.ptr<i32>(builtin::AddressSpace::kFunction), AddressOf(v));
+    auto* p = Let("p", ty.ptr<function, i32>(), AddressOf(v));
     auto* x = Var("x", ty.i32(), Deref(p));
 
     Func("main", utils::Empty, ty.void_(),
@@ -276,12 +277,9 @@
     // let vp : ptr<function, vec4<f32>> = &(*mp)[2i];
     // let v : vec4<f32> = *vp;
     auto* a = Var("a", ty.array(ty.mat4x4<f32>(), 4_u));
-    auto* ap = Let("ap", ty.ptr(builtin::AddressSpace::kFunction, ty.array(ty.mat4x4<f32>(), 4_u)),
-                   AddressOf(a));
-    auto* mp = Let("mp", ty.ptr(builtin::AddressSpace::kFunction, ty.mat4x4<f32>()),
-                   AddressOf(IndexAccessor(Deref(ap), 3_i)));
-    auto* vp = Let("vp", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()),
-                   AddressOf(IndexAccessor(Deref(mp), 2_i)));
+    auto* ap = Let("ap", ty.ptr<function, array<mat4x4<f32>, 4>>(), AddressOf(a));
+    auto* mp = Let("mp", ty.ptr<function, mat4x4<f32>>(), AddressOf(IndexAccessor(Deref(ap), 3_i)));
+    auto* vp = Let("vp", ty.ptr<function, vec4<f32>>(), AddressOf(IndexAccessor(Deref(mp), 2_i)));
     auto* v = Var("v", ty.vec4<f32>(), Deref(vp));
 
     Func("main", utils::Empty, ty.void_(),
diff --git a/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc b/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc
index 821b0e9..8ebe654 100644
--- a/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc
+++ b/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/number.h"
+#include "src/tint/builtin/number.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 #include "gmock/gmock.h"
diff --git a/src/tint/writer/glsl/generator_impl_variable_decl_statement_test.cc b/src/tint/writer/glsl/generator_impl_variable_decl_statement_test.cc
index 80a1fa0..d4171c5 100644
--- a/src/tint/writer/glsl/generator_impl_variable_decl_statement_test.cc
+++ b/src/tint/writer/glsl/generator_impl_variable_decl_statement_test.cc
@@ -17,12 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::glsl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using GlslGeneratorImplTest_VariableDecl = TestHelper;
 
@@ -186,7 +186,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_AInt) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* C = Const("C", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -206,7 +206,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_AFloat) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* C = Const("C", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -226,7 +226,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_f32) {
-    auto* C = Const("C", vec3<f32>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -248,7 +248,7 @@
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", vec3<f16>(1_h, 2_h, 3_h));
+    auto* C = Const("C", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -269,7 +269,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_AFloat) {
-    auto* C = Const("C", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* C = Const("C", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -289,7 +289,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_f32) {
-    auto* C = Const("C", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* C = Const("C", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -311,7 +311,7 @@
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* C = Const("C", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -332,7 +332,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_arr_f32) {
-    auto* C = Const("C", Call(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<array<f32, 3>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -352,7 +352,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_arr_f32_zero) {
-    auto* C = Const("C", Call(ty.array<f32, 2>()));
+    auto* C = Const("C", Call<array<f32, 2>>());
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -372,7 +372,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_arr_arr_f32_zero) {
-    auto* C = Const("C", Call(ty.array(ty.array<f32, 2>(), 3_i)));
+    auto* C = Const("C", Call<array<array<f32, 2>, 3>>());
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -418,10 +418,10 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_arr_vec2_bool) {
-    auto* C = Const("C", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                              vec2<bool>(true, false),         //
-                              vec2<bool>(false, true),         //
-                              vec2<bool>(true, true)));
+    auto* C = Const("C", Call<array<vec2<bool>, 3>>(         //
+                             Call<vec2<bool>>(true, false),  //
+                             Call<vec2<bool>>(false, true),  //
+                             Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -466,7 +466,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroVec_f32) {
-    auto* var = Var("a", ty.vec3<f32>(), vec3<f32>());
+    auto* var = Var("a", ty.vec3<f32>(), Call<vec3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -481,7 +481,7 @@
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroVec_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.vec3<f16>(), vec3<f16>());
+    auto* var = Var("a", ty.vec3<f16>(), Call<vec3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -494,7 +494,7 @@
 }
 
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroMat_f32) {
-    auto* var = Var("a", ty.mat2x3<f32>(), mat2x3<f32>());
+    auto* var = Var("a", ty.mat2x3<f32>(), Call<mat2x3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -510,7 +510,7 @@
 TEST_F(GlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroMat_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.mat2x3<f16>(), mat2x3<f16>());
+    auto* var = Var("a", ty.mat2x3<f16>(), Call<mat2x3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index def946a..1267bea 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -194,9 +194,16 @@
         manager.Add<ast::transform::Robustness>();
 
         ast::transform::Robustness::Config config = {};
+
         config.bindings_ignored = std::unordered_set<sem::BindingPoint>(
             options.binding_points_ignored_in_robustness_transform.cbegin(),
             options.binding_points_ignored_in_robustness_transform.cend());
+
+        // Direct3D guarantees to return zero for any resource that is accessed out of bounds, and
+        // according to the description of the assembly store_uav_typed, out of bounds addressing
+        // means nothing gets written to memory.
+        config.texture_action = ast::transform::Robustness::Action::kIgnore;
+
         data.Add<ast::transform::Robustness::Config>(config);
     }
 
diff --git a/src/tint/writer/hlsl/generator_impl_binary_test.cc b/src/tint/writer/hlsl/generator_impl_binary_test.cc
index 61a4ee1..5269dca 100644
--- a/src/tint/writer/hlsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_binary_test.cc
@@ -17,11 +17,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_Binary = TestHelper;
 
 struct BinaryData {
@@ -176,7 +177,7 @@
                                BinaryData::Types::Float}));
 
 TEST_F(HlslGeneratorImplTest_Binary, Multiply_VectorScalar_f32) {
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* rhs = Expr(1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -193,7 +194,7 @@
 TEST_F(HlslGeneratorImplTest_Binary, Multiply_VectorScalar_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
     auto* rhs = Expr(1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -209,7 +210,7 @@
 
 TEST_F(HlslGeneratorImplTest_Binary, Multiply_ScalarVector_f32) {
     auto* lhs = Expr(1_f);
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
 
@@ -226,7 +227,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* lhs = Expr(1_h);
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
 
@@ -306,7 +307,7 @@
 TEST_F(HlslGeneratorImplTest_Binary, Multiply_MatrixVector_f32) {
     GlobalVar("mat", ty.mat3x3<f32>(), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("mat");
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
     WrapInFunction(expr);
@@ -323,7 +324,7 @@
 
     GlobalVar("mat", ty.mat3x3<f16>(), builtin::AddressSpace::kPrivate);
     auto* lhs = Expr("mat");
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
     WrapInFunction(expr);
@@ -337,7 +338,7 @@
 
 TEST_F(HlslGeneratorImplTest_Binary, Multiply_VectorMatrix_f32) {
     GlobalVar("mat", ty.mat3x3<f32>(), builtin::AddressSpace::kPrivate);
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* rhs = Expr("mat");
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -354,7 +355,7 @@
     Enable(builtin::Extension::kF16);
 
     GlobalVar("mat", ty.mat3x3<f16>(), builtin::AddressSpace::kPrivate);
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
     auto* rhs = Expr("mat");
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
diff --git a/src/tint/writer/hlsl/generator_impl_builtin_test.cc b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
index 92406a6..16dfcea 100644
--- a/src/tint/writer/hlsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
@@ -19,13 +19,13 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_Builtin = TestHelper;
 
 enum class CallParamType {
@@ -368,9 +368,9 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Select_Vector) {
-    GlobalVar("a", vec2<i32>(1_i, 2_i), builtin::AddressSpace::kPrivate);
-    GlobalVar("b", vec2<i32>(3_i, 4_i), builtin::AddressSpace::kPrivate);
-    auto* call = Call("select", "a", "b", vec2<bool>(true, false));
+    GlobalVar("a", Call<vec2<i32>>(1_i, 2_i), builtin::AddressSpace::kPrivate);
+    GlobalVar("b", Call<vec2<i32>>(3_i, 4_i), builtin::AddressSpace::kPrivate);
+    auto* call = Call("select", "a", "b", Call<vec2<bool>>(true, false));
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
@@ -435,7 +435,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Runtime_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("f", vec3<f32>(1.5_f, 2.5_f, 3.5_f))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -463,7 +463,7 @@
 TEST_F(HlslGeneratorImplTest_Builtin, Runtime_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("f", vec3<f16>(1.5_h, 2.5_h, 3.5_h))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -527,7 +527,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Const_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f32>(1.5_f, 2.5_f, 3.5_f)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -547,7 +547,7 @@
 TEST_F(HlslGeneratorImplTest_Builtin, Const_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f16>(1.5_h, 2.5_h, 3.5_h)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -568,10 +568,10 @@
     WrapInFunction(
         // Declare a variable with the result of a modf call.
         // This is required to infer the 'var' type.
-        Decl(Var("v", Call("modf", vec3<f32>(1.5_f, 2.5_f, 3.5_f)))),
+        Decl(Var("v", Call("modf", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f)))),
         // Now assign 'v' again with another modf call.
         // This requires generating a temporary variable for the struct initializer.
-        Assign("v", Call("modf", vec3<f32>(4.5_a, 5.5_a, 6.5_a))));
+        Assign("v", Call("modf", Call<vec3<f32>>(4.5_a, 5.5_a, 6.5_a))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -647,7 +647,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Runtime_Frexp_Vector_f32) {
-    WrapInFunction(Var("f", Expr(vec3<f32>())),  //
+    WrapInFunction(Var("f", Call<vec3<f32>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -676,7 +676,7 @@
 TEST_F(HlslGeneratorImplTest_Builtin, Runtime_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Var("f", Expr(vec3<f16>())),  //
+    WrapInFunction(Var("f", Call<vec3<f16>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -741,7 +741,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Const_Frexp_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f32>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f32>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -761,7 +761,7 @@
 TEST_F(HlslGeneratorImplTest_Builtin, Const_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f16>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f16>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -782,10 +782,10 @@
     WrapInFunction(
         // Declare a variable with the result of a frexp call.
         // This is required to infer the 'var' type.
-        Decl(Var("v", Call("frexp", vec3<f32>(1.5_f, 2.5_f, 3.5_f)))),
+        Decl(Var("v", Call("frexp", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f)))),
         // Now assign 'v' again with another frexp call.
         // This requires generating a temporary variable for the struct initializer.
-        Assign("v", Call("frexp", vec3<f32>(4.5_a, 5.5_a, 6.5_a))));
+        Assign("v", Call("frexp", Call<vec3<f32>>(4.5_a, 5.5_a, 6.5_a))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
diff --git a/src/tint/writer/hlsl/generator_impl_cast_test.cc b/src/tint/writer/hlsl/generator_impl_cast_test.cc
index 73bc170..6484974 100644
--- a/src/tint/writer/hlsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_cast_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_Cast = TestHelper;
 
 TEST_F(HlslGeneratorImplTest_Cast, EmitExpression_Cast_Scalar) {
@@ -34,7 +35,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Cast, EmitExpression_Cast_Vector) {
-    auto* cast = vec3<f32>(vec3<i32>(1_i, 2_i, 3_i));
+    auto* cast = Call<vec3<f32>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(cast);
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/hlsl/generator_impl_constructor_test.cc b/src/tint/writer/hlsl/generator_impl_constructor_test.cc
index f9c41ea..bdb05ba 100644
--- a/src/tint/writer/hlsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_constructor_test.cc
@@ -15,12 +15,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using HlslGeneratorImplTest_Constructor = TestHelper;
 
@@ -121,7 +121,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_F32) {
-    WrapInFunction(vec3<f32>(1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec3<f32>>(1_f, 2_f, 3_f));
 
     GeneratorImpl& gen = Build();
 
@@ -132,7 +132,7 @@
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(1_h, 2_h, 3_h));
+    WrapInFunction(Call<vec3<f16>>(1_h, 2_h, 3_h));
 
     GeneratorImpl& gen = Build();
 
@@ -143,7 +143,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_Empty_F32) {
-    WrapInFunction(vec3<f32>());
+    WrapInFunction(Call<vec3<f32>>());
 
     GeneratorImpl& gen = Build();
 
@@ -154,7 +154,7 @@
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>());
+    WrapInFunction(Call<vec3<f16>>());
 
     GeneratorImpl& gen = Build();
 
@@ -163,7 +163,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Literal) {
-    WrapInFunction(vec3<f32>(2_f));
+    WrapInFunction(Call<vec3<f32>>(2_f));
 
     GeneratorImpl& gen = Build();
 
@@ -174,7 +174,7 @@
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F16_Literal) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(2_h));
+    WrapInFunction(Call<vec3<f16>>(2_h));
 
     GeneratorImpl& gen = Build();
 
@@ -184,7 +184,7 @@
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Var) {
     auto* var = Var("v", Expr(2_f));
-    auto* cast = vec3<f32>(var);
+    auto* cast = Call<vec3<f32>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -198,7 +198,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Var("v", Expr(2_h));
-    auto* cast = vec3<f16>(var);
+    auto* cast = Call<vec3<f16>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -209,7 +209,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Bool_Literal) {
-    WrapInFunction(vec3<bool>(true));
+    WrapInFunction(Call<vec3<bool>>(true));
 
     GeneratorImpl& gen = Build();
 
@@ -219,7 +219,7 @@
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Bool_Var) {
     auto* var = Var("v", Expr(true));
-    auto* cast = vec3<bool>(var);
+    auto* cast = Call<vec3<bool>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -230,7 +230,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Int) {
-    WrapInFunction(vec3<i32>(2_i));
+    WrapInFunction(Call<vec3<i32>>(2_i));
 
     GeneratorImpl& gen = Build();
 
@@ -239,7 +239,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_UInt) {
-    WrapInFunction(vec3<u32>(2_u));
+    WrapInFunction(Call<vec3<u32>>(2_u));
 
     GeneratorImpl& gen = Build();
 
@@ -248,7 +248,8 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Mat_F32) {
-    WrapInFunction(mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(3_f, 4_f, 5_f)));
+    WrapInFunction(
+        Call<mat2x3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(3_f, 4_f, 5_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -261,7 +262,8 @@
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Mat_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>(vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(3_h, 4_h, 5_h)));
+    WrapInFunction(
+        Call<mat2x3<f16>>(Call<vec3<f16>>(1_h, 2_h, 3_h), Call<vec3<f16>>(3_h, 4_h, 5_h)));
 
     GeneratorImpl& gen = Build();
 
@@ -280,15 +282,14 @@
     //     vec4<f32>(7.0f),
     //     vec4<f32>(vec4<f32>(42.0f, 21.0f, 6.0f, -5.0f)),
     //   );
-    auto* vector_literal =
-        vec4<f32>(Expr(f32(2.0)), Expr(f32(3.0)), Expr(f32(4.0)), Expr(f32(8.0)));
-    auto* vector_zero_init = vec4<f32>();
-    auto* vector_single_scalar_init = vec4<f32>(Expr(f32(7.0)));
+    auto* vector_literal = Call<vec4<f32>>(f32(2.0), f32(3.0), f32(4.0), f32(8.0));
+    auto* vector_zero_init = Call<vec4<f32>>();
+    auto* vector_single_scalar_init = Call<vec4<f32>>(f32(7.0));
     auto* vector_identical_init =
-        vec4<f32>(vec4<f32>(Expr(f32(42.0)), Expr(f32(21.0)), Expr(f32(6.0)), Expr(f32(-5.0))));
+        Call<vec4<f32>>(Call<vec4<f32>>(f32(42.0), f32(21.0), f32(6.0), f32(-5.0)));
 
-    auto* constructor = mat4x4<f32>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                                    vector_identical_init);
+    auto* constructor = Call<mat4x4<f32>>(vector_literal, vector_zero_init,
+                                          vector_single_scalar_init, vector_identical_init);
 
     WrapInFunction(constructor);
 
@@ -309,15 +310,14 @@
     //   );
     Enable(builtin::Extension::kF16);
 
-    auto* vector_literal =
-        vec4<f16>(Expr(f16(2.0)), Expr(f16(3.0)), Expr(f16(4.0)), Expr(f16(8.0)));
-    auto* vector_zero_init = vec4<f16>();
-    auto* vector_single_scalar_init = vec4<f16>(Expr(f16(7.0)));
+    auto* vector_literal = Call<vec4<f16>>(f16(2.0), f16(3.0), f16(4.0), f16(8.0));
+    auto* vector_zero_init = Call<vec4<f16>>();
+    auto* vector_single_scalar_init = Call<vec4<f16>>(f16(7.0));
     auto* vector_identical_init =
-        vec4<f16>(vec4<f16>(Expr(f16(42.0)), Expr(f16(21.0)), Expr(f16(6.0)), Expr(f16(-5.0))));
+        Call<vec4<f16>>(Call<vec4<f16>>(f16(42.0), f16(21.0), f16(6.0), f16(-5.0)));
 
-    auto* constructor = mat4x4<f16>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                                    vector_identical_init);
+    auto* constructor = Call<mat4x4<f16>>(vector_literal, vector_zero_init,
+                                          vector_single_scalar_init, vector_identical_init);
 
     WrapInFunction(constructor);
 
@@ -334,7 +334,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Mat_Empty_F32) {
-    WrapInFunction(mat2x3<f32>());
+    WrapInFunction(Call<mat2x3<f32>>());
 
     GeneratorImpl& gen = Build();
 
@@ -346,7 +346,7 @@
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Mat_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>());
+    WrapInFunction(Call<mat2x3<f16>>());
 
     GeneratorImpl& gen = Build();
 
@@ -362,8 +362,8 @@
     //     var m_2: mat4x4<f32> = mat4x4<f32>(m_1);
     // }
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), mat4x4<f32>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), mat4x4<f32>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -382,8 +382,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), mat4x4<f16>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), mat4x4<f16>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -396,8 +396,9 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Array) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u), vec3<f32>(1_f, 2_f, 3_f),
-                        vec3<f32>(4_f, 5_f, 6_f), vec3<f32>(7_f, 8_f, 9_f)));
+    WrapInFunction(Call<array<vec3<f32>, 3>>(Call<vec3<f32>>(1_f, 2_f, 3_f),
+                                             Call<vec3<f32>>(4_f, 5_f, 6_f),
+                                             Call<vec3<f32>>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -409,7 +410,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Array_Empty) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u)));
+    WrapInFunction(Call<array<vec3<f32>, 3>>());
 
     GeneratorImpl& gen = Build();
 
@@ -424,7 +425,7 @@
                                    Member("c", ty.vec3<i32>()),
                                });
 
-    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, vec3<i32>(3_i, 4_i, 5_i)));
+    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, Call<vec3<i32>>(3_i, 4_i, 5_i)));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
diff --git a/src/tint/writer/hlsl/generator_impl_function_test.cc b/src/tint/writer/hlsl/generator_impl_function_test.cc
index 37d94c3..a374924 100644
--- a/src/tint/writer/hlsl/generator_impl_function_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_function_test.cc
@@ -20,11 +20,12 @@
 
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_Function = TestHelper;
 
 TEST_F(HlslGeneratorImplTest_Function, Emit_Function) {
@@ -101,7 +102,7 @@
     // fn f(foo : ptr<function, f32>) -> f32 {
     //   return *foo;
     // }
-    Func("f", utils::Vector{Param("foo", ty.ptr<f32>(builtin::AddressSpace::kFunction))}, ty.f32(),
+    Func("f", utils::Vector{Param("foo", ty.ptr<function, f32>())}, ty.f32(),
          utils::Vector{Return(Deref("foo"))});
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -216,7 +217,7 @@
 
     Func("vert_main", utils::Empty, ty.Of(interface_struct),
          utils::Vector{
-             Return(Call(ty.Of(interface_struct), Call(ty.vec4<f32>()), Expr(0.5_f), Expr(0.25_f))),
+             Return(Call(ty.Of(interface_struct), Call<vec4<f32>>(), 0.5_f, 0.25_f)),
          },
          utils::Vector{Stage(ast::PipelineStage::kVertex)});
 
@@ -296,8 +297,7 @@
 
     Func("foo", utils::Vector{Param("x", ty.f32())}, ty.Of(vertex_output_struct),
          utils::Vector{
-             Return(
-                 Call(ty.Of(vertex_output_struct), Call(ty.vec4<f32>(), "x", "x", "x", Expr(1_f)))),
+             Return(Call(ty.Of(vertex_output_struct), Call<vec4<f32>>("x", "x", "x", 1_f))),
          },
          utils::Empty);
 
@@ -752,7 +752,7 @@
 TEST_F(HlslGeneratorImplTest_Function, Emit_Function_WithArrayReturn) {
     Func("my_func", utils::Empty, ty.array<f32, 5>(),
          utils::Vector{
-             Return(Call(ty.array<f32, 5>())),
+             Return(Call<array<f32, 5>>()),
          });
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/hlsl/generator_impl_import_test.cc b/src/tint/writer/hlsl/generator_impl_import_test.cc
index 67af40d..1b78943 100644
--- a/src/tint/writer/hlsl/generator_impl_import_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_import_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_Import = TestHelper;
 
 struct HlslImportData {
@@ -89,7 +90,7 @@
 TEST_P(HlslImportData_SingleVectorParamTest, FloatVector) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(0.1_f, 0.2_f, 0.3_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(0.1_f, 0.2_f, 0.3_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -152,7 +153,7 @@
 TEST_P(HlslImportData_DualParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -215,8 +216,8 @@
 TEST_P(HlslImportData_TripleParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f),
-                      vec3<f32>(7_f, 8_f, 9_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f),
+                      Call<vec3<f32>>(7_f, 8_f, 9_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -279,7 +280,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Import, HlslImportData_QuantizeToF16_Vector) {
-    GlobalVar("v", vec3<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec3<f32>>(2_f), builtin::AddressSpace::kPrivate);
 
     auto* expr = Call("quantizeToF16", "v");
     WrapInFunction(expr);
diff --git a/src/tint/writer/hlsl/generator_impl_member_accessor_test.cc b/src/tint/writer/hlsl/generator_impl_member_accessor_test.cc
index 389b8f2..6232315 100644
--- a/src/tint/writer/hlsl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_member_accessor_test.cc
@@ -18,11 +18,12 @@
 
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using create_type_func_ptr = ast::Type (*)(const ProgramBuilder::TypesBuilder& ty);
 
 inline ast::Type ty_i32(const ProgramBuilder::TypesBuilder& ty) {
@@ -1133,7 +1134,7 @@
     });
 
     SetupFunction(utils::Vector{
-        Assign(MemberAccessor("data", "b"), Call(ty.mat2x3<f32>())),
+        Assign(MemberAccessor("data", "b"), Call<mat2x3<f32>>()),
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -1330,7 +1331,7 @@
 
     SetupUniformBuffer(utils::Vector{
         Member("z", ty.f32()),
-        Member("a", ty.array(ty.vec4(ty.i32()), 5_i)),
+        Member("a", ty.array<vec4<i32>, 5>()),
     });
 
     SetupFunction(utils::Vector{
@@ -1505,7 +1506,7 @@
 
     SetupUniformBuffer(utils::Vector{
         Member("z", ty.f32()),
-        Member("a", ty.array(ty.vec4(ty.i32()), 5_i)),
+        Member("a", ty.array<vec4<i32>, 5>()),
     });
 
     SetupFunction(utils::Vector{
@@ -1922,7 +1923,7 @@
 
     SetupFunction(utils::Vector{
         Assign(MemberAccessor(IndexAccessor(MemberAccessor("data", "c"), 2_i), "b"),
-               vec3<f32>(1_f, 2_f, 3_f)),
+               Call<vec3<f32>>(1_f, 2_f, 3_f)),
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -1981,7 +1982,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_MemberAccessor, Swizzle_xyz) {
-    auto* var = Var("my_vec", ty.vec4<f32>(), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+    auto* var = Var("my_vec", ty.vec4<f32>(), Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
     auto* expr = MemberAccessor("my_vec", "xyz");
     WrapInFunction(var, expr);
 
@@ -1991,7 +1992,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_MemberAccessor, Swizzle_gbr) {
-    auto* var = Var("my_vec", ty.vec4<f32>(), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+    auto* var = Var("my_vec", ty.vec4<f32>(), Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f));
     auto* expr = MemberAccessor("my_vec", "gbr");
     WrapInFunction(var, expr);
 
diff --git a/src/tint/writer/hlsl/generator_impl_module_constant_test.cc b/src/tint/writer/hlsl/generator_impl_module_constant_test.cc
index e7805a7..c4e4e01 100644
--- a/src/tint/writer/hlsl/generator_impl_module_constant_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_module_constant_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslGeneratorImplTest_ModuleConstant = TestHelper;
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_AInt) {
@@ -109,7 +110,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_AInt) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -123,7 +124,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -137,7 +138,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_f32) {
-    auto* var = GlobalConst("G", vec3<f32>(1_f, 2_f, 3_f));
+    auto* var = GlobalConst("G", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -153,7 +154,7 @@
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", vec3<f16>(1_h, 2_h, 3_h));
+    auto* var = GlobalConst("G", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -167,7 +168,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* var = GlobalConst("G", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -181,7 +182,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_f32) {
-    auto* var = GlobalConst("G", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* var = GlobalConst("G", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -197,7 +198,7 @@
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* var = GlobalConst("G", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -211,7 +212,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_arr_f32) {
-    auto* var = GlobalConst("G", Call(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
+    auto* var = GlobalConst("G", Call<array<f32, 3>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -225,10 +226,10 @@
 }
 
 TEST_F(HlslGeneratorImplTest_ModuleConstant, Emit_GlobalConst_arr_vec2_bool) {
-    auto* var = GlobalConst("G", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                                      vec2<bool>(true, false),         //
-                                      vec2<bool>(false, true),         //
-                                      vec2<bool>(true, true)));
+    auto* var = GlobalConst("G", Call<array<vec2<bool>, 3>>(         //
+                                     Call<vec2<bool>>(true, false),  //
+                                     Call<vec2<bool>>(false, true),  //
+                                     Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/hlsl/generator_impl_sanitizer_test.cc b/src/tint/writer/hlsl/generator_impl_sanitizer_test.cc
index b1fef10..0d5ed73 100644
--- a/src/tint/writer/hlsl/generator_impl_sanitizer_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_sanitizer_test.cc
@@ -17,11 +17,12 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using HlslSanitizerTest = TestHelper;
 
 TEST_F(HlslSanitizerTest, Call_ArrayLength) {
@@ -170,7 +171,7 @@
 }
 
 TEST_F(HlslSanitizerTest, PromoteArrayInitializerToConstVar) {
-    auto* array_init = array<i32, 4>(1_i, 2_i, 3_i, 4_i);
+    auto* array_init = Call<array<i32, 4>>(1_i, 2_i, 3_i, 4_i);
 
     Func("main", utils::Empty, ty.void_(),
          utils::Vector{
@@ -203,7 +204,7 @@
                                    Member("b", ty.vec3<f32>()),
                                    Member("c", ty.i32()),
                                });
-    auto* struct_init = Call(ty.Of(str), 1_i, vec3<f32>(2_f, runtime_value, 4_f), 4_i);
+    auto* struct_init = Call(ty.Of(str), 1_i, Call<vec3<f32>>(2_f, runtime_value, 4_f), 4_i);
     auto* struct_access = MemberAccessor(struct_init, "b");
     auto* pos = Var("pos", ty.vec3<f32>(), struct_access);
 
@@ -242,7 +243,7 @@
     // let p : ptr<function, i32> = &v;
     // let x : i32 = *p;
     auto* v = Var("v", ty.i32());
-    auto* p = Let("p", ty.ptr<i32>(builtin::AddressSpace::kFunction), AddressOf(v));
+    auto* p = Let("p", ty.ptr<function, i32>(), AddressOf(v));
     auto* x = Var("x", ty.i32(), Deref(p));
 
     Func("main", utils::Empty, ty.void_(),
@@ -276,12 +277,9 @@
     // let vp : ptr<function, vec4<f32>> = &(*mp)[2i];
     // let v : vec4<f32> = *vp;
     auto* a = Var("a", ty.array(ty.mat4x4<f32>(), 4_u));
-    auto* ap = Let("ap", ty.ptr(builtin::AddressSpace::kFunction, ty.array(ty.mat4x4<f32>(), 4_u)),
-                   AddressOf(a));
-    auto* mp = Let("mp", ty.ptr(builtin::AddressSpace::kFunction, ty.mat4x4<f32>()),
-                   AddressOf(IndexAccessor(Deref(ap), 3_i)));
-    auto* vp = Let("vp", ty.ptr(builtin::AddressSpace::kFunction, ty.vec4<f32>()),
-                   AddressOf(IndexAccessor(Deref(mp), 2_i)));
+    auto* ap = Let("ap", ty.ptr<function, array<mat4x4<f32>, 4>>(), AddressOf(a));
+    auto* mp = Let("mp", ty.ptr<function, mat4x4<f32>>(), AddressOf(IndexAccessor(Deref(ap), 3_i)));
+    auto* vp = Let("vp", ty.ptr<function, vec4<f32>>(), AddressOf(IndexAccessor(Deref(mp), 2_i)));
     auto* v = Var("v", ty.vec4<f32>(), Deref(vp));
 
     Func("main", utils::Empty, ty.void_(),
diff --git a/src/tint/writer/hlsl/generator_impl_variable_decl_statement_test.cc b/src/tint/writer/hlsl/generator_impl_variable_decl_statement_test.cc
index f50ab58..f53eee2 100644
--- a/src/tint/writer/hlsl/generator_impl_variable_decl_statement_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_variable_decl_statement_test.cc
@@ -16,11 +16,12 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::hlsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using ::testing::HasSubstr;
 
 using HlslGeneratorImplTest_VariableDecl = TestHelper;
@@ -175,7 +176,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_AInt) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* C = Const("C", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -193,7 +194,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_AFloat) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* C = Const("C", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -211,7 +212,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_f32) {
-    auto* C = Const("C", vec3<f32>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -231,7 +232,7 @@
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", vec3<f16>(1_h, 2_h, 3_h));
+    auto* C = Const("C", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -249,7 +250,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_AFloat) {
-    auto* C = Const("C", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* C = Const("C", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -267,7 +268,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_f32) {
-    auto* C = Const("C", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* C = Const("C", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -287,7 +288,7 @@
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* C = Const("C", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -323,10 +324,10 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Const_arr_vec2_bool) {
-    auto* C = Const("C", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                              vec2<bool>(true, false),         //
-                              vec2<bool>(false, true),         //
-                              vec2<bool>(true, true)));
+    auto* C = Const("C", Call<array<vec2<bool>, 3>>(         //
+                             Call<vec2<bool>>(true, false),  //
+                             Call<vec2<bool>>(false, true),  //
+                             Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -370,7 +371,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroVec_F32) {
-    auto* var = Var("a", ty.vec3<f32>(), vec3<f32>());
+    auto* var = Var("a", ty.vec3<f32>(), Call<vec3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -385,7 +386,7 @@
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroVec_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.vec3<f16>(), vec3<f16>());
+    auto* var = Var("a", ty.vec3<f16>(), Call<vec3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -398,7 +399,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroMat_F32) {
-    auto* var = Var("a", ty.mat2x3<f32>(), mat2x3<f32>());
+    auto* var = Var("a", ty.mat2x3<f32>(), Call<mat2x3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -414,7 +415,7 @@
 TEST_F(HlslGeneratorImplTest_VariableDecl, Emit_VariableDeclStatement_Initializer_ZeroMat_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.mat2x3<f16>(), mat2x3<f16>());
+    auto* var = Var("a", ty.mat2x3<f16>(), Call<mat2x3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
diff --git a/src/tint/writer/msl/generator_impl_builtin_test.cc b/src/tint/writer/msl/generator_impl_builtin_test.cc
index 5a38ba2..bb117b1 100644
--- a/src/tint/writer/msl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/msl/generator_impl_builtin_test.cc
@@ -17,11 +17,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using MslGeneratorImplTest = TestHelper;
 
 enum class CallParamType {
@@ -473,7 +474,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Runtime_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("f", vec3<f32>(1.5_f, 2.5_f, 3.5_f))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -505,7 +506,7 @@
 TEST_F(MslGeneratorImplTest, Runtime_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("f", vec3<f16>(1.5_h, 2.5_h, 3.5_h))),  //
+    WrapInFunction(Decl(Let("f", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h))),  //
                    Decl(Let("v", Call("modf", "f"))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -581,7 +582,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Const_Modf_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f32>(1.5_f, 2.5_f, 3.5_f)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f32>>(1.5_f, 2.5_f, 3.5_f)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -605,7 +606,7 @@
 TEST_F(MslGeneratorImplTest, Const_Modf_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("modf", vec3<f16>(1.5_h, 2.5_h, 3.5_h)))));
+    WrapInFunction(Decl(Let("v", Call("modf", Call<vec3<f16>>(1.5_h, 2.5_h, 3.5_h)))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -689,7 +690,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Runtime_Frexp_Vector_f32) {
-    WrapInFunction(Var("f", Expr(vec3<f32>())),  //
+    WrapInFunction(Var("f", Call<vec3<f32>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -721,7 +722,7 @@
 TEST_F(MslGeneratorImplTest, Runtime_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Var("f", Expr(vec3<f16>())),  //
+    WrapInFunction(Var("f", Call<vec3<f16>>()),  //
                    Var("v", Call("frexp", "f")));
 
     GeneratorImpl& gen = SanitizeAndBuild();
@@ -797,7 +798,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Const_Frexp_Vector_f32) {
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f32>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f32>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
@@ -821,7 +822,7 @@
 TEST_F(MslGeneratorImplTest, Const_Frexp_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(Decl(Let("v", Call("frexp", vec3<f16>()))));
+    WrapInFunction(Decl(Let("v", Call("frexp", Call<vec3<f16>>()))));
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
diff --git a/src/tint/writer/msl/generator_impl_cast_test.cc b/src/tint/writer/msl/generator_impl_cast_test.cc
index 78d6c58..3895559 100644
--- a/src/tint/writer/msl/generator_impl_cast_test.cc
+++ b/src/tint/writer/msl/generator_impl_cast_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using MslGeneratorImplTest = TestHelper;
 
 TEST_F(MslGeneratorImplTest, EmitExpression_Cast_Scalar) {
@@ -34,7 +35,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, EmitExpression_Cast_Vector) {
-    auto* cast = vec3<f32>(vec3<i32>(1_i, 2_i, 3_i));
+    auto* cast = Call<vec3<f32>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(cast);
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/msl/generator_impl_constructor_test.cc b/src/tint/writer/msl/generator_impl_constructor_test.cc
index b3a333e..64c2629 100644
--- a/src/tint/writer/msl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/msl/generator_impl_constructor_test.cc
@@ -15,12 +15,12 @@
 #include "gmock/gmock.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using MslGeneratorImplTest_Constructor = TestHelper;
 
@@ -121,7 +121,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_F32) {
-    WrapInFunction(vec3<f32>(1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec3<f32>>(1_f, 2_f, 3_f));
 
     GeneratorImpl& gen = Build();
 
@@ -132,7 +132,7 @@
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(1_h, 2_h, 3_h));
+    WrapInFunction(Call<vec3<f16>>(1_h, 2_h, 3_h));
 
     GeneratorImpl& gen = Build();
 
@@ -141,7 +141,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_Empty_F32) {
-    WrapInFunction(vec3<f32>());
+    WrapInFunction(Call<vec3<f32>>());
 
     GeneratorImpl& gen = Build();
 
@@ -152,7 +152,7 @@
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>());
+    WrapInFunction(Call<vec3<f16>>());
 
     GeneratorImpl& gen = Build();
 
@@ -161,7 +161,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Literal) {
-    WrapInFunction(vec3<f32>(2_f));
+    WrapInFunction(Call<vec3<f32>>(2_f));
 
     GeneratorImpl& gen = Build();
 
@@ -172,7 +172,7 @@
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F16_Literal) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(2_h));
+    WrapInFunction(Call<vec3<f16>>(2_h));
 
     GeneratorImpl& gen = Build();
 
@@ -182,7 +182,7 @@
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_F32_Var) {
     auto* var = Var("v", Expr(2_f));
-    auto* cast = vec3<f32>(var);
+    auto* cast = Call<vec3<f32>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -196,7 +196,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Var("v", Expr(2_h));
-    auto* cast = vec3<f16>(var);
+    auto* cast = Call<vec3<f16>>(var);
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
@@ -207,7 +207,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Bool) {
-    WrapInFunction(vec3<bool>(true));
+    WrapInFunction(Call<vec3<bool>>(true));
 
     GeneratorImpl& gen = Build();
 
@@ -216,7 +216,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_Int) {
-    WrapInFunction(vec3<i32>(2_i));
+    WrapInFunction(Call<vec3<i32>>(2_i));
 
     GeneratorImpl& gen = Build();
 
@@ -225,7 +225,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Vec_SingleScalar_UInt) {
-    WrapInFunction(vec3<u32>(2_u));
+    WrapInFunction(Call<vec3<u32>>(2_u));
 
     GeneratorImpl& gen = Build();
 
@@ -234,7 +234,8 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Mat_F32) {
-    WrapInFunction(mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(3_f, 4_f, 5_f)));
+    WrapInFunction(
+        Call<mat2x3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(3_f, 4_f, 5_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -247,7 +248,8 @@
 TEST_F(MslGeneratorImplTest_Constructor, Type_Mat_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>(vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(3_h, 4_h, 5_h)));
+    WrapInFunction(
+        Call<mat2x3<f16>>(Call<vec3<f16>>(1_h, 2_h, 3_h), Call<vec3<f16>>(3_h, 4_h, 5_h)));
 
     GeneratorImpl& gen = Build();
 
@@ -264,15 +266,14 @@
     //     vec4<f32>(7.0f),
     //     vec4<f32>(vec4<f32>(42.0f, 21.0f, 6.0f, -5.0f)),
     //   );
-    auto* vector_literal =
-        vec4<f32>(Expr(f32(2.0)), Expr(f32(3.0)), Expr(f32(4.0)), Expr(f32(8.0)));
-    auto* vector_zero_init = vec4<f32>();
-    auto* vector_single_scalar_init = vec4<f32>(Expr(f32(7.0)));
+    auto* vector_literal = Call<vec4<f32>>(f32(2.0), f32(3.0), f32(4.0), f32(8.0));
+    auto* vector_zero_init = Call<vec4<f32>>();
+    auto* vector_single_scalar_init = Call<vec4<f32>>(f32(7.0));
     auto* vector_identical_init =
-        vec4<f32>(vec4<f32>(Expr(f32(42.0)), Expr(f32(21.0)), Expr(f32(6.0)), Expr(f32(-5.0))));
+        Call<vec4<f32>>(Call<vec4<f32>>(f32(42.0), f32(21.0), f32(6.0), f32(-5.0)));
 
-    auto* constructor = mat4x4<f32>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                                    vector_identical_init);
+    auto* constructor = Call<mat4x4<f32>>(vector_literal, vector_zero_init,
+                                          vector_single_scalar_init, vector_identical_init);
 
     WrapInFunction(constructor);
 
@@ -293,15 +294,14 @@
     //   );
     Enable(builtin::Extension::kF16);
 
-    auto* vector_literal =
-        vec4<f16>(Expr(f16(2.0)), Expr(f16(3.0)), Expr(f16(4.0)), Expr(f16(8.0)));
-    auto* vector_zero_init = vec4<f16>();
-    auto* vector_single_scalar_init = vec4<f16>(Expr(f16(7.0)));
+    auto* vector_literal = Call<vec4<f16>>(f16(2.0), f16(3.0), f16(4.0), f16(8.0));
+    auto* vector_zero_init = Call<vec4<f16>>();
+    auto* vector_single_scalar_init = Call<vec4<f16>>(f16(7.0));
     auto* vector_identical_init =
-        vec4<f16>(vec4<f16>(Expr(f16(42.0)), Expr(f16(21.0)), Expr(f16(6.0)), Expr(f16(-5.0))));
+        Call<vec4<f16>>(Call<vec4<f16>>(f16(42.0), f16(21.0), f16(6.0), f16(-5.0)));
 
-    auto* constructor = mat4x4<f16>(vector_literal, vector_zero_init, vector_single_scalar_init,
-                                    vector_identical_init);
+    auto* constructor = Call<mat4x4<f16>>(vector_literal, vector_zero_init,
+                                          vector_single_scalar_init, vector_identical_init);
 
     WrapInFunction(constructor);
 
@@ -314,7 +314,7 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Mat_Empty_F32) {
-    WrapInFunction(mat2x3<f32>());
+    WrapInFunction(Call<mat2x3<f32>>());
 
     GeneratorImpl& gen = Build();
 
@@ -327,7 +327,7 @@
 TEST_F(MslGeneratorImplTest_Constructor, Type_Mat_Empty_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>());
+    WrapInFunction(Call<mat2x3<f16>>());
 
     GeneratorImpl& gen = Build();
 
@@ -343,8 +343,8 @@
     //     var m_2: mat4x4<f32> = mat4x4<f32>(m_1);
     // }
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), mat4x4<f32>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), mat4x4<f32>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f32()), Call<mat4x4<f32>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -363,8 +363,8 @@
 
     Enable(builtin::Extension::kF16);
 
-    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), mat4x4<f16>());
-    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), mat4x4<f16>(m_1));
+    auto* m_1 = Var("m_1", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>());
+    auto* m_2 = Var("m_2", ty.mat4x4(ty.f16()), Call<mat4x4<f16>>(m_1));
 
     WrapInFunction(m_1, m_2);
 
@@ -376,8 +376,9 @@
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Array) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u), vec3<f32>(1_f, 2_f, 3_f),
-                        vec3<f32>(4_f, 5_f, 6_f), vec3<f32>(7_f, 8_f, 9_f)));
+    WrapInFunction(Call<array<vec3<f32>, 3>>(Call<vec3<f32>>(1_f, 2_f, 3_f),
+                                             Call<vec3<f32>>(4_f, 5_f, 6_f),
+                                             Call<vec3<f32>>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -393,7 +394,7 @@
                                    Member("c", ty.vec3<i32>()),
                                });
 
-    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, vec3<i32>(3_i, 4_i, 5_i)));
+    WrapInFunction(Call(ty.Of(str), 1_i, 2_f, Call<vec3<i32>>(3_i, 4_i, 5_i)));
 
     GeneratorImpl& gen = Build();
 
diff --git a/src/tint/writer/msl/generator_impl_function_test.cc b/src/tint/writer/msl/generator_impl_function_test.cc
index b828574..c8020f5 100644
--- a/src/tint/writer/msl/generator_impl_function_test.cc
+++ b/src/tint/writer/msl/generator_impl_function_test.cc
@@ -16,11 +16,12 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using MslGeneratorImplTest = TestHelper;
 
 TEST_F(MslGeneratorImplTest, Emit_Function) {
@@ -194,7 +195,7 @@
         });
 
     Func("vert_main", utils::Empty, ty.Of(interface_struct),
-         utils::Vector{Return(Call(ty.Of(interface_struct), 0.5_f, 0.25_f, vec4<f32>()))},
+         utils::Vector{Return(Call(ty.Of(interface_struct), 0.5_f, 0.25_f, Call<vec4<f32>>()))},
          utils::Vector{Stage(ast::PipelineStage::kVertex)});
 
     Func("frag_main", utils::Vector{Param("colors", ty.Of(interface_struct))}, ty.void_(),
@@ -276,8 +277,7 @@
 
     Func("foo", utils::Vector{Param("x", ty.f32())}, ty.Of(vertex_output_struct),
          utils::Vector{
-             Return(
-                 Call(ty.Of(vertex_output_struct), Call(ty.vec4<f32>(), "x", "x", "x", Expr(1_f)))),
+             Return(Call(ty.Of(vertex_output_struct), Call<vec4<f32>>("x", "x", "x", 1_f))),
          });
     Func("vert_main1", utils::Empty, ty.Of(vertex_output_struct),
          utils::Vector{Return(Expr(Call("foo", Expr(0.5_f))))},
diff --git a/src/tint/writer/msl/generator_impl_import_test.cc b/src/tint/writer/msl/generator_impl_import_test.cc
index d221665..8b4a877 100644
--- a/src/tint/writer/msl/generator_impl_import_test.cc
+++ b/src/tint/writer/msl/generator_impl_import_test.cc
@@ -16,11 +16,12 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using MslGeneratorImplTest = TestHelper;
 
 struct MslImportData {
@@ -134,7 +135,7 @@
 TEST_P(MslImportData_DualParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -196,8 +197,8 @@
 TEST_P(MslImportData_TripleParam_VectorTest, Float) {
     auto param = GetParam();
 
-    auto* expr = Call(param.name, vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f),
-                      vec3<f32>(7_f, 8_f, 9_f));
+    auto* expr = Call(param.name, Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(4_f, 5_f, 6_f),
+                      Call<vec3<f32>>(7_f, 8_f, 9_f));
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
@@ -262,7 +263,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, MslImportData_QuantizeToF16_Vector) {
-    GlobalVar("v", vec3<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec3<f32>>(2_f), builtin::AddressSpace::kPrivate);
 
     auto* expr = Call("quantizeToF16", "v");
     WrapInFunction(expr);
diff --git a/src/tint/writer/msl/generator_impl_module_constant_test.cc b/src/tint/writer/msl/generator_impl_module_constant_test.cc
index 4d977b0..4f99b31 100644
--- a/src/tint/writer/msl/generator_impl_module_constant_test.cc
+++ b/src/tint/writer/msl/generator_impl_module_constant_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using MslGeneratorImplTest = TestHelper;
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_AInt) {
@@ -133,7 +134,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_vec3_AInt) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -151,7 +152,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_vec3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* var = GlobalConst("G", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -169,7 +170,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_vec3_f32) {
-    auto* var = GlobalConst("G", vec3<f32>(1_f, 2_f, 3_f));
+    auto* var = GlobalConst("G", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -189,7 +190,7 @@
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", vec3<f16>(1_h, 2_h, 3_h));
+    auto* var = GlobalConst("G", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -207,7 +208,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_mat2x3_AFloat) {
-    auto* var = GlobalConst("G", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* var = GlobalConst("G", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -225,7 +226,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_mat2x3_f32) {
-    auto* var = GlobalConst("G", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* var = GlobalConst("G", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -245,7 +246,7 @@
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = GlobalConst("G", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* var = GlobalConst("G", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
@@ -294,10 +295,9 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_GlobalConst_arr_vec2_bool) {
-    auto* var = GlobalConst("G", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                                      vec2<bool>(true, false),         //
-                                      vec2<bool>(false, true),         //
-                                      vec2<bool>(true, true)));
+    auto* var = GlobalConst("G", Call<array<vec2<bool>, 3>>(Call<vec2<bool>>(true, false),  //
+                                                            Call<vec2<bool>>(false, true),  //
+                                                            Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(Let("l", Expr(var)))});
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/msl/generator_impl_type_test.cc b/src/tint/writer/msl/generator_impl_type_test.cc
index 52d02ee..05109c0 100644
--- a/src/tint/writer/msl/generator_impl_type_test.cc
+++ b/src/tint/writer/msl/generator_impl_type_test.cc
@@ -26,13 +26,13 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
+using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 void FormatMSLField(utils::StringStream& out,
                     const char* addr,
                     const char* type,
@@ -562,7 +562,7 @@
 
 TEST_F(MslGeneratorImplTest, EmitType_Struct_Layout_ArrayVec3DefaultStride) {
     // array: size(64), align(16)
-    auto array = ty.array(ty.vec3<f32>(), 4_u);
+    auto array = ty.array<vec3<f32>, 4>();
 
     auto* s = Structure("S", utils::Vector{
                                  Member("a", ty.i32()),
diff --git a/src/tint/writer/msl/generator_impl_variable_decl_statement_test.cc b/src/tint/writer/msl/generator_impl_variable_decl_statement_test.cc
index 3660be5..be9fe72 100644
--- a/src/tint/writer/msl/generator_impl_variable_decl_statement_test.cc
+++ b/src/tint/writer/msl/generator_impl_variable_decl_statement_test.cc
@@ -16,12 +16,12 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/msl/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::msl {
 namespace {
 
 using ::testing::HasSubstr;
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
 
 using MslGeneratorImplTest = TestHelper;
 
@@ -175,7 +175,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_AInt) {
-    auto* C = Const("C", vec3<Infer>(1_a, 2_a, 3_a));
+    auto* C = Const("C", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -193,7 +193,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_AFloat) {
-    auto* C = Const("C", vec3<Infer>(1._a, 2._a, 3._a));
+    auto* C = Const("C", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -211,7 +211,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_f32) {
-    auto* C = Const("C", vec3<f32>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -231,7 +231,7 @@
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", vec3<f16>(1_h, 2_h, 3_h));
+    auto* C = Const("C", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -249,7 +249,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_AFloat) {
-    auto* C = Const("C", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* C = Const("C", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -267,7 +267,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_f32) {
-    auto* C = Const("C", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* C = Const("C", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -287,7 +287,7 @@
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* C = Const("C", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -305,7 +305,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_arr_f32) {
-    auto* C = Const("C", array<f32, 3>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<array<f32, 3>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -336,10 +336,10 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Const_arr_vec2_bool) {
-    auto* C = Const("C", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                              vec2<bool>(true, false),         //
-                              vec2<bool>(false, true),         //
-                              vec2<bool>(true, true)));
+    auto* C = Const("C", Call<array<vec2<bool>, 3>>(         //
+                             Call<vec2<bool>>(true, false),  //
+                             Call<vec2<bool>>(false, true),  //
+                             Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(), utils::Vector{Decl(C), Decl(Let("l", Expr(C)))});
 
     GeneratorImpl& gen = Build();
@@ -460,7 +460,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_ZeroVec_f32) {
-    auto* var = Var("a", ty.vec3<f32>(), vec3<f32>());
+    auto* var = Var("a", ty.vec3<f32>(), Call<vec3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -475,7 +475,7 @@
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_ZeroVec_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.vec3<f16>(), vec3<f16>());
+    auto* var = Var("a", ty.vec3<f16>(), Call<vec3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -488,7 +488,7 @@
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_ZeroMat_f32) {
-    auto* var = Var("a", ty.mat2x3<f32>(), mat2x3<f32>());
+    auto* var = Var("a", ty.mat2x3<f32>(), Call<mat2x3<f32>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
@@ -504,7 +504,7 @@
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_ZeroMat_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Var("a", ty.mat2x3<f16>(), mat2x3<f16>());
+    auto* var = Var("a", ty.mat2x3<f16>(), Call<mat2x3<f16>>());
 
     auto* stmt = Decl(var);
     WrapInFunction(stmt);
diff --git a/src/tint/writer/spirv/builder_accessor_expression_test.cc b/src/tint/writer/spirv/builder_accessor_expression_test.cc
index d93b414..f42da66 100644
--- a/src/tint/writer/spirv/builder_accessor_expression_test.cc
+++ b/src/tint/writer/spirv/builder_accessor_expression_test.cc
@@ -15,18 +15,19 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, Let_IndexAccessor_Vector) {
     // let ary = vec3<i32>(1, 2, 3);
     // var x = ary[1i];
 
-    auto* ary = Let("ary", vec3<i32>(1_i, 2_i, 3_i));
+    auto* ary = Let("ary", Call<vec3<i32>>(1_i, 2_i, 3_i));
     auto* x = Var("x", IndexAccessor(ary, 1_i));
     WrapInFunction(ary, x);
 
@@ -61,7 +62,7 @@
     // const ary = vec3<i32>(1, 2, 3);
     // var x = ary[1i];
 
-    auto* ary = Const("ary", vec3<i32>(1_i, 2_i, 3_i));
+    auto* ary = Const("ary", Call<vec3<i32>>(1_i, 2_i, 3_i));
     auto* x = Var("x", IndexAccessor(ary, 1_i));
     WrapInFunction(ary, x);
 
@@ -169,7 +170,7 @@
     // let ary : vec3<i32>(1, 2, 3);
     // var x = ary[1i + 1i];
 
-    auto* ary = Let("ary", vec3<i32>(1_i, 2_i, 3_i));
+    auto* ary = Let("ary", Call<vec3<i32>>(1_i, 2_i, 3_i));
     auto* x = Var("x", IndexAccessor(ary, Add(1_i, 1_i)));
     WrapInFunction(ary, x);
 
@@ -286,8 +287,8 @@
     // let ary = array<vec3<f32>, 2u>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));
     // var x = ary[1i][2i];
 
-    auto* ary = Let("ary", array(ty.vec3<f32>(), 2_u, vec3<f32>(1._f, 2._f, 3._f),
-                                 vec3<f32>(4._f, 5._f, 6._f)));
+    auto* ary = Let("ary", Call<array<vec3<f32>, 2>>(Call<vec3<f32>>(1._f, 2._f, 3._f),
+                                                     Call<vec3<f32>>(4._f, 5._f, 6._f)));
     auto* x = Var("x", IndexAccessor(IndexAccessor(ary, 1_i), 2_i));
     WrapInFunction(ary, x);
 
@@ -334,8 +335,8 @@
     // const ary = array<vec3<f32>, 2u>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));
     // var x = ary[1i][2i];
 
-    auto* ary = Const("ary", array(ty.vec3<f32>(), 2_u, vec3<f32>(1._f, 2._f, 3._f),
-                                   vec3<f32>(4._f, 5._f, 6._f)));
+    auto* ary = Const("ary", Call<array<vec3<f32>, 2>>(Call<vec3<f32>>(1._f, 2._f, 3._f),
+                                                       Call<vec3<f32>>(4._f, 5._f, 6._f)));
     auto* x = Var("x", IndexAccessor(IndexAccessor(ary, 1_i), 2_i));
     WrapInFunction(ary, x);
 
@@ -365,7 +366,7 @@
     // var ary : array<vec3<f32>, 4u>;
     // var x = ary[1i][2i];
 
-    auto* ary = Var("ary", ty.array(ty.vec3<f32>(), 4_u));
+    auto* ary = Var("ary", ty.array<vec3<f32>, 4>());
     auto* x = Var("x", IndexAccessor(IndexAccessor(ary, 1_i), 2_i));
     WrapInFunction(ary, x);
 
@@ -407,7 +408,7 @@
     // var one = 1i;
     // var x = ary[one][2i];
 
-    auto* ary = Var("ary", ty.array(ty.vec3<f32>(), 4_u));
+    auto* ary = Var("ary", ty.array<vec3<f32>, 4>());
     auto* one = Var("one", Expr(3_i));
     auto* x = Var("x", IndexAccessor(IndexAccessor(ary, one), 2_i));
     WrapInFunction(ary, one, x);
@@ -453,8 +454,8 @@
     // let ary = array<vec3<f32>, 2u>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));
     // var x = a[1i].xy;
 
-    auto* ary = Let("ary", array(ty.vec3<f32>(), 2_u, vec3<f32>(1._f, 2._f, 3._f),
-                                 vec3<f32>(4._f, 5._f, 6._f)));
+    auto* ary = Let("ary", Call<array<vec3<f32>, 2>>(Call<vec3<f32>>(1._f, 2._f, 3._f),
+                                                     Call<vec3<f32>>(4._f, 5._f, 6._f)));
     auto* x = Var("x", MemberAccessor(IndexAccessor("ary", 1_i), "xy"));
     WrapInFunction(ary, x);
 
@@ -501,7 +502,7 @@
     // var ary : array<vec3<f32>, 4u>;
     // var x = ary[1i].xy;
 
-    auto* ary = Var("ary", ty.array(ty.vec3<f32>(), 4_u));
+    auto* ary = Var("ary", ty.array<vec3<f32>, 4>());
     auto* x = Var("x", MemberAccessor(IndexAccessor("ary", 1_i), "xy"));
     WrapInFunction(ary, x);
 
@@ -545,7 +546,7 @@
     // var one = 1i;
     // var x = ary[one].xy;
 
-    auto* ary = Var("ary", ty.array(ty.vec3<f32>(), 4_u));
+    auto* ary = Var("ary", ty.array<vec3<f32>, 4>());
     auto* one = Var("one", Expr(1_i));
     auto* x = Var("x", MemberAccessor(IndexAccessor("ary", one), "xy"));
     WrapInFunction(ary, one, x);
@@ -596,9 +597,10 @@
     //   array<f32, 2>(0.5, -0.5));
     // var x = pos[1u][0u];
 
-    auto* pos = Let("pos", ty.array(ty.vec2<f32>(), 3_u),
-                    Call(ty.array(ty.vec2<f32>(), 3_u), vec2<f32>(0_f, 0.5_f),
-                         vec2<f32>(-0.5_f, -0.5_f), vec2<f32>(0.5_f, -0.5_f)));
+    auto* pos =
+        Let("pos", ty.array<vec2<f32>, 3>(),
+            Call<array<vec2<f32>, 3>>(Call<vec2<f32>>(0_f, 0.5_f), Call<vec2<f32>>(-0.5_f, -0.5_f),
+                                      Call<vec2<f32>>(0.5_f, -0.5_f)));
     auto* x = Var("x", IndexAccessor(IndexAccessor(pos, 1_u), 0_u));
     WrapInFunction(pos, x);
 
@@ -644,9 +646,10 @@
     //   array<f32, 2>(0.5, -0.5));
     // var x = pos[1u][0u];
 
-    auto* pos = Const("pos", ty.array(ty.vec2<f32>(), 3_u),
-                      Call(ty.array(ty.vec2<f32>(), 3_u), vec2<f32>(0_f, 0.5_f),
-                           vec2<f32>(-0.5_f, -0.5_f), vec2<f32>(0.5_f, -0.5_f)));
+    auto* pos = Const(
+        "pos", ty.array<vec2<f32>, 3>(),
+        Call<array<vec2<f32>, 3>>(Call<vec2<f32>>(0_f, 0.5_f), Call<vec2<f32>>(-0.5_f, -0.5_f),
+                                  Call<vec2<f32>>(0.5_f, -0.5_f)));
     auto* x = Var("x", IndexAccessor(IndexAccessor(pos, 1_u), 0_u));
     WrapInFunction(pos, x);
 
@@ -676,7 +679,7 @@
     // var pos : array<vec3<f32>, 3u>;
     // var x = pos[1u][2u];
 
-    auto* pos = Var("pos", ty.array(ty.vec3<f32>(), 3_a));
+    auto* pos = Var("pos", ty.array<vec3<f32>, 3>());
     auto* x = Var("x", IndexAccessor(IndexAccessor(pos, 1_u), 2_u));
     WrapInFunction(pos, x);
 
@@ -762,9 +765,10 @@
     // let a : mat2x2<f32>(vec2<f32>(1., 2.), vec2<f32>(3., 4.));
     // var x = a[1i]
 
-    auto* a =
-        Let("a", ty.mat2x2<f32>(),
-            Call(ty.mat2x2<f32>(), Call(ty.vec2<f32>(), 1_f, 2_f), Call(ty.vec2<f32>(), 3_f, 4_f)));
+    auto* a = Let("a", ty.mat2x2<f32>(),
+                  Call<mat2x2<f32>>(              //
+                      Call<vec2<f32>>(1_f, 2_f),  //
+                      Call<vec2<f32>>(3_f, 4_f)));
     auto* x = Var("x", IndexAccessor("a", 1_i));
     WrapInFunction(a, x);
 
@@ -805,9 +809,10 @@
     // const a : mat2x2<f32>(vec2<f32>(1., 2.), vec2<f32>(3., 4.));
     // var x = a[1i]
 
-    auto* a = Const(
-        "a", ty.mat2x2<f32>(),
-        Call(ty.mat2x2<f32>(), Call(ty.vec2<f32>(), 1_f, 2_f), Call(ty.vec2<f32>(), 3_f, 4_f)));
+    auto* a = Const("a", ty.mat2x2<f32>(),
+                    Call<mat2x2<f32>>(              //
+                        Call<vec2<f32>>(1_f, 2_f),  //
+                        Call<vec2<f32>>(3_f, 4_f)));
     auto* x = Var("x", IndexAccessor("a", 1_i));
     WrapInFunction(a, x);
 
diff --git a/src/tint/writer/spirv/builder_assign_test.cc b/src/tint/writer/spirv/builder_assign_test.cc
index e37563a..d6d9d35 100644
--- a/src/tint/writer/spirv/builder_assign_test.cc
+++ b/src/tint/writer/spirv/builder_assign_test.cc
@@ -17,11 +17,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, Assign_Var) {
@@ -75,7 +76,7 @@
 TEST_F(BuilderTest, Assign_Var_ZeroInitializer) {
     auto* v = GlobalVar("var", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
-    auto* val = vec3<f32>();
+    auto* val = Call<vec3<f32>>();
     auto* assign = Assign("var", val);
 
     WrapInFunction(assign);
@@ -102,7 +103,7 @@
 }
 
 TEST_F(BuilderTest, Assign_Var_Complex_InitializerNestedVector) {
-    auto* init = vec3<f32>(vec2<f32>(1_f, 2_f), 3_f);
+    auto* init = Call<vec3<f32>>(Call<vec2<f32>>(1_f, 2_f), 3_f);
 
     auto* v = GlobalVar("var", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
@@ -135,7 +136,7 @@
 }
 
 TEST_F(BuilderTest, Assign_Var_Complex_Initializer) {
-    auto* init = vec3<f32>(1_f, 2_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 2_f, 3_f);
 
     auto* v = GlobalVar("var", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
@@ -214,7 +215,7 @@
 TEST_F(BuilderTest, Assign_Vector) {
     auto* v = GlobalVar("var", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
-    auto* val = vec3<f32>(1_f, 1_f, 3_f);
+    auto* val = Call<vec3<f32>>(1_f, 1_f, 3_f);
     auto* assign = Assign("var", val);
 
     WrapInFunction(assign);
diff --git a/src/tint/writer/spirv/builder_binary_expression_test.cc b/src/tint/writer/spirv/builder_binary_expression_test.cc
index 36bd427..70dc106 100644
--- a/src/tint/writer/spirv/builder_binary_expression_test.cc
+++ b/src/tint/writer/spirv/builder_binary_expression_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 struct BinaryData {
@@ -66,8 +67,8 @@
         return;
     }
 
-    auto* lhs = vec3<i32>(1_i, 1_i, 1_i);
-    auto* rhs = vec3<i32>(1_i, 1_i, 1_i);
+    auto* lhs = Call<vec3<i32>>(1_i, 1_i, 1_i);
+    auto* rhs = Call<vec3<i32>>(1_i, 1_i, 1_i);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -159,8 +160,8 @@
         return;
     }
 
-    auto* lhs = vec3<u32>(1_u, 1_u, 1_u);
-    auto* rhs = vec3<u32>(1_u, 1_u, 1_u);
+    auto* lhs = Call<vec3<u32>>(1_u, 1_u, 1_u);
+    auto* rhs = Call<vec3<u32>>(1_u, 1_u, 1_u);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -220,8 +221,8 @@
 TEST_P(BinaryArithF32Test, Vector) {
     auto param = GetParam();
 
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -279,8 +280,8 @@
 
     auto param = GetParam();
 
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -334,8 +335,8 @@
 TEST_P(BinaryOperatorBoolTest, Vector) {
     auto param = GetParam();
 
-    auto* lhs = vec3<bool>(false, true, false);
-    auto* rhs = vec3<bool>(true, false, true);
+    auto* lhs = Call<vec3<bool>>(false, true, false);
+    auto* rhs = Call<vec3<bool>>(true, false, true);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -391,8 +392,8 @@
 TEST_P(BinaryCompareUnsignedIntegerTest, Vector) {
     auto param = GetParam();
 
-    auto* lhs = vec3<u32>(1_u, 1_u, 1_u);
-    auto* rhs = vec3<u32>(1_u, 1_u, 1_u);
+    auto* lhs = Call<vec3<u32>>(1_u, 1_u, 1_u);
+    auto* rhs = Call<vec3<u32>>(1_u, 1_u, 1_u);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -451,8 +452,8 @@
 TEST_P(BinaryCompareSignedIntegerTest, Vector) {
     auto param = GetParam();
 
-    auto* lhs = vec3<i32>(1_i, 1_i, 1_i);
-    auto* rhs = vec3<i32>(1_i, 1_i, 1_i);
+    auto* lhs = Call<vec3<i32>>(1_i, 1_i, 1_i);
+    auto* rhs = Call<vec3<i32>>(1_i, 1_i, 1_i);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -511,8 +512,8 @@
 TEST_P(BinaryCompareF32Test, Vector) {
     auto param = GetParam();
 
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -575,8 +576,8 @@
 
     auto param = GetParam();
 
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -608,7 +609,7 @@
                     BinaryData{ast::BinaryOp::kNotEqual, "OpFOrdNotEqual"}));
 
 TEST_F(BuilderTest, Binary_Multiply_VectorScalar_F32) {
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* rhs = Expr(1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -633,7 +634,7 @@
 TEST_F(BuilderTest, Binary_Multiply_VectorScalar_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
     auto* rhs = Expr(1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
@@ -657,7 +658,7 @@
 
 TEST_F(BuilderTest, Binary_Multiply_ScalarVector_F32) {
     auto* lhs = Expr(1_f);
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
 
@@ -682,7 +683,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* lhs = Expr(1_h);
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, rhs);
 
@@ -813,7 +814,7 @@
 
 TEST_F(BuilderTest, Binary_Multiply_MatrixVector_F32) {
     auto* var = Var("mat", ty.mat3x3<f32>());
-    auto* rhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* rhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, Expr("mat"), rhs);
 
     WrapInFunction(var, expr);
@@ -843,7 +844,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Var("mat", ty.mat3x3<f16>());
-    auto* rhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* rhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, Expr("mat"), rhs);
 
     WrapInFunction(var, expr);
@@ -871,7 +872,7 @@
 
 TEST_F(BuilderTest, Binary_Multiply_VectorMatrix_F32) {
     auto* var = Var("mat", ty.mat3x3<f32>());
-    auto* lhs = vec3<f32>(1_f, 1_f, 1_f);
+    auto* lhs = Call<vec3<f32>>(1_f, 1_f, 1_f);
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, Expr("mat"));
 
     WrapInFunction(var, expr);
@@ -901,7 +902,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Var("mat", ty.mat3x3<f16>());
-    auto* lhs = vec3<f16>(1_h, 1_h, 1_h);
+    auto* lhs = Call<vec3<f16>>(1_h, 1_h, 1_h);
 
     auto* expr = create<ast::BinaryExpression>(ast::BinaryOp::kMultiply, lhs, Expr("mat"));
 
@@ -1252,19 +1253,23 @@
     auto name = builder->Symbols().New();
     switch (type) {
         case Type::f32:
-            builder->GlobalVar(name, builder->ty.vec3<f32>(), builder->vec3<f32>(1_f, 1_f, 1_f),
+            builder->GlobalVar(name, builder->ty.vec3<f32>(),
+                               builder->Call<vec3<f32>>(1_f, 1_f, 1_f),
                                builtin::AddressSpace::kPrivate);
             break;
         case Type::f16:
-            builder->GlobalVar(name, builder->ty.vec3<f16>(), builder->vec3<f16>(1_h, 1_h, 1_h),
+            builder->GlobalVar(name, builder->ty.vec3<f16>(),
+                               builder->Call<vec3<f16>>(1_h, 1_h, 1_h),
                                builtin::AddressSpace::kPrivate);
             break;
         case Type::i32:
-            builder->GlobalVar(name, builder->ty.vec3<i32>(), builder->vec3<i32>(1_i, 1_i, 1_i),
+            builder->GlobalVar(name, builder->ty.vec3<i32>(),
+                               builder->Call<vec3<i32>>(1_i, 1_i, 1_i),
                                builtin::AddressSpace::kPrivate);
             break;
         case Type::u32:
-            builder->GlobalVar(name, builder->ty.vec3<u32>(), builder->vec3<u32>(1_u, 1_u, 1_u),
+            builder->GlobalVar(name, builder->ty.vec3<u32>(),
+                               builder->Call<vec3<u32>>(1_u, 1_u, 1_u),
                                builtin::AddressSpace::kPrivate);
             break;
     }
@@ -1581,11 +1586,11 @@
     auto name = builder->Symbols().New();
     switch (type) {
         case Type::f32:
-            builder->GlobalVar(name, builder->ty.mat3x4<f32>(), builder->mat3x4<f32>(),
+            builder->GlobalVar(name, builder->ty.mat3x4<f32>(), builder->Call<mat3x4<f32>>(),
                                builtin::AddressSpace::kPrivate);
             break;
         case Type::f16:
-            builder->GlobalVar(name, builder->ty.mat3x4<f16>(), builder->mat3x4<f16>(),
+            builder->GlobalVar(name, builder->ty.mat3x4<f16>(), builder->Call<mat3x4<f16>>(),
                                builtin::AddressSpace::kPrivate);
             break;
     }
@@ -1595,11 +1600,11 @@
     auto name = builder->Symbols().New();
     switch (type) {
         case Type::f32:
-            builder->GlobalVar(name, builder->ty.mat4x3<f32>(), builder->mat4x3<f32>(),
+            builder->GlobalVar(name, builder->ty.mat4x3<f32>(), builder->Call<mat4x3<f32>>(),
                                builtin::AddressSpace::kPrivate);
             break;
         case Type::f16:
-            builder->GlobalVar(name, builder->ty.mat4x3<f16>(), builder->mat4x3<f16>(),
+            builder->GlobalVar(name, builder->ty.mat4x3<f16>(), builder->Call<mat4x3<f16>>(),
                                builtin::AddressSpace::kPrivate);
     }
     return builder->Expr(name);
diff --git a/src/tint/writer/spirv/builder_builtin_test.cc b/src/tint/writer/spirv/builder_builtin_test.cc
index ba33a443..dd76407 100644
--- a/src/tint/writer/spirv/builder_builtin_test.cc
+++ b/src/tint/writer/spirv/builder_builtin_test.cc
@@ -20,11 +20,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuiltinBuilderTest = TestHelper;
 
 template <typename T>
@@ -47,8 +48,10 @@
     auto* tex = GlobalVar("texture", t, Binding(0_a), Group(0_a));
     auto* sampler = GlobalVar("sampler", s, Binding(1_a), Group(0_a));
 
-    auto* expr1 = Call("textureSampleCompare", "texture", "sampler", vec2<f32>(1_f, 2_f), 2_f);
-    auto* expr2 = Call("textureSampleCompare", "texture", "sampler", vec2<f32>(1_f, 2_f), 2_f);
+    auto* expr1 =
+        Call("textureSampleCompare", "texture", "sampler", Call<vec2<f32>>(1_f, 2_f), 2_f);
+    auto* expr2 =
+        Call("textureSampleCompare", "texture", "sampler", Call<vec2<f32>>(1_f, 2_f), 2_f);
 
     Func("f1", utils::Empty, ty.void_(),
          utils::Vector{
@@ -561,7 +564,7 @@
     auto param = GetParam();
 
     // Use a variable to prevent the function being evaluated as constant.
-    auto* vec = Var("a", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("a", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call(param.name, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -605,7 +608,7 @@
     auto param = GetParam();
 
     // Use a variable to prevent the function being evaluated as constant.
-    auto* vec = Var("a", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("a", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call(param.name, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -743,7 +746,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_Length_Vector_f32) {
-    auto* vec = Var("a", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("a", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call("length", vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -782,7 +785,7 @@
 TEST_F(BuiltinBuilderTest, Call_Length_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("a", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("a", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call("length", vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -819,7 +822,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_Normalize_f32) {
-    auto* vec = Var("a", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("a", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call("normalize", vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -858,7 +861,7 @@
 TEST_F(BuiltinBuilderTest, Call_Normalize_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("a", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("a", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call("normalize", vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -977,7 +980,7 @@
 
 TEST_P(Builtin_Builder_DualParam_Float_Test, Call_Vector_f32) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call(param.name, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1020,7 +1023,7 @@
     Enable(builtin::Extension::kF16);
 
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call(param.name, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1068,7 +1071,7 @@
                                          BuiltinData{"step", "Step"}));
 
 TEST_F(BuiltinBuilderTest, Call_Reflect_Vector_f32) {
-    auto* vec = Var("vec", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call("reflect", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1108,7 +1111,7 @@
 TEST_F(BuiltinBuilderTest, Call_Reflect_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call("reflect", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1220,7 +1223,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_Distance_Vector_f32) {
-    auto* vec = Var("vec", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call("distance", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1260,7 +1263,7 @@
 TEST_F(BuiltinBuilderTest, Call_Distance_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call("distance", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1298,7 +1301,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_Cross_f32) {
-    auto* vec = Var("vec", vec3<f32>(1_f, 1_f, 1_f));
+    auto* vec = Var("vec", Call<vec3<f32>>(1_f, 1_f, 1_f));
     auto* expr = Call("cross", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1338,7 +1341,7 @@
 TEST_F(BuiltinBuilderTest, Call_Cross_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec3<f16>(1_h, 1_h, 1_h));
+    auto* vec = Var("vec", Call<vec3<f16>>(1_h, 1_h, 1_h));
     auto* expr = Call("cross", vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1460,7 +1463,7 @@
 
 TEST_P(Builtin_Builder_ThreeParam_Float_Test, Call_Vector_f32) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call(param.name, vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1504,7 +1507,7 @@
     Enable(builtin::Extension::kF16);
 
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call(param.name, vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1553,7 +1556,7 @@
                                          BuiltinData{"smoothstep", "SmoothStep"}));
 
 TEST_F(BuiltinBuilderTest, Call_FaceForward_Vector_f32) {
-    auto* vec = Var("vec", vec2<f32>(1_f, 1_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 1_f));
     auto* expr = Call("faceForward", vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1594,7 +1597,7 @@
 TEST_F(BuiltinBuilderTest, Call_FaceForward_Vector_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec2<f16>(1_h, 1_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 1_h));
     auto* expr = Call("faceForward", vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -1633,7 +1636,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Runtime_Call_Modf_f32) {
-    auto* vec = Var("vec", vec2<f32>(1_f, 2_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 2_f));
     auto* expr = Call("modf", vec);
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -1687,7 +1690,7 @@
 TEST_F(BuiltinBuilderTest, Runtime_Call_Modf_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec2<f16>(1_h, 2_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 2_h));
     auto* expr = Call("modf", vec);
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -1743,7 +1746,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Const_Call_Modf_f32) {
-    auto* expr = Call("modf", vec2<f32>(1_f, 2_f));
+    auto* expr = Call("modf", Call<vec2<f32>>(1_f, 2_f));
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", expr)),
@@ -1789,7 +1792,7 @@
 TEST_F(BuiltinBuilderTest, Const_Call_Modf_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* expr = Call("modf", vec2<f16>(1_h, 2_h));
+    auto* expr = Call("modf", Call<vec2<f16>>(1_h, 2_h));
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(Let("l", expr)),
@@ -1837,7 +1840,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Runtime_Call_Frexp_f32) {
-    auto* vec = Var("vec", vec2<f32>(1_f, 2_f));
+    auto* vec = Var("vec", Call<vec2<f32>>(1_f, 2_f));
     auto* expr = Call("frexp", vec);
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -1893,7 +1896,7 @@
 TEST_F(BuiltinBuilderTest, Runtime_Call_Frexp_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* vec = Var("vec", vec2<f16>(1_h, 2_h));
+    auto* vec = Var("vec", Call<vec2<f16>>(1_h, 2_h));
     auto* expr = Call("frexp", vec);
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -1953,7 +1956,7 @@
 TEST_F(BuiltinBuilderTest, Const_Call_Frexp_f32) {
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Let("l", Call("frexp", vec2<f32>(1_f, 2_f)))),
+             Decl(Let("l", Call("frexp", Call<vec2<f32>>(1_f, 2_f)))),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -2001,7 +2004,7 @@
 
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
-             Decl(Let("l", Call("frexp", vec2<f16>(1_h, 2_h)))),
+             Decl(Let("l", Call("frexp", Call<vec2<f16>>(1_h, 2_h)))),
          },
          utils::Vector{
              Stage(ast::PipelineStage::kFragment),
@@ -2088,7 +2091,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_QuantizeToF16_Vector) {
-    GlobalVar("v", vec3<f32>(2_f), builtin::AddressSpace::kPrivate);
+    GlobalVar("v", Call<vec3<f32>>(2_f), builtin::AddressSpace::kPrivate);
 
     Func("a_func", utils::Empty, ty.void_(),
          utils::Vector{
@@ -2320,7 +2323,7 @@
 
 TEST_P(Builtin_Builder_SingleParam_Sint_Test, Call_Vector) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<i32>(1_i, 1_i));
+    auto* vec = Var("vec", Call<vec2<i32>>(1_i, 1_i));
     auto* expr = Call(param.name, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -2397,7 +2400,7 @@
 }
 
 TEST_F(Builtin_Builder_Abs_Uint_Test, Call_Vector) {
-    auto* scalar = Var("scalar", vec2<u32>(1_u, 1_u));
+    auto* scalar = Var("scalar", Call<vec2<u32>>(1_u, 1_u));
     auto* expr = Call("abs", scalar);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -2473,7 +2476,7 @@
 
 TEST_P(Builtin_Builder_DualParam_SInt_Test, Call_Vector) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<i32>(1_i, 1_i));
+    auto* vec = Var("vec", Call<vec2<i32>>(1_i, 1_i));
     auto* expr = Call(param.name, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -2557,7 +2560,7 @@
 
 TEST_P(Builtin_Builder_DualParam_UInt_Test, Call_Vector) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<u32>(1_u, 1_u));
+    auto* vec = Var("vec", Call<vec2<u32>>(1_u, 1_u));
     auto* expr = Call(param.name, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -2642,7 +2645,7 @@
 
 TEST_P(Builtin_Builder_ThreeParam_Sint_Test, Call_Vector) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<i32>(1_i, 1_i));
+    auto* vec = Var("vec", Call<vec2<i32>>(1_i, 1_i));
     auto* expr = Call(param.name, vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -2728,7 +2731,7 @@
 
 TEST_P(Builtin_Builder_ThreeParam_Uint_Test, Call_Vector) {
     auto param = GetParam();
-    auto* vec = Var("vec", vec2<u32>(1_u, 1_u));
+    auto* vec = Var("vec", Call<vec2<u32>>(1_u, 1_u));
     auto* expr = Call(param.name, vec, vec, vec);
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
@@ -3945,7 +3948,8 @@
     auto param = GetParam();
 
     bool pack4 = param.name == "pack4x8snorm" || param.name == "pack4x8unorm";
-    auto* call = pack4 ? Call(param.name, vec4<f32>("one")) : Call(param.name, vec2<f32>("one"));
+    auto* call =
+        pack4 ? Call(param.name, Call<vec4<f32>>("one")) : Call(param.name, Call<vec2<f32>>("one"));
     auto* func = Func("a_func", utils::Empty, ty.void_(),
                       utils::Vector{
                           Decl(Let("one", Expr(1_f))),
diff --git a/src/tint/writer/spirv/builder_constructor_expression_test.cc b/src/tint/writer/spirv/builder_constructor_expression_test.cc
index 7416bc2..b1eb366 100644
--- a/src/tint/writer/spirv/builder_constructor_expression_test.cc
+++ b/src/tint/writer/spirv/builder_constructor_expression_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using SpvBuilderConstructorTest = TestHelper;
 
 TEST_F(SpvBuilderConstructorTest, Const) {
@@ -37,7 +38,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type) {
-    auto* t = vec3<f32>(1_f, 1_f, 3_f);
+    auto* t = Call<vec3<f32>>(1_f, 1_f, 3_f);
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -54,7 +55,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_WithCasts) {
-    auto* t = vec2<f32>(Call<f32>(1_i), Call<f32>(1_i));
+    auto* t = Call<vec2<f32>>(Call<f32>(1_i), Call<f32>(1_i));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -94,7 +95,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_IdentifierExpression_Param) {
     auto* var = Var("ident", ty.f32());
 
-    auto* t = vec2<f32>(1_f, "ident");
+    auto* t = Call<vec2<f32>>(1_f, "ident");
     WrapInFunction(var, t);
 
     spirv::Builder& b = Build();
@@ -122,7 +123,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Vector_Bitcast_Params) {
-    auto* var = Var("v", vec3<f32>(1_f, 2_f, 3_f));
+    auto* var = Var("v", Call<vec3<f32>>(1_f, 2_f, 3_f));
     auto* cast = Bitcast(ty.vec3<u32>(), var);
     WrapInFunction(var, cast);
 
@@ -230,7 +231,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_Bool_Literal) {
-    auto* cast = vec2<bool>(true);
+    auto* cast = Call<vec2<bool>>(true);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -248,7 +249,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_Bool_Var) {
     auto* var = Var("v", Expr(true));
-    auto* cast = vec2<bool>(var);
+    auto* cast = Call<vec2<bool>>(var);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -271,7 +272,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_F32_Literal) {
-    auto* cast = vec2<f32>(2_f);
+    auto* cast = Call<vec2<f32>>(2_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -290,7 +291,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_F16_Literal) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(2_h);
+    auto* cast = Call<vec2<f16>>(2_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -308,7 +309,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_F32_F32) {
     auto* var = Decl(Var("x", ty.f32(), Expr(2_f)));
-    auto* cast = vec2<f32>("x", "x");
+    auto* cast = Call<vec2<f32>>("x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -335,7 +336,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Decl(Var("x", ty.f16(), Expr(2_h)));
-    auto* cast = vec2<f16>("x", "x");
+    auto* cast = Call<vec2<f16>>("x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -359,7 +360,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_F32_F32_Const) {
-    auto* cast = vec2<f32>(1_f, 2_f);
+    auto* cast = Call<vec2<f32>>(1_f, 2_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -379,7 +380,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_With_F16_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(1_h, 2_h);
+    auto* cast = Call<vec2<f16>>(1_h, 2_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -397,8 +398,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_F32_With_Vec2) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(1_f, 2_f)));
-    auto* cast = vec2<f32>("x");
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(1_f, 2_f)));
+    auto* cast = Call<vec2<f32>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -424,8 +425,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_F16_With_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(1_h, 2_h)));
-    auto* cast = vec2<f16>("x");
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(1_h, 2_h)));
+    auto* cast = Call<vec2<f16>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -449,7 +450,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_F32_With_Vec2_Const) {
-    auto* cast = vec2<f32>(vec2<f32>(1_f, 2_f));
+    auto* cast = Call<vec2<f32>>(Call<vec2<f32>>(1_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -469,7 +470,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec2_F16_With_Vec2_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(vec2<f16>(1_h, 2_h));
+    auto* cast = Call<vec2<f16>>(Call<vec2<f16>>(1_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -488,7 +489,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32) {
     auto* var = Decl(Var("x", ty.f32(), Expr(2_f)));
-    auto* cast = vec3<f32>("x", "x", "x");
+    auto* cast = Call<vec3<f32>>("x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -516,7 +517,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Decl(Var("x", ty.f16(), Expr(2_h)));
-    auto* cast = vec3<f16>("x", "x", "x");
+    auto* cast = Call<vec3<f16>>("x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -541,7 +542,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32_Const) {
-    auto* cast = vec3<f32>(1_f, 2_f, 3_f);
+    auto* cast = Call<vec3<f32>>(1_f, 2_f, 3_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -562,7 +563,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(1_h, 2_h, 3_h);
+    auto* cast = Call<vec3<f16>>(1_h, 2_h, 3_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -582,7 +583,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Bool) {
     auto* var = Decl(Var("x", ty.bool_(), Expr(true)));
-    auto* cast = vec3<bool>("x", "x", "x");
+    auto* cast = Call<vec3<bool>>("x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -607,7 +608,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Bool_Const) {
-    auto* cast = vec3<bool>(true, false, true);
+    auto* cast = Call<vec3<bool>>(true, false, true);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -626,7 +627,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32_F32_F32) {
     auto* var = Decl(Var("x", ty.f32(), Expr(2_f)));
-    auto* cast = vec3<f32>("x", "x", "x");
+    auto* cast = Call<vec3<f32>>("x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -654,7 +655,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Decl(Var("x", ty.f16(), Expr(2_h)));
-    auto* cast = vec3<f16>("x", "x", "x");
+    auto* cast = Call<vec3<f16>>("x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -679,7 +680,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32_F32_F32_Const) {
-    auto* cast = vec3<f32>(1_f, 2_f, 3_f);
+    auto* cast = Call<vec3<f32>>(1_f, 2_f, 3_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -700,7 +701,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F16_F16_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(1_h, 2_h, 3_h);
+    auto* cast = Call<vec3<f16>>(1_h, 2_h, 3_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -719,8 +720,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32_Vec2) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(2_f, 3_f)));
-    auto* cast = vec3<f32>(1_f, "x");
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(2_f, 3_f)));
+    auto* cast = Call<vec3<f32>>(1_f, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -751,8 +752,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(2_h, 3_h)));
-    auto* cast = vec3<f16>(1_h, "x");
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(2_h, 3_h)));
+    auto* cast = Call<vec3<f16>>(1_h, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -781,7 +782,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F32_Vec2_Const) {
-    auto* cast = vec3<f32>(1_f, vec2<f32>(2_f, 3_f));
+    auto* cast = Call<vec3<f32>>(1_f, Call<vec2<f32>>(2_f, 3_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -802,7 +803,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_F16_Vec2_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(1_h, vec2<f16>(2_h, 3_h));
+    auto* cast = Call<vec3<f16>>(1_h, Call<vec2<f16>>(2_h, 3_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -821,8 +822,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Vec2_F32) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(1_f, 2_f)));
-    auto* cast = vec3<f32>("x", 3_f);
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(1_f, 2_f)));
+    auto* cast = Call<vec3<f32>>("x", 3_f);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -853,8 +854,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(1_h, 2_h)));
-    auto* cast = vec3<f16>("x", 3_h);
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(1_h, 2_h)));
+    auto* cast = Call<vec3<f16>>("x", 3_h);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -883,7 +884,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Vec2_F32_Const) {
-    auto* cast = vec3<f32>(vec2<f32>(1_f, 2_f), 3_f);
+    auto* cast = Call<vec3<f32>>(Call<vec2<f32>>(1_f, 2_f), 3_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -904,7 +905,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_With_Vec2_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec2<f16>(1_h, 2_h), 3_h);
+    auto* cast = Call<vec3<f16>>(Call<vec2<f16>>(1_h, 2_h), 3_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -923,8 +924,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_F32_With_Vec3) {
-    auto* var = Decl(Var("x", ty.vec3<f32>(), vec3<f32>(1_f, 2_f, 3_f)));
-    auto* cast = vec3<f32>("x");
+    auto* var = Decl(Var("x", ty.vec3<f32>(), Call<vec3<f32>>(1_f, 2_f, 3_f)));
+    auto* cast = Call<vec3<f32>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -951,8 +952,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_F16_With_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec3<f16>(), vec3<f16>(1_h, 2_h, 3_h)));
-    auto* cast = vec3<f16>("x");
+    auto* var = Decl(Var("x", ty.vec3<f16>(), Call<vec3<f16>>(1_h, 2_h, 3_h)));
+    auto* cast = Call<vec3<f16>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -977,7 +978,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_F32_With_Vec3_Const) {
-    auto* cast = vec3<f32>(vec3<f32>(1_f, 2_f, 3_f));
+    auto* cast = Call<vec3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -998,7 +999,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec3_F16_With_Vec3_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec3<f16>(1_h, 2_h, 3_h));
+    auto* cast = Call<vec3<f16>>(Call<vec3<f16>>(1_h, 2_h, 3_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1018,7 +1019,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Bool) {
     auto* var = Decl(Var("x", ty.bool_(), Expr(true)));
-    auto* cast = vec4<bool>("x");
+    auto* cast = Call<vec4<bool>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1041,7 +1042,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Bool_Const) {
-    auto* cast = vec4<bool>(true);
+    auto* cast = Call<vec4<bool>>(true);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1059,7 +1060,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32) {
     auto* var = Decl(Var("x", ty.f32(), Expr(2_f)));
-    auto* cast = vec4<f32>("x");
+    auto* cast = Call<vec4<f32>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1085,7 +1086,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Decl(Var("x", ty.f16(), Expr(2_h)));
-    auto* cast = vec4<f16>("x");
+    auto* cast = Call<vec4<f16>>("x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1108,7 +1109,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_Const) {
-    auto* cast = vec4<f32>(2_f);
+    auto* cast = Call<vec4<f32>>(2_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1127,7 +1128,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h);
+    auto* cast = Call<vec4<f16>>(2_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1145,7 +1146,7 @@
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_F32_F32_F32) {
     auto* var = Decl(Var("x", ty.f32(), Expr(2_f)));
-    auto* cast = vec4<f32>("x", "x", "x", "x");
+    auto* cast = Call<vec4<f32>>("x", "x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1174,7 +1175,7 @@
     Enable(builtin::Extension::kF16);
 
     auto* var = Decl(Var("x", ty.f16(), Expr(2_h)));
-    auto* cast = vec4<f16>("x", "x", "x", "x");
+    auto* cast = Call<vec4<f16>>("x", "x", "x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1200,7 +1201,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_F32_F32_F32_Const) {
-    auto* cast = vec4<f32>(1_f, 2_f, 3_f, 4_f);
+    auto* cast = Call<vec4<f32>>(1_f, 2_f, 3_f, 4_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1222,7 +1223,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_F16_F16_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(1_h, 2_h, 3_h, 4_h);
+    auto* cast = Call<vec4<f16>>(1_h, 2_h, 3_h, 4_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1242,8 +1243,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_F32_Vec2) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(1_f, 2_f)));
-    auto* cast = vec4<f32>(1_f, 2_f, "x");
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(1_f, 2_f)));
+    auto* cast = Call<vec4<f32>>(1_f, 2_f, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1273,8 +1274,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(1_h, 2_h)));
-    auto* cast = vec4<f16>(1_h, 2_h, "x");
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(1_h, 2_h)));
+    auto* cast = Call<vec4<f16>>(1_h, 2_h, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1302,7 +1303,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_F32_Vec2_Const) {
-    auto* cast = vec4<f32>(1_f, 2_f, vec2<f32>(3_f, 4_f));
+    auto* cast = Call<vec4<f32>>(1_f, 2_f, Call<vec2<f32>>(3_f, 4_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1324,7 +1325,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_F16_Vec2_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(1_h, 2_h, vec2<f16>(3_h, 4_h));
+    auto* cast = Call<vec4<f16>>(1_h, 2_h, Call<vec2<f16>>(3_h, 4_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1344,8 +1345,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_Vec2_F32) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(2_f, 3_f)));
-    auto* cast = vec4<f32>(1_f, "x", 4_f);
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(2_f, 3_f)));
+    auto* cast = Call<vec4<f32>>(1_f, "x", 4_f);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1377,8 +1378,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(2_h, 3_h)));
-    auto* cast = vec4<f16>(1_h, "x", 4_h);
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(2_h, 3_h)));
+    auto* cast = Call<vec4<f16>>(1_h, "x", 4_h);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1408,7 +1409,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_Vec2_F32_Const) {
-    auto* cast = vec4<f32>(1_f, vec2<f32>(2_f, 3_f), 4_f);
+    auto* cast = Call<vec4<f32>>(1_f, Call<vec2<f32>>(2_f, 3_f), 4_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1430,7 +1431,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_Vec2_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(1_h, vec2<f16>(2_h, 3_h), 4_h);
+    auto* cast = Call<vec4<f16>>(1_h, Call<vec2<f16>>(2_h, 3_h), 4_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1450,8 +1451,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec2_F32_F32) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(1_f, 2_f)));
-    auto* cast = vec4<f32>("x", 3_f, 4_f);
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(1_f, 2_f)));
+    auto* cast = Call<vec4<f32>>("x", 3_f, 4_f);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1483,8 +1484,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec2_F16_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(1_h, 2_h)));
-    auto* cast = vec4<f16>("x", 3_h, 4_h);
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(1_h, 2_h)));
+    auto* cast = Call<vec4<f16>>("x", 3_h, 4_h);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1514,7 +1515,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec2_F32_F32_Const) {
-    auto* cast = vec4<f32>(vec2<f32>(1_f, 2_f), 3_f, 4_f);
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(1_f, 2_f), 3_f, 4_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1536,7 +1537,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec2_F16_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(1_h, 2_h), 3_h, 4_h);
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(1_h, 2_h), 3_h, 4_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1556,8 +1557,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F32_With_Vec2_Vec2) {
-    auto* var = Decl(Var("x", ty.vec2<f32>(), vec2<f32>(1_f, 2_f)));
-    auto* cast = vec4<f32>("x", "x");
+    auto* var = Decl(Var("x", ty.vec2<f32>(), Call<vec2<f32>>(1_f, 2_f)));
+    auto* cast = Call<vec4<f32>>("x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1590,8 +1591,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F16_With_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec2<f16>(), vec2<f16>(1_h, 2_h)));
-    auto* cast = vec4<f16>("x", "x");
+    auto* var = Decl(Var("x", ty.vec2<f16>(), Call<vec2<f16>>(1_h, 2_h)));
+    auto* cast = Call<vec4<f16>>("x", "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1622,7 +1623,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F32_With_Vec2_Vec2_Const) {
-    auto* cast = vec4<f32>(vec2<f32>(1_f, 2_f), vec2<f32>(1_f, 2_f));
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(1_f, 2_f), Call<vec2<f32>>(1_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1642,7 +1643,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F16_With_Vec2_Vec2_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(1_h, 2_h), vec2<f16>(1_h, 2_h));
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(1_h, 2_h), Call<vec2<f16>>(1_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1660,8 +1661,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_Vec3) {
-    auto* var = Decl(Var("x", ty.vec3<f32>(), vec3<f32>(2_f, 2_f, 2_f)));
-    auto* cast = vec4<f32>(2_f, "x");
+    auto* var = Decl(Var("x", ty.vec3<f32>(), Call<vec3<f32>>(2_f, 2_f, 2_f)));
+    auto* cast = Call<vec4<f32>>(2_f, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1691,8 +1692,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec3<f16>(), vec3<f16>(2_h, 2_h, 2_h)));
-    auto* cast = vec4<f16>(2_h, "x");
+    auto* var = Decl(Var("x", ty.vec3<f16>(), Call<vec3<f16>>(2_h, 2_h, 2_h)));
+    auto* cast = Call<vec4<f16>>(2_h, "x");
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1720,7 +1721,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F32_Vec3_Const) {
-    auto* cast = vec4<f32>(2_f, vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(2_f, Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1739,7 +1740,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_F16_Vec3_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(2_h, Call<vec3<f16>>(2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1756,8 +1757,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec3_F32) {
-    auto* var = Decl(Var("x", ty.vec3<f32>(), vec3<f32>(2_f, 2_f, 2_f)));
-    auto* cast = vec4<f32>("x", 2_f);
+    auto* var = Decl(Var("x", ty.vec3<f32>(), Call<vec3<f32>>(2_f, 2_f, 2_f)));
+    auto* cast = Call<vec4<f32>>("x", 2_f);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1787,8 +1788,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec3_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* var = Decl(Var("x", ty.vec3<f16>(), vec3<f16>(2_h, 2_h, 2_h)));
-    auto* cast = vec4<f16>("x", 2_h);
+    auto* var = Decl(Var("x", ty.vec3<f16>(), Call<vec3<f16>>(2_h, 2_h, 2_h)));
+    auto* cast = Call<vec4<f16>>("x", 2_h);
     WrapInFunction(var, cast);
 
     spirv::Builder& b = Build();
@@ -1816,7 +1817,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec3_F32_Const) {
-    auto* cast = vec4<f32>(vec3<f32>(2_f, 2_f, 2_f), 2_f);
+    auto* cast = Call<vec4<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), 2_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1835,7 +1836,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_With_Vec3_F16_Const) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec3<f16>(2_h, 2_h, 2_h), 2_h);
+    auto* cast = Call<vec4<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), 2_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1852,8 +1853,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F32_With_Vec4) {
-    auto* value = vec4<f32>(2_f, 2_f, 2_f, 2_f);
-    auto* cast = vec4<f32>(value);
+    auto* value = Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f);
+    auto* cast = Call<vec4<f32>>(value);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -1872,8 +1873,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Vec4_F16_With_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* value = vec4<f16>(2_h, 2_h, 2_h, 2_h);
-    auto* cast = vec4<f16>(value);
+    auto* value = Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h);
+    auto* cast = Call<vec4<f16>>(value);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -2050,7 +2051,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec2_With_F32) {
-    auto* cast = vec2<f32>(2_f);
+    auto* cast = Call<vec2<f32>>(2_f);
     GlobalConst("g", ty.vec2<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2075,7 +2076,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec2_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(2_h);
+    auto* cast = Call<vec2<f16>>(2_h);
     GlobalConst("g", ty.vec2<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2098,7 +2099,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec2_With_F32) {
-    auto* cast = vec2<f32>(2_f);
+    auto* cast = Call<vec2<f32>>(2_f);
     auto* g = GlobalVar("g", ty.vec2<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2116,7 +2117,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec2_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(2_h);
+    auto* cast = Call<vec2<f16>>(2_h);
     auto* g = GlobalVar("g", ty.vec2<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2132,7 +2133,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec2_F32_With_Vec2) {
-    auto* cast = vec2<f32>(vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec2<f32>>(Call<vec2<f32>>(2_f, 2_f));
     GlobalConst("g", ty.vec2<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2157,7 +2158,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec2_F16_With_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec2<f16>>(Call<vec2<f16>>(2_h, 2_h));
     GlobalConst("g", ty.vec2<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2180,7 +2181,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec2_F32_With_Vec2) {
-    auto* cast = vec2<f32>(vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec2<f32>>(Call<vec2<f32>>(2_f, 2_f));
     GlobalVar("a", ty.vec2<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2202,7 +2203,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec2_F16_With_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec2<f16>(vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec2<f16>>(Call<vec2<f16>>(2_h, 2_h));
     GlobalVar("a", ty.vec2<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2222,7 +2223,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_F32_With_Vec3) {
-    auto* cast = vec3<f32>(vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<vec3<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f));
     GlobalConst("g", ty.vec3<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2247,7 +2248,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_F16_With_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<vec3<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h));
     GlobalConst("g", ty.vec3<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2270,7 +2271,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_F32_With_Vec3) {
-    auto* cast = vec3<f32>(vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<vec3<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f));
     GlobalVar("a", ty.vec3<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2292,7 +2293,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_F16_With_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<vec3<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h));
     GlobalVar("a", ty.vec3<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2312,7 +2313,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_F32_With_Vec4) {
-    auto* cast = vec4<f32>(vec4<f32>(2_f, 2_f, 2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f));
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2337,7 +2338,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_F16_With_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec4<f16>(2_h, 2_h, 2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h));
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2360,7 +2361,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_F32_With_Vec4) {
-    auto* cast = vec4<f32>(vec4<f32>(2_f, 2_f, 2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f));
     GlobalVar("a", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2382,7 +2383,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_F16_With_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec4<f16>(2_h, 2_h, 2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h));
     GlobalVar("a", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -2402,7 +2403,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_F32) {
-    auto* cast = vec3<f32>(2_f);
+    auto* cast = Call<vec3<f32>>(2_f);
     GlobalConst("g", ty.vec3<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2427,7 +2428,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(2_h);
+    auto* cast = Call<vec3<f16>>(2_h);
     GlobalConst("g", ty.vec3<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2450,7 +2451,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_F32) {
-    auto* cast = vec3<f32>(2_f);
+    auto* cast = Call<vec3<f32>>(2_f);
     auto* g = GlobalVar("g", ty.vec3<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2468,7 +2469,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(2_h);
+    auto* cast = Call<vec3<f16>>(2_h);
     auto* g = GlobalVar("g", ty.vec3<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2484,7 +2485,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_F32_Vec2) {
-    auto* cast = vec3<f32>(2_f, vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec3<f32>>(2_f, Call<vec2<f32>>(2_f, 2_f));
     GlobalConst("g", ty.vec3<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2509,7 +2510,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(2_h, vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec3<f16>>(2_h, Call<vec2<f16>>(2_h, 2_h));
     GlobalConst("g", ty.vec3<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2532,7 +2533,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_F32_Vec2) {
-    auto* cast = vec3<f32>(2_f, vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec3<f32>>(2_f, Call<vec2<f32>>(2_f, 2_f));
     auto* g = GlobalVar("g", ty.vec3<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2550,7 +2551,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(2_h, vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec3<f16>>(2_h, Call<vec2<f16>>(2_h, 2_h));
     auto* g = GlobalVar("g", ty.vec3<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2566,7 +2567,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_Vec2_F32) {
-    auto* cast = vec3<f32>(vec2<f32>(2_f, 2_f), 2_f);
+    auto* cast = Call<vec3<f32>>(Call<vec2<f32>>(2_f, 2_f), 2_f);
     GlobalConst("g", ty.vec3<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2591,7 +2592,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec3_With_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec2<f16>(2_h, 2_h), 2_h);
+    auto* cast = Call<vec3<f16>>(Call<vec2<f16>>(2_h, 2_h), 2_h);
     GlobalConst("g", ty.vec3<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2614,7 +2615,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_Vec2_F32) {
-    auto* cast = vec3<f32>(vec2<f32>(2_f, 2_f), 2_f);
+    auto* cast = Call<vec3<f32>>(Call<vec2<f32>>(2_f, 2_f), 2_f);
     auto* g = GlobalVar("g", ty.vec3<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2632,7 +2633,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec3_With_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec2<f16>(2_h, 2_h), 2_h);
+    auto* cast = Call<vec3<f16>>(Call<vec2<f16>>(2_h, 2_h), 2_h);
     auto* g = GlobalVar("g", ty.vec3<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2648,7 +2649,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F32) {
-    auto* cast = vec4<f32>(2_f);
+    auto* cast = Call<vec4<f32>>(2_f);
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2673,7 +2674,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h);
+    auto* cast = Call<vec4<f16>>(2_h);
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2696,7 +2697,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F32) {
-    auto* cast = vec4<f32>(2_f);
+    auto* cast = Call<vec4<f32>>(2_f);
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2714,7 +2715,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h);
+    auto* cast = Call<vec4<f16>>(2_h);
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2730,7 +2731,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F32_F32_Vec2) {
-    auto* cast = vec4<f32>(2_f, 2_f, vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(2_f, 2_f, Call<vec2<f32>>(2_f, 2_f));
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2755,7 +2756,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F16_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, 2_h, vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(2_h, 2_h, Call<vec2<f16>>(2_h, 2_h));
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2778,7 +2779,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F32_F32_Vec2) {
-    auto* cast = vec4<f32>(2_f, 2_f, vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(2_f, 2_f, Call<vec2<f32>>(2_f, 2_f));
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2796,7 +2797,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F16_F16_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, 2_h, vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(2_h, 2_h, Call<vec2<f16>>(2_h, 2_h));
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2812,7 +2813,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F32_Vec2_F32) {
-    auto* cast = vec4<f32>(2_f, vec2<f32>(2_f, 2_f), 2_f);
+    auto* cast = Call<vec4<f32>>(2_f, Call<vec2<f32>>(2_f, 2_f), 2_f);
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2837,7 +2838,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F16_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, vec2<f16>(2_h, 2_h), 2_h);
+    auto* cast = Call<vec4<f16>>(2_h, Call<vec2<f16>>(2_h, 2_h), 2_h);
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2860,7 +2861,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F32_Vec2_F32) {
-    auto* cast = vec4<f32>(2_f, vec2<f32>(2_f, 2_f), 2_f);
+    auto* cast = Call<vec4<f32>>(2_f, Call<vec2<f32>>(2_f, 2_f), 2_f);
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2878,7 +2879,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F16_Vec2_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, vec2<f16>(2_h, 2_h), 2_h);
+    auto* cast = Call<vec4<f16>>(2_h, Call<vec2<f16>>(2_h, 2_h), 2_h);
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2894,7 +2895,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_Vec2_F32_F32) {
-    auto* cast = vec4<f32>(vec2<f32>(2_f, 2_f), 2_f, 2_f);
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(2_f, 2_f), 2_f, 2_f);
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2919,7 +2920,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_Vec2_F16_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(2_h, 2_h), 2_h, 2_h);
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(2_h, 2_h), 2_h, 2_h);
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -2942,7 +2943,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_Vec2_F32_F32) {
-    auto* cast = vec4<f32>(vec2<f32>(2_f, 2_f), 2_f, 2_f);
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(2_f, 2_f), 2_f, 2_f);
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2960,7 +2961,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_Vec2_F16_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(2_h, 2_h), 2_h, 2_h);
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(2_h, 2_h), 2_h, 2_h);
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -2976,7 +2977,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_F32_With_Vec2_Vec2) {
-    auto* cast = vec4<f32>(vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f));
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -3001,7 +3002,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_F16_With_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h));
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -3024,7 +3025,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_F32_With_Vec2_Vec2) {
-    auto* cast = vec4<f32>(vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f));
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3042,7 +3043,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_F16_With_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h));
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3058,7 +3059,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_F32_Vec3) {
-    auto* cast = vec4<f32>(2_f, vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(2_f, Call<vec3<f32>>(2_f, 2_f, 2_f));
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -3081,7 +3082,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F32_Vec3) {
-    auto* cast = vec4<f32>(2_f, vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<vec4<f32>>(2_f, Call<vec3<f32>>(2_f, 2_f, 2_f));
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3099,7 +3100,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_F16_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(2_h, vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<vec4<f16>>(2_h, Call<vec3<f16>>(2_h, 2_h, 2_h));
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3115,7 +3116,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_Vec3_F32) {
-    auto* cast = vec4<f32>(vec3<f32>(2_f, 2_f, 2_f), 2_f);
+    auto* cast = Call<vec4<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), 2_f);
     GlobalConst("g", ty.vec4<f32>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -3140,7 +3141,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalConst_Vec4_With_Vec3_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec3<f16>(2_h, 2_h, 2_h), 2_h);
+    auto* cast = Call<vec4<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), 2_h);
     GlobalConst("g", ty.vec4<f16>(), cast);
     WrapInFunction(Decl(Var("l", Expr("g"))));
 
@@ -3163,7 +3164,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_Vec3_F32) {
-    auto* cast = vec4<f32>(vec3<f32>(2_f, 2_f, 2_f), 2_f);
+    auto* cast = Call<vec4<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), 2_f);
     auto* g = GlobalVar("g", ty.vec4<f32>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3181,7 +3182,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_GlobalVar_Vec4_With_Vec3_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec4<f16>(vec3<f16>(2_h, 2_h, 2_h), 2_h);
+    auto* cast = Call<vec4<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), 2_h);
     auto* g = GlobalVar("g", ty.vec4<f16>(), builtin::AddressSpace::kPrivate, cast);
 
     spirv::Builder& b = Build();
@@ -3197,7 +3198,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x2_F32_With_Vec2_Vec2) {
-    auto* cast = mat2x2<f32>(vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f));
+    auto* cast = Call<mat2x2<f32>>(Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3217,7 +3218,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x2_F16_With_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat2x2<f16>(vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h));
+    auto* cast = Call<mat2x2<f16>>(Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3235,7 +3236,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x2_F32_With_Vec2_Vec2_Vec2) {
-    auto* cast = mat3x2<f32>(vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f));
+    auto* cast = Call<mat3x2<f32>>(Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f),
+                                   Call<vec2<f32>>(2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3255,7 +3257,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x2_F16_With_Vec2_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat3x2<f16>(vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h));
+    auto* cast = Call<mat3x2<f16>>(Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h),
+                                   Call<vec2<f16>>(2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3273,8 +3276,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x2_F32_With_Vec2_Vec2_Vec2_Vec2) {
-    auto* cast = mat4x2<f32>(vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f), vec2<f32>(2_f, 2_f),
-                             vec2<f32>(2_f, 2_f));
+    auto* cast = Call<mat4x2<f32>>(Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f),
+                                   Call<vec2<f32>>(2_f, 2_f), Call<vec2<f32>>(2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3294,8 +3297,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x2_F16_With_Vec2_Vec2_Vec2_Vec2) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat4x2<f16>(vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h), vec2<f16>(2_h, 2_h),
-                             vec2<f16>(2_h, 2_h));
+    auto* cast = Call<mat4x2<f16>>(Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h),
+                                   Call<vec2<f16>>(2_h, 2_h), Call<vec2<f16>>(2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3313,7 +3316,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x3_F32_With_Vec3_Vec3) {
-    auto* cast = mat2x3<f32>(vec3<f32>(2_f, 2_f, 2_f), vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<mat2x3<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3333,7 +3336,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x3_F16_With_Vec3_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat2x3<f16>(vec3<f16>(2_h, 2_h, 2_h), vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<mat2x3<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), Call<vec3<f16>>(2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3351,8 +3354,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x3_F32_With_Vec3_Vec3_Vec3) {
-    auto* cast =
-        mat3x3<f32>(vec3<f32>(2_f, 2_f, 2_f), vec3<f32>(2_f, 2_f, 2_f), vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<mat3x3<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), Call<vec3<f32>>(2_f, 2_f, 2_f),
+                                   Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3372,8 +3375,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x3_F16_With_Vec3_Vec3_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast =
-        mat3x3<f16>(vec3<f16>(2_h, 2_h, 2_h), vec3<f16>(2_h, 2_h, 2_h), vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<mat3x3<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), Call<vec3<f16>>(2_h, 2_h, 2_h),
+                                   Call<vec3<f16>>(2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3391,8 +3394,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x3_F32_With_Vec3_Vec3_Vec3_Vec3) {
-    auto* cast = mat4x3<f32>(vec3<f32>(2_f, 2_f, 2_f), vec3<f32>(2_f, 2_f, 2_f),
-                             vec3<f32>(2_f, 2_f, 2_f), vec3<f32>(2_f, 2_f, 2_f));
+    auto* cast = Call<mat4x3<f32>>(Call<vec3<f32>>(2_f, 2_f, 2_f), Call<vec3<f32>>(2_f, 2_f, 2_f),
+                                   Call<vec3<f32>>(2_f, 2_f, 2_f), Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3412,8 +3415,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x3_F16_With_Vec3_Vec3_Vec3_Vec3) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat4x3<f16>(vec3<f16>(2_h, 2_h, 2_h), vec3<f16>(2_h, 2_h, 2_h),
-                             vec3<f16>(2_h, 2_h, 2_h), vec3<f16>(2_h, 2_h, 2_h));
+    auto* cast = Call<mat4x3<f16>>(Call<vec3<f16>>(2_h, 2_h, 2_h), Call<vec3<f16>>(2_h, 2_h, 2_h),
+                                   Call<vec3<f16>>(2_h, 2_h, 2_h), Call<vec3<f16>>(2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3431,7 +3434,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x4_F32_With_Vec4_Vec4) {
-    auto* cast = mat2x4<f32>(vec4<f32>(2_f, 2_f, 2_f, 2_f), vec4<f32>(2_f, 2_f, 2_f, 2_f));
+    auto* cast =
+        Call<mat2x4<f32>>(Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f), Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3451,7 +3455,8 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat2x4_F16_With_Vec4_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat2x4<f16>(vec4<f16>(2_h, 2_h, 2_h, 2_h), vec4<f16>(2_h, 2_h, 2_h, 2_h));
+    auto* cast =
+        Call<mat2x4<f16>>(Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h), Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3469,8 +3474,9 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x4_F32_With_Vec4_Vec4_Vec4) {
-    auto* cast = mat3x4<f32>(vec4<f32>(2_f, 2_f, 2_f, 2_f), vec4<f32>(2_f, 2_f, 2_f, 2_f),
-                             vec4<f32>(2_f, 2_f, 2_f, 2_f));
+    auto* cast =
+        Call<mat3x4<f32>>(Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f), Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f),
+                          Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3490,8 +3496,9 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat3x4_F16_With_Vec4_Vec4_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat3x4<f16>(vec4<f16>(2_h, 2_h, 2_h, 2_h), vec4<f16>(2_h, 2_h, 2_h, 2_h),
-                             vec4<f16>(2_h, 2_h, 2_h, 2_h));
+    auto* cast =
+        Call<mat3x4<f16>>(Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h), Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h),
+                          Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3509,8 +3516,9 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x4_F32_With_Vec4_Vec4_Vec4_Vec4) {
-    auto* cast = mat4x4<f32>(vec4<f32>(2_f, 2_f, 2_f, 2_f), vec4<f32>(2_f, 2_f, 2_f, 2_f),
-                             vec4<f32>(2_f, 2_f, 2_f, 2_f), vec4<f32>(2_f, 2_f, 2_f, 2_f));
+    auto* cast =
+        Call<mat4x4<f32>>(Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f), Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f),
+                          Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f), Call<vec4<f32>>(2_f, 2_f, 2_f, 2_f));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3530,8 +3538,9 @@
 TEST_F(SpvBuilderConstructorTest, Type_Mat4x4_F16_With_Vec4_Vec4_Vec4_Vec4) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = mat4x4<f16>(vec4<f16>(2_h, 2_h, 2_h, 2_h), vec4<f16>(2_h, 2_h, 2_h, 2_h),
-                             vec4<f16>(2_h, 2_h, 2_h, 2_h), vec4<f16>(2_h, 2_h, 2_h, 2_h));
+    auto* cast =
+        Call<mat4x4<f16>>(Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h), Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h),
+                          Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h), Call<vec4<f16>>(2_h, 2_h, 2_h, 2_h));
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3549,7 +3558,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Array_5_F32) {
-    auto* cast = array<f32, 5>(2_f, 2_f, 2_f, 2_f, 2_f);
+    auto* cast = Call<array<f32, 5>>(2_f, 2_f, 2_f, 2_f, 2_f);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3569,7 +3578,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Array_5_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = array<f16, 5>(2_h, 2_h, 2_h, 2_h, 2_h);
+    auto* cast = Call<array<f16, 5>>(2_h, 2_h, 2_h, 2_h, 2_h);
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -3587,9 +3596,9 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_Array_2_Vec3_F32) {
-    auto* first = vec3<f32>(1_f, 2_f, 3_f);
-    auto* second = vec3<f32>(1_f, 2_f, 3_f);
-    auto* t = Call(ty.array(ty.vec3<f32>(), 2_u), first, second);
+    auto* first = Call<vec3<f32>>(1_f, 2_f, 3_f);
+    auto* second = Call<vec3<f32>>(1_f, 2_f, 3_f);
+    auto* t = Call(ty.array<vec3<f32>, 2>(), first, second);
     WrapInFunction(t);
     spirv::Builder& b = Build();
 
@@ -3611,9 +3620,9 @@
 TEST_F(SpvBuilderConstructorTest, Type_Array_2_Vec3_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* first = vec3<f16>(1_h, 2_h, 3_h);
-    auto* second = vec3<f16>(1_h, 2_h, 3_h);
-    auto* t = Call(ty.array(ty.vec3<f16>(), 2_u), first, second);
+    auto* first = Call<vec3<f16>>(1_h, 2_h, 3_h);
+    auto* second = Call<vec3<f16>>(1_h, 2_h, 3_h);
+    auto* t = Call(ty.array<vec3<f16>, 2>(), first, second);
     WrapInFunction(t);
     spirv::Builder& b = Build();
 
@@ -3633,8 +3642,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, CommonInitializer_TwoVectors) {
-    auto* v1 = vec3<f32>(2_f, 2_f, 2_f);
-    auto* v2 = vec3<f32>(2_f, 2_f, 2_f);
+    auto* v1 = Call<vec3<f32>>(2_f, 2_f, 2_f);
+    auto* v2 = Call<vec3<f32>>(2_f, 2_f, 2_f);
     WrapInFunction(WrapInStatement(v1), WrapInStatement(v2));
 
     spirv::Builder& b = Build();
@@ -3651,8 +3660,8 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, CommonInitializer_TwoArrays) {
-    auto* a1 = array<f32, 3>(2_f, 2_f, 2_f);
-    auto* a2 = array<f32, 3>(2_f, 2_f, 2_f);
+    auto* a1 = Call<array<f32, 3>>(2_f, 2_f, 2_f);
+    auto* a2 = Call<array<f32, 3>>(2_f, 2_f, 2_f);
     WrapInFunction(WrapInStatement(a1), WrapInStatement(a2));
 
     spirv::Builder& b = Build();
@@ -3674,8 +3683,8 @@
     // Test that initializers of different types with the same values produce
     // different OpConstantComposite instructions.
     // crbug.com/tint/777
-    auto* a1 = array<f32, 2>(1_f, 2_f);
-    auto* a2 = vec2<f32>(1_f, 2_f);
+    auto* a1 = Call<array<f32, 2>>(1_f, 2_f);
+    auto* a2 = Call<vec2<f32>>(1_f, 2_f);
     WrapInFunction(WrapInStatement(a1), WrapInStatement(a2));
     spirv::Builder& b = Build();
 
@@ -3701,7 +3710,7 @@
                                          Member("b", ty.vec3<f32>()),
                                      });
 
-    auto* t = Call(ty.Of(s), 2_f, vec3<f32>(2_f, 2_f, 2_f));
+    auto* t = Call(ty.Of(s), 2_f, Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -3808,7 +3817,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_ZeroInit_Vector) {
-    auto* t = vec2<i32>();
+    auto* t = Call<vec2<i32>>();
 
     WrapInFunction(t);
 
@@ -3826,7 +3835,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_ZeroInit_Matrix_F32) {
-    auto* t = mat4x2<f32>();
+    auto* t = Call<mat4x2<f32>>();
 
     WrapInFunction(t);
 
@@ -3847,7 +3856,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_ZeroInit_Matrix_F16) {
     Enable(builtin::Extension::kF16);
 
-    auto* t = mat4x2<f16>();
+    auto* t = Call<mat4x2<f16>>();
 
     WrapInFunction(t);
 
@@ -3866,7 +3875,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, Type_ZeroInit_Array) {
-    auto* t = array<i32, 2>();
+    auto* t = Call<array<i32, 2>>();
 
     WrapInFunction(t);
 
@@ -4206,7 +4215,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_U32_to_I32) {
     auto* var = GlobalVar("i", ty.vec3<u32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<i32>("i");
+    auto* cast = Call<vec3<i32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4232,7 +4241,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_F32_to_I32) {
     auto* var = GlobalVar("i", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<i32>("i");
+    auto* cast = Call<vec3<i32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4260,7 +4269,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<f16>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<i32>("i");
+    auto* cast = Call<vec3<i32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4286,7 +4295,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_I32_to_U32) {
     auto* var = GlobalVar("i", ty.vec3<i32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<u32>("i");
+    auto* cast = Call<vec3<u32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4312,7 +4321,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_F32_to_U32) {
     auto* var = GlobalVar("i", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<u32>("i");
+    auto* cast = Call<vec3<u32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4340,7 +4349,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<f16>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<u32>("i");
+    auto* cast = Call<vec3<u32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4366,7 +4375,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_I32_to_F32) {
     auto* var = GlobalVar("i", ty.vec3<i32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f32>("i");
+    auto* cast = Call<vec3<f32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4392,7 +4401,7 @@
 TEST_F(SpvBuilderConstructorTest, Type_Convert_Vectors_U32_to_F32) {
     auto* var = GlobalVar("i", ty.vec3<u32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f32>("i");
+    auto* cast = Call<vec3<f32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4420,7 +4429,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<f16>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f32>("i");
+    auto* cast = Call<vec3<f32>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4448,7 +4457,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<i32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f16>("i");
+    auto* cast = Call<vec3<f16>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4476,7 +4485,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<u32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f16>("i");
+    auto* cast = Call<vec3<f16>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4504,7 +4513,7 @@
 
     auto* var = GlobalVar("i", ty.vec3<f32>(), builtin::AddressSpace::kPrivate);
 
-    auto* cast = vec3<f16>("i");
+    auto* cast = Call<vec3<f16>>("i");
     WrapInFunction(cast);
 
     spirv::Builder& b = Build();
@@ -4529,7 +4538,7 @@
 
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_GlobalVectorWithAllConstInitializers) {
     // vec3<f32>(1.0, 2.0, 3.0)  -> true
-    auto* t = vec3<f32>(1_f, 2_f, 3_f);
+    auto* t = Call<vec3<f32>>(1_f, 2_f, 3_f);
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4541,8 +4550,8 @@
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_GlobalArrayWithAllConstInitializers) {
     // array<vec3<f32>, 2u>(vec3<f32>(1.0, 2.0, 3.0), vec3<f32>(1.0, 2.0, 3.0))
     //   -> true
-    auto* t =
-        Call(ty.array(ty.vec3<f32>(), 2_u), vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
+    auto* t = Call(ty.array<vec3<f32>, 2>(), Call<vec3<f32>>(1_f, 2_f, 3_f),
+                   Call<vec3<f32>>(1_f, 2_f, 3_f));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4554,7 +4563,7 @@
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_GlobalVectorWithMatchingTypeInitializers) {
     // vec2<f32>(f32(1.0), f32(2.0))  -> false
 
-    auto* t = vec2<f32>(Call<f32>(1_f), Call<f32>(2_f));
+    auto* t = Call<vec2<f32>>(Call<f32>(1_f), Call<f32>(2_f));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4566,7 +4575,7 @@
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_GlobalWithTypeConversionInitializer) {
     // vec2<f32>(f32(1), f32(2)) -> false
 
-    auto* t = vec2<f32>(Call<f32>(1_i), Call<f32>(2_i));
+    auto* t = Call<vec2<f32>>(Call<f32>(1_i), Call<f32>(2_i));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4578,7 +4587,7 @@
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_VectorWithAllConstInitializers) {
     // vec3<f32>(1.0, 2.0, 3.0)  -> true
 
-    auto* t = vec3<f32>(1_f, 2_f, 3_f);
+    auto* t = Call<vec3<f32>>(1_f, 2_f, 3_f);
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4594,7 +4603,7 @@
     GlobalVar("b", ty.f32(), builtin::AddressSpace::kPrivate);
     GlobalVar("c", ty.f32(), builtin::AddressSpace::kPrivate);
 
-    auto* t = vec3<f32>("a", "b", "c");
+    auto* t = Call<vec3<f32>>("a", "b", "c");
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4607,10 +4616,10 @@
     // array<vec3<f32>, 2u>(vec3<f32>(1.0, 2.0, 3.0), vec3<f32>(1.0, 2.0, 3.0))
     //   -> true
 
-    auto* first = vec3<f32>(1_f, 2_f, 3_f);
-    auto* second = vec3<f32>(1_f, 2_f, 3_f);
+    auto* first = Call<vec3<f32>>(1_f, 2_f, 3_f);
+    auto* second = Call<vec3<f32>>(1_f, 2_f, 3_f);
 
-    auto* t = Call(ty.array(ty.vec3<f32>(), 2_u), first, second);
+    auto* t = Call(ty.array<vec3<f32>, 2>(), first, second);
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4622,7 +4631,7 @@
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_VectorWithTypeConversionConstInitializers) {
     // vec2<f32>(f32(1), f32(2))  -> false
 
-    auto* t = vec2<f32>(Call<f32>(1_i), Call<f32>(2_i));
+    auto* t = Call<vec2<f32>>(Call<f32>(1_i), Call<f32>(2_i));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4632,7 +4641,7 @@
 }
 
 TEST_F(SpvBuilderConstructorTest, IsConstructorConst_BitCastScalars) {
-    auto* t = vec2<u32>(Call<u32>(1_i), Call<u32>(1_i));
+    auto* t = Call<vec2<u32>>(Call<u32>(1_i), Call<u32>(1_i));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4647,7 +4656,7 @@
                                          Member("b", ty.vec3<f32>()),
                                      });
 
-    auto* t = Call(ty.Of(s), 2_f, vec3<f32>(2_f, 2_f, 2_f));
+    auto* t = Call(ty.Of(s), 2_f, Call<vec3<f32>>(2_f, 2_f, 2_f));
     WrapInFunction(t);
 
     spirv::Builder& b = Build();
@@ -4680,8 +4689,8 @@
     // }
     // let y = vec3<f32>(1.0, 2.0, 3.0); // Reuses the ID 'x'
 
-    WrapInFunction(If(true, Block(Decl(Let("x", vec3<f32>(1_f, 2_f, 3_f))))),
-                   Decl(Let("y", vec3<f32>(1_f, 2_f, 3_f))));
+    WrapInFunction(If(true, Block(Decl(Let("x", Call<vec3<f32>>(1_f, 2_f, 3_f))))),
+                   Decl(Let("y", Call<vec3<f32>>(1_f, 2_f, 3_f))));
 
     spirv::Builder& b = SanitizeAndBuild();
     ASSERT_TRUE(b.Build());
@@ -4725,8 +4734,8 @@
     // let y = vec3<f32>(one, 2.0, 3.0); // Mustn't reuse the ID 'x'
 
     WrapInFunction(Decl(Var("one", Expr(1_f))),
-                   If(true, Block(Decl(Let("x", vec3<f32>("one", 2_f, 3_f))))),
-                   Decl(Let("y", vec3<f32>("one", 2_f, 3_f))));
+                   If(true, Block(Decl(Let("x", Call<vec3<f32>>("one", 2_f, 3_f))))),
+                   Decl(Let("y", Call<vec3<f32>>("one", 2_f, 3_f))));
 
     spirv::Builder& b = SanitizeAndBuild();
     ASSERT_TRUE(b.Build());
diff --git a/src/tint/writer/spirv/builder_entry_point_test.cc b/src/tint/writer/spirv/builder_entry_point_test.cc
index 0e057a3..46ffb2d 100644
--- a/src/tint/writer/spirv/builder_entry_point_test.cc
+++ b/src/tint/writer/spirv/builder_entry_point_test.cc
@@ -29,11 +29,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, EntryPoint_Parameters) {
@@ -215,7 +216,7 @@
             Member("pos", ty.vec4<f32>(), utils::Vector{Builtin(builtin::BuiltinValue::kPosition)}),
         });
 
-    auto* vert_retval = Call(ty.Of(interface), 42_f, vec4<f32>());
+    auto* vert_retval = Call(ty.Of(interface), 42_f, Call<vec4<f32>>());
     Func("vert_main", utils::Empty, ty.Of(interface), utils::Vector{Return(vert_retval)},
          utils::Vector{
              Stage(ast::PipelineStage::kVertex),
diff --git a/src/tint/writer/spirv/builder_function_attribute_test.cc b/src/tint/writer/spirv/builder_function_attribute_test.cc
index c47429b..2bdcc44 100644
--- a/src/tint/writer/spirv/builder_function_attribute_test.cc
+++ b/src/tint/writer/spirv/builder_function_attribute_test.cc
@@ -19,11 +19,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, Attribute_Stage) {
@@ -61,7 +62,7 @@
     if (params.stage == ast::PipelineStage::kVertex) {
         ret_type = ty.vec4<f32>();
         ret_type_attrs.Push(Builtin(builtin::BuiltinValue::kPosition));
-        body.Push(Return(Call(ty.vec4<f32>())));
+        body.Push(Return(Call<vec4<f32>>()));
     }
 
     utils::Vector<const ast::Attribute*, 2> deco_list{Stage(params.stage)};
diff --git a/src/tint/writer/spirv/builder_function_variable_test.cc b/src/tint/writer/spirv/builder_function_variable_test.cc
index 9f77d4e..d85a761 100644
--- a/src/tint/writer/spirv/builder_function_variable_test.cc
+++ b/src/tint/writer/spirv/builder_function_variable_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, FunctionVar_NoAddressSpace) {
@@ -44,7 +45,7 @@
 }
 
 TEST_F(BuilderTest, FunctionVar_WithConstantInitializer) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 1_f, 3_f);
     auto* v = Var("var", ty.vec3<f32>(), builtin::AddressSpace::kFunction, init);
     WrapInFunction(v);
 
@@ -74,7 +75,7 @@
 
 TEST_F(BuilderTest, FunctionVar_WithNonConstantInitializer) {
     auto* a = Let("a", Expr(3_f));
-    auto* init = vec2<f32>(1_f, Add(Expr("a"), 3_f));
+    auto* init = Call<vec2<f32>>(1_f, Add(Expr("a"), 3_f));
 
     auto* v = Var("var", ty.vec2<f32>(), init);
     WrapInFunction(a, v);
@@ -207,7 +208,7 @@
 }
 
 TEST_F(BuilderTest, FunctionVar_Let) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 1_f, 3_f);
 
     auto* v = Let("var", ty.vec3<f32>(), init);
 
@@ -227,7 +228,7 @@
 }
 
 TEST_F(BuilderTest, FunctionVar_Const) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 1_f, 3_f);
 
     auto* v = Const("var", ty.vec3<f32>(), init);
 
diff --git a/src/tint/writer/spirv/builder_global_variable_test.cc b/src/tint/writer/spirv/builder_global_variable_test.cc
index 671bacc..0640ec7 100644
--- a/src/tint/writer/spirv/builder_global_variable_test.cc
+++ b/src/tint/writer/spirv/builder_global_variable_test.cc
@@ -18,11 +18,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, GlobalVar_WithAddressSpace) {
@@ -41,7 +42,7 @@
 }
 
 TEST_F(BuilderTest, GlobalVar_WithInitializer) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 1_f, 3_f);
 
     auto* v = GlobalVar("var", ty.vec3<f32>(), builtin::AddressSpace::kPrivate, init);
 
@@ -91,7 +92,7 @@
     // const c = vec3<f32>(1f, 2f, 3f);
     // var v = c;
 
-    auto* c = GlobalConst("c", vec3<f32>(1_f, 2_f, 3_f));
+    auto* c = GlobalConst("c", Call<vec3<f32>>(1_f, 2_f, 3_f));
     GlobalVar("v", builtin::AddressSpace::kPrivate, Expr(c));
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -121,7 +122,7 @@
     // var v = c;
     Enable(builtin::Extension::kF16);
 
-    auto* c = GlobalConst("c", vec3<f16>(1_h, 2_h, 3_h));
+    auto* c = GlobalConst("c", Call<vec3<f16>>(1_h, 2_h, 3_h));
     GlobalVar("v", builtin::AddressSpace::kPrivate, Expr(c));
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -150,7 +151,7 @@
     // const c = vec3(1, 2, 3);
     // var v = c;
 
-    auto* c = GlobalConst("c", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* c = GlobalConst("c", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     GlobalVar("v", builtin::AddressSpace::kPrivate, Expr(c));
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -179,7 +180,7 @@
     // const c = vec3(1.0, 2.0, 3.0);
     // var v = c;
 
-    auto* c = GlobalConst("c", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* c = GlobalConst("c", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     GlobalVar("v", builtin::AddressSpace::kPrivate, Expr(c));
 
     spirv::Builder& b = SanitizeAndBuild();
@@ -208,7 +209,7 @@
     // const c = vec3<f32>(vec2<f32>(1f, 2f), 3f));
     // var v = c;
 
-    auto* c = GlobalConst("c", vec3<f32>(vec2<f32>(1_f, 2_f), 3_f));
+    auto* c = GlobalConst("c", Call<vec3<f32>>(Call<vec2<f32>>(1_f, 2_f), 3_f));
     GlobalVar("v", builtin::AddressSpace::kPrivate, Expr(c));
 
     spirv::Builder& b = SanitizeAndBuild();
diff --git a/src/tint/writer/spirv/builder_ident_expression_test.cc b/src/tint/writer/spirv/builder_ident_expression_test.cc
index 5ab7c19..1278cfc 100644
--- a/src/tint/writer/spirv/builder_ident_expression_test.cc
+++ b/src/tint/writer/spirv/builder_ident_expression_test.cc
@@ -16,11 +16,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, IdentifierExpression_GlobalConst) {
@@ -28,7 +29,7 @@
         {
             ProgramBuilder pb;
 
-            auto* init = pb.vec3<f32>(1_f, 1_f, 3_f);
+            auto* init = pb.Call<vec3<f32>>(1_f, 1_f, 3_f);
 
             auto* v = pb.GlobalConst("c", pb.ty.vec3<f32>(), init);
 
@@ -66,7 +67,7 @@
 }
 
 TEST_F(BuilderTest, IdentifierExpression_FunctionConst) {
-    auto* init = vec3<f32>(1_f, 1_f, 3_f);
+    auto* init = Call<vec3<f32>>(1_f, 1_f, 3_f);
 
     auto* v = Let("var", ty.vec3<f32>(), init);
 
diff --git a/src/tint/writer/spirv/builder_return_test.cc b/src/tint/writer/spirv/builder_return_test.cc
index 13a65d6..041092f 100644
--- a/src/tint/writer/spirv/builder_return_test.cc
+++ b/src/tint/writer/spirv/builder_return_test.cc
@@ -15,11 +15,12 @@
 #include "src/tint/writer/spirv/spv_dump.h"
 #include "src/tint/writer/spirv/test_helper.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using BuilderTest = TestHelper;
 
 TEST_F(BuilderTest, Return) {
@@ -37,7 +38,7 @@
 }
 
 TEST_F(BuilderTest, Return_WithValue) {
-    auto* val = vec3<f32>(1_f, 1_f, 3_f);
+    auto* val = Call<vec3<f32>>(1_f, 1_f, 3_f);
 
     auto* ret = Return(val);
     Func("test", utils::Empty, ty.vec3<f32>(), utils::Vector{ret}, utils::Empty);
diff --git a/src/tint/writer/spirv/generator.h b/src/tint/writer/spirv/generator.h
index 0200426..3319c05 100644
--- a/src/tint/writer/spirv/generator.h
+++ b/src/tint/writer/spirv/generator.h
@@ -60,6 +60,10 @@
     /// VK_KHR_zero_initialize_workgroup_memory is enabled.
     bool use_zero_initialize_workgroup_memory_extension = false;
 
+    /// Set to `true` to skip robustness transform on textures when VK_EXT_robustness is enabled and
+    /// robustImageAccess == VK_TRUE.
+    bool disable_image_robustness = false;
+
 #if TINT_BUILD_IR
     /// Set to `true` to generate SPIR-V via the Tint IR instead of from the AST.
     bool use_tint_ir = false;
@@ -70,7 +74,8 @@
                  emit_vertex_point_size,
                  disable_workgroup_init,
                  external_texture_options,
-                 use_zero_initialize_workgroup_memory_extension);
+                 use_zero_initialize_workgroup_memory_extension,
+                 disable_image_robustness);
 };
 
 /// The result produced when generating SPIR-V.
diff --git a/src/tint/writer/spirv/generator_impl.cc b/src/tint/writer/spirv/generator_impl.cc
index 1b22710..b5e5a5c 100644
--- a/src/tint/writer/spirv/generator_impl.cc
+++ b/src/tint/writer/spirv/generator_impl.cc
@@ -82,6 +82,12 @@
         // Robustness must come after PromoteSideEffectsToDecl
         // Robustness must come before BuiltinPolyfill and CanonicalizeEntryPointIO
         manager.Add<ast::transform::Robustness>();
+
+        ast::transform::Robustness::Config config = {};
+        if (options.disable_image_robustness) {
+            config.texture_action = ast::transform::Robustness::Action::kIgnore;
+        }
+        data.Add<ast::transform::Robustness::Config>(config);
     }
 
     // BindingRemapper must come before MultiplanarExternalTexture. Note, this is flipped to the
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index ad1b180..1a821da 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -25,6 +25,7 @@
 #include "src/tint/ir/block_param.h"
 #include "src/tint/ir/break_if.h"
 #include "src/tint/ir/builtin_call.h"
+#include "src/tint/ir/construct.h"
 #include "src/tint/ir/continue.h"
 #include "src/tint/ir/exit_if.h"
 #include "src/tint/ir/exit_loop.h"
@@ -40,7 +41,9 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/transform/add_empty_entry_point.h"
 #include "src/tint/ir/transform/block_decorated_structs.h"
+#include "src/tint/ir/transform/merge_return.h"
 #include "src/tint/ir/transform/var_for_dynamic_index.h"
+#include "src/tint/ir/unreachable.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/validate.h"
 #include "src/tint/ir/var.h"
@@ -58,6 +61,7 @@
 #include "src/tint/type/u32.h"
 #include "src/tint/type/vector.h"
 #include "src/tint/type/void.h"
+#include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/writer/spirv/generator.h"
 #include "src/tint/writer/spirv/module.h"
 
@@ -71,6 +75,7 @@
 
     manager.Add<ir::transform::AddEmptyEntryPoint>();
     manager.Add<ir::transform::BlockDecoratedStructs>();
+    manager.Add<ir::transform::MergeReturn>();
     manager.Add<ir::transform::VarForDynamicIndex>();
 
     transform::DataMap outputs;
@@ -222,6 +227,10 @@
                 module_.PushType(spv::Op::OpTypeFloat, {id, 32u});
             },
             [&](const type::F16*) {
+                module_.PushCapability(SpvCapabilityFloat16);
+                module_.PushCapability(SpvCapabilityUniformAndStorageBuffer16BitAccess);
+                module_.PushCapability(SpvCapabilityStorageBuffer16BitAccess);
+                module_.PushCapability(SpvCapabilityStorageInputOutput16);
                 module_.PushType(spv::Op::OpTypeFloat, {id, 16u});
             },
             [&](const type::Vector* vec) {
@@ -256,6 +265,10 @@
     });
 }
 
+uint32_t GeneratorImplIr::Value(ir::Instruction* inst) {
+    return Value(inst->Result());
+}
+
 uint32_t GeneratorImplIr::Value(ir::Value* value) {
     return Switch(
         value,  //
@@ -458,6 +471,7 @@
             [&](ir::Access* a) { EmitAccess(a); },            //
             [&](ir::Binary* b) { EmitBinary(b); },            //
             [&](ir::BuiltinCall* b) { EmitBuiltinCall(b); },  //
+            [&](ir::Construct* c) { EmitConstruct(c); },      //
             [&](ir::Load* l) { EmitLoad(l); },                //
             [&](ir::Loop* l) { EmitLoop(l); },                //
             [&](ir::Switch* sw) { EmitSwitch(sw); },          //
@@ -471,6 +485,11 @@
                     << "unimplemented instruction: " << inst->TypeInfo().name;
             });
     }
+
+    if (block->IsEmpty()) {
+        // If the last emitted instruction is not a branch, then this should be unreachable.
+        current_function_.push_inst(spv::Op::OpUnreachable, {});
+    }
 }
 
 void GeneratorImplIr::EmitBranch(ir::Branch* b) {
@@ -491,48 +510,47 @@
             current_function_.push_inst(spv::Op::OpBranchConditional,
                                         {
                                             Value(breakif->Condition()),
-                                            Label(breakif->Loop()->Merge()),
+                                            loop_merge_label_,
                                             Label(breakif->Loop()->Body()),
                                         });
         },
         [&](ir::Continue* cont) {
             current_function_.push_inst(spv::Op::OpBranch, {Label(cont->Loop()->Continuing())});
         },
-        [&](ir::ExitIf* if_) {
-            current_function_.push_inst(spv::Op::OpBranch, {Label(if_->If()->Merge())});
-        },
-        [&](ir::ExitLoop* loop) {
-            current_function_.push_inst(spv::Op::OpBranch, {Label(loop->Loop()->Merge())});
-        },
-        [&](ir::ExitSwitch* swtch) {
-            current_function_.push_inst(spv::Op::OpBranch, {Label(swtch->Switch()->Merge())});
+        [&](ir::ExitIf*) { current_function_.push_inst(spv::Op::OpBranch, {if_merge_label_}); },
+        [&](ir::ExitLoop*) { current_function_.push_inst(spv::Op::OpBranch, {loop_merge_label_}); },
+        [&](ir::ExitSwitch*) {
+            current_function_.push_inst(spv::Op::OpBranch, {switch_merge_label_});
         },
         [&](ir::NextIteration* loop) {
             current_function_.push_inst(spv::Op::OpBranch, {Label(loop->Loop()->Body())});
         },
+        [&](ir::Unreachable*) { current_function_.push_inst(spv::Op::OpUnreachable, {}); },
+
         [&](Default) {
             TINT_ICE(Writer, diagnostics_) << "unimplemented branch: " << b->TypeInfo().name;
         });
 }
 
 void GeneratorImplIr::EmitIf(ir::If* i) {
-    auto* merge_block = i->Merge();
     auto* true_block = i->True();
     auto* false_block = i->False();
 
     // Generate labels for the blocks. We emit the true or false block if it:
     // 1. contains instructions other then the branch, or
-    // 2. branches somewhere other then the Merge(), or
-    // 3. the merge has input parameters
+    // 2. branches somewhere instead of exiting the loop (e.g. return or break), or
+    // 3. the if returns a value
     // Otherwise we skip them and branch straight to the merge block.
-    uint32_t merge_label = Label(merge_block);
+    uint32_t merge_label = module_.NextId();
+    TINT_SCOPED_ASSIGNMENT(if_merge_label_, merge_label);
+
     uint32_t true_label = merge_label;
     uint32_t false_label = merge_label;
-    if (true_block->Length() > 1 || !merge_block->Params().IsEmpty() ||
+    if (true_block->Length() > 1 || i->HasResults() ||
         (true_block->HasBranchTarget() && !true_block->Branch()->Is<ir::ExitIf>())) {
         true_label = Label(true_block);
     }
-    if (false_block->Length() > 1 || !merge_block->Params().IsEmpty() ||
+    if (false_block->Length() > 1 || i->HasResults() ||
         (false_block->HasBranchTarget() && !false_block->Branch()->Is<ir::ExitIf>())) {
         false_label = Label(false_block);
     }
@@ -551,15 +569,19 @@
         EmitBlock(false_block);
     }
 
-    // Emit the merge block.
-    EmitBlock(merge_block);
+    current_function_.push_inst(spv::Op::OpLabel, {merge_label});
+
+    // Emit the OpPhis for the ExitIfs
+    EmitExitPhis(i);
 }
 
 void GeneratorImplIr::EmitAccess(ir::Access* access) {
-    auto id = Value(access);
-    OperandList operands = {Type(access->Type()), id, Value(access->Object())};
+    auto* ty = access->Result()->Type();
 
-    if (access->Type()->Is<type::Pointer>()) {
+    auto id = Value(access);
+    OperandList operands = {Type(ty), id, Value(access->Object())};
+
+    if (ty->Is<type::Pointer>()) {
         // Use OpAccessChain for accesses into pointer types.
         for (auto* idx : access->Indices()) {
             operands.push_back(Value(idx));
@@ -593,7 +615,7 @@
             }
 
             // Now emit the OpVectorExtractDynamic instruction.
-            operands = {Type(access->Type()), id, vec_id, Value(idx)};
+            operands = {Type(ty), id, vec_id, Value(idx)};
             current_function_.push_inst(spv::Op::OpVectorExtractDynamic, std::move(operands));
             return;
         }
@@ -603,17 +625,18 @@
 
 void GeneratorImplIr::EmitBinary(ir::Binary* binary) {
     auto id = Value(binary);
+    auto* ty = binary->Result()->Type();
     auto* lhs_ty = binary->LHS()->Type();
 
     // Determine the opcode.
     spv::Op op = spv::Op::Max;
     switch (binary->Kind()) {
         case ir::Binary::Kind::kAdd: {
-            op = binary->Type()->is_integer_scalar_or_vector() ? spv::Op::OpIAdd : spv::Op::OpFAdd;
+            op = ty->is_integer_scalar_or_vector() ? spv::Op::OpIAdd : spv::Op::OpFAdd;
             break;
         }
         case ir::Binary::Kind::kSubtract: {
-            op = binary->Type()->is_integer_scalar_or_vector() ? spv::Op::OpISub : spv::Op::OpFSub;
+            op = ty->is_integer_scalar_or_vector() ? spv::Op::OpISub : spv::Op::OpFSub;
             break;
         }
 
@@ -698,17 +721,16 @@
     }
 
     // Emit the instruction.
-    current_function_.push_inst(
-        op, {Type(binary->Type()), id, Value(binary->LHS()), Value(binary->RHS())});
+    current_function_.push_inst(op, {Type(ty), id, Value(binary->LHS()), Value(binary->RHS())});
 }
 
 void GeneratorImplIr::EmitBuiltinCall(ir::BuiltinCall* builtin) {
-    auto* result_ty = builtin->Type();
+    auto* result_ty = builtin->Result()->Type();
 
     if (builtin->Func() == builtin::Function::kAbs &&
         result_ty->is_unsigned_integer_scalar_or_vector()) {
         // abs() is a no-op for unsigned integers.
-        values_.Add(builtin, Value(builtin->Args()[0]));
+        values_.Add(builtin->Result(), Value(builtin->Args()[0]));
         return;
     }
 
@@ -771,9 +793,17 @@
     current_function_.push_inst(op, operands);
 }
 
+void GeneratorImplIr::EmitConstruct(ir::Construct* construct) {
+    OperandList operands = {Type(construct->Result()->Type()), Value(construct)};
+    for (auto* arg : construct->Args()) {
+        operands.push_back(Value(arg));
+    }
+    current_function_.push_inst(spv::Op::OpCompositeConstruct, std::move(operands));
+}
+
 void GeneratorImplIr::EmitLoad(ir::Load* load) {
     current_function_.push_inst(spv::Op::OpLoad,
-                                {Type(load->Type()), Value(load), Value(load->From())});
+                                {Type(load->Result()->Type()), Value(load), Value(load->From())});
 }
 
 void GeneratorImplIr::EmitLoop(ir::Loop* loop) {
@@ -781,7 +811,9 @@
     auto header_label = Label(loop->Body());  // Back-edge needs to branch to the loop header
     auto body_label = module_.NextId();
     auto continuing_label = Label(loop->Continuing());
-    auto merge_label = Label(loop->Merge());
+
+    uint32_t merge_label = module_.NextId();
+    TINT_SCOPED_ASSIGNMENT(loop_merge_label_, merge_label);
 
     if (init_label != 0) {
         // Emit the loop initializer.
@@ -814,7 +846,10 @@
     }
 
     // Emit the loop merge block.
-    EmitBlock(loop->Merge());
+    current_function_.push_inst(spv::Op::OpLabel, {merge_label});
+
+    // Emit the OpPhis for the ExitLoops
+    EmitExitPhis(loop);
 }
 
 void GeneratorImplIr::EmitSwitch(ir::Switch* swtch) {
@@ -823,7 +858,7 @@
     for (auto& c : swtch->Cases()) {
         for (auto& sel : c.selectors) {
             if (sel.IsDefault()) {
-                default_label = Label(c.Start());
+                default_label = Label(c.Block());
             }
         }
     }
@@ -832,7 +867,7 @@
     // Build the operands to the OpSwitch instruction.
     OperandList switch_operands = {Value(swtch->Condition()), default_label};
     for (auto& c : swtch->Cases()) {
-        auto label = Label(c.Start());
+        auto label = Label(c.Block());
         for (auto& sel : c.selectors) {
             if (sel.IsDefault()) {
                 continue;
@@ -842,18 +877,24 @@
         }
     }
 
+    uint32_t merge_label = module_.NextId();
+    TINT_SCOPED_ASSIGNMENT(switch_merge_label_, merge_label);
+
     // Emit the OpSelectionMerge and OpSwitch instructions.
     current_function_.push_inst(spv::Op::OpSelectionMerge,
-                                {Label(swtch->Merge()), U32Operand(SpvSelectionControlMaskNone)});
+                                {merge_label, U32Operand(SpvSelectionControlMaskNone)});
     current_function_.push_inst(spv::Op::OpSwitch, switch_operands);
 
     // Emit the cases.
     for (auto& c : swtch->Cases()) {
-        EmitBlock(c.Start());
+        EmitBlock(c.Block());
     }
 
     // Emit the switch merge block.
-    EmitBlock(swtch->Merge());
+    current_function_.push_inst(spv::Op::OpLabel, {merge_label});
+
+    // Emit the OpPhis for the ExitSwitches
+    EmitExitPhis(swtch);
 }
 
 void GeneratorImplIr::EmitStore(ir::Store* store) {
@@ -862,7 +903,7 @@
 
 void GeneratorImplIr::EmitUserCall(ir::UserCall* call) {
     auto id = Value(call);
-    OperandList operands = {Type(call->Type()), id, Value(call->Func())};
+    OperandList operands = {Type(call->Result()->Type()), id, Value(call->Func())};
     for (auto* arg : call->Args()) {
         operands.push_back(Value(arg));
     }
@@ -871,7 +912,7 @@
 
 void GeneratorImplIr::EmitVar(ir::Var* var) {
     auto id = Value(var);
-    auto* ptr = var->Type();
+    auto* ptr = var->Result()->Type()->As<type::Pointer>();
     auto ty = Type(ptr);
 
     switch (ptr->AddressSpace()) {
@@ -928,4 +969,32 @@
     }
 }
 
+void GeneratorImplIr::EmitExitPhis(ir::ControlInstruction* inst) {
+    struct Branch {
+        uint32_t label = 0;
+        ir::Value* value = nullptr;
+        bool operator<(const Branch& other) const { return label < other.label; }
+    };
+
+    auto results = inst->Results();
+    for (size_t index = 0; index < results.Length(); index++) {
+        auto* result = results[index];
+        auto* ty = result->Type();
+
+        utils::Vector<Branch, 8> branches;
+        branches.Reserve(inst->Exits().Count());
+        for (auto& exit : inst->Exits()) {
+            branches.Push(Branch{Label(exit->Block()), exit->Args()[index]});
+        }
+        branches.Sort();  // Sort the branches by label to ensure deterministic output
+
+        OperandList ops{Type(ty), Value(result)};
+        for (auto& branch : branches) {
+            ops.push_back(Value(branch.value));
+            ops.push_back(branch.label);
+        }
+        current_function_.push_inst(spv::Op::OpPhi, std::move(ops));
+    }
+}
+
 }  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.h b/src/tint/writer/spirv/ir/generator_impl_ir.h
index dccb23a..1a2ec8e 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.h
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.h
@@ -35,6 +35,11 @@
 class BlockParam;
 class Branch;
 class BuiltinCall;
+class Construct;
+class ControlInstruction;
+class ExitIf;
+class ExitLoop;
+class ExitSwitch;
 class Function;
 class If;
 class Load;
@@ -95,6 +100,11 @@
     /// @returns the result ID of the value
     uint32_t Value(ir::Value* value);
 
+    /// Get the result ID of the instruction result `value`, emitting its instruction if necessary.
+    /// @param inst the instruction to get the ID for
+    /// @returns the result ID of the instruction
+    uint32_t Value(ir::Instruction* inst);
+
     /// Get the ID of the label for `block`.
     /// @param block the block to get the label ID for
     /// @returns the ID of the block's label
@@ -146,6 +156,10 @@
     /// @param call the builtin call instruction to emit
     void EmitBuiltinCall(ir::BuiltinCall* call);
 
+    /// Emit a construct instruction.
+    /// @param construct the construct instruction to emit
+    void EmitConstruct(ir::Construct* construct);
+
     /// Emit a load instruction.
     /// @param load the load instruction to emit
     void EmitLoad(ir::Load* load);
@@ -174,6 +188,10 @@
     /// @param b the branch instruction to emit
     void EmitBranch(ir::Branch* b);
 
+    /// Emit the OpPhis for the given flow control instruction.
+    /// @param inst the flow control instruction
+    void EmitExitPhis(ir::ControlInstruction* inst);
+
   private:
     /// Get the result ID of the constant `constant`, emitting its instruction if necessary.
     /// @param constant the constant to get the ID for
@@ -234,6 +252,15 @@
     /// The current function that is being emitted.
     Function current_function_;
 
+    /// The merge block for the current if statement
+    uint32_t if_merge_label_ = 0;
+
+    /// The merge block for the current loop statement
+    uint32_t loop_merge_label_ = 0;
+
+    /// The merge block for the current switch statement
+    uint32_t switch_merge_label_ = 0;
+
     bool zero_init_workgroup_memory_ = false;
 };
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
index 4318147..1f33350 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_access_test.cc
@@ -14,24 +14,22 @@
 
 #include "src/tint/writer/spirv/ir/test_helper_ir.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
-class SpvGeneratorImplTest_Access : public SpvGeneratorImplTest {
-  protected:
-    const type::Pointer* ptr(const type::Type* elem) {
-        return ty.ptr(builtin::AddressSpace::kFunction, elem, builtin::Access::kReadWrite);
-    }
-};
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+using SpvGeneratorImplTest_Access = SpvGeneratorImplTest;
 
 TEST_F(SpvGeneratorImplTest_Access, Array_Value_ConstantIndex) {
     auto* arr_val = b.FunctionParam(ty.array(ty.i32(), 4));
-    auto* access = b.Access(ty.i32(), arr_val, 1_u);
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({arr_val});
-    func->StartTarget()->SetInstructions({access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    sb.Access(ty.i32(), arr_val, 1_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -54,10 +52,12 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Array_Pointer_ConstantIndex) {
-    auto* arr_var = b.Var(ptr(ty.array(ty.i32(), 4)));
-    auto* access = b.Access(ptr(ty.i32()), arr_var, 1_u);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({arr_var, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* arr_var = sb.Var(ty.ptr<function, array<i32, 4>>());
+    sb.Access(ty.ptr<function, i32>(), arr_var, 1_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -83,13 +83,14 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Array_Pointer_DynamicIndex) {
-    auto* arr_var = b.Var(ptr(ty.array(ty.i32(), 4)));
-    auto* idx_var = b.Var(ptr(ty.i32()));
-    auto* idx = b.Load(idx_var);
-    auto* access = b.Access(ptr(ty.i32()), arr_var, idx);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{idx_var, idx, arr_var, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* idx_var = sb.Var(ty.ptr<function, i32>());
+    auto* idx = sb.Load(idx_var);
+    auto* arr_var = sb.Var(ty.ptr<function, array<i32, 4>>());
+    sb.Access(ty.ptr<function, i32>(), arr_var, idx);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -117,11 +118,13 @@
 
 TEST_F(SpvGeneratorImplTest_Access, Matrix_Value_ConstantIndex) {
     auto* mat_val = b.FunctionParam(ty.mat2x2(ty.f32()));
-    auto* access_vec = b.Access(ty.vec2(ty.f32()), mat_val, 1_u);
-    auto* access_el = b.Access(ty.f32(), mat_val, 1_u, 0_u);
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({mat_val});
-    func->StartTarget()->SetInstructions({access_vec, access_el, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    sb.Access(ty.vec2(ty.f32()), mat_val, 1_u);
+    sb.Access(ty.f32(), mat_val, 1_u, 0_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -143,11 +146,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Matrix_Pointer_ConstantIndex) {
-    auto* mat_var = b.Var(ptr(ty.mat2x2(ty.f32())));
-    auto* access_vec = b.Access(ptr(ty.vec2(ty.f32())), mat_var, 1_u);
-    auto* access_el = b.Access(ptr(ty.f32()), mat_var, 1_u, 0_u);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({access_vec, access_el, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* mat_var = sb.Var(ty.ptr<function, mat2x2<f32>>());
+    sb.Access(ty.ptr<function, vec2<f32>>(), mat_var, 1_u);
+    sb.Access(ty.ptr<function, f32>(), mat_var, 1_u, 0_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -155,31 +160,35 @@
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
 %2 = OpTypeVoid
 %3 = OpTypeFunction %2
-%8 = OpTypeFloat 32
-%7 = OpTypeVector %8 2
+%9 = OpTypeFloat 32
+%8 = OpTypeVector %9 2
+%7 = OpTypeMatrix %8 2
 %6 = OpTypePointer Function %7
-%11 = OpTypeInt 32 0
-%10 = OpConstant %11 1
-%13 = OpTypePointer Function %8
-%14 = OpConstant %11 0
+%11 = OpTypePointer Function %8
+%13 = OpTypeInt 32 0
+%12 = OpConstant %13 1
+%15 = OpTypePointer Function %9
+%16 = OpConstant %13 0
 %1 = OpFunction %2 None %3
 %4 = OpLabel
-%5 = OpAccessChain %6 %9 %10
-%12 = OpAccessChain %13 %9 %10 %14
+%5 = OpVariable %6 Function
+%10 = OpAccessChain %11 %5 %12
+%14 = OpAccessChain %15 %5 %12 %16
 OpReturn
 OpFunctionEnd
 )");
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Matrix_Pointer_DynamicIndex) {
-    auto* mat_var = b.Var(ptr(ty.mat2x2(ty.f32())));
-    auto* idx_var = b.Var(ptr(ty.i32()));
-    auto* idx = b.Load(idx_var);
-    auto* access_vec = b.Access(ptr(ty.vec2(ty.f32())), mat_var, idx);
-    auto* access_el = b.Access(ptr(ty.f32()), mat_var, idx, idx);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{idx_var, idx, mat_var, access_vec, access_el, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* idx_var = sb.Var(ty.ptr<function, i32>());
+    auto* idx = sb.Load(idx_var);
+    auto* mat_var = sb.Var(ty.ptr<function, mat2x2<f32>>());
+    sb.Access(ty.ptr<function, vec2<f32>>(), mat_var, idx);
+    sb.Access(ty.ptr<function, f32>(), mat_var, idx, idx);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -208,11 +217,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Vector_Value_ConstantIndex) {
-    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
-    auto* access = b.Access(ty.i32(), vec_val, 1_u);
     auto* func = b.Function("foo", ty.void_());
+    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
     func->SetParams({vec_val});
-    func->StartTarget()->SetInstructions({access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    sb.Access(ty.i32(), vec_val, 1_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -232,13 +243,15 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Vector_Value_DynamicIndex) {
-    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
-    auto* idx_var = b.Var(ptr(ty.i32()));
-    auto* idx = b.Load(idx_var);
-    auto* access = b.Access(ty.i32(), vec_val, idx);
     auto* func = b.Function("foo", ty.void_());
+    auto* vec_val = b.FunctionParam(ty.vec4(ty.i32()));
     func->SetParams({vec_val});
-    func->StartTarget()->SetInstructions({idx_var, idx, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* idx_var = sb.Var(ty.ptr<function, i32>());
+    auto* idx = sb.Load(idx_var);
+    sb.Access(ty.i32(), vec_val, idx);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -261,10 +274,12 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Vector_Pointer_ConstantIndex) {
-    auto* vec_var = b.Var(ptr(ty.vec4(ty.i32())));
-    auto* access = b.Access(ptr(ty.i32()), vec_var, 1_u);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({vec_var, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* vec_var = sb.Var(ty.ptr<function, vec4<i32>>());
+    sb.Access(ty.ptr<function, i32>(), vec_var, 1_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -288,13 +303,14 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Vector_Pointer_DynamicIndex) {
-    auto* vec_var = b.Var(ptr(ty.vec4(ty.i32())));
-    auto* idx_var = b.Var(ptr(ty.i32()));
-    auto* idx = b.Load(idx_var);
-    auto* access = b.Access(ptr(ty.i32()), vec_var, idx);
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{idx_var, idx, vec_var, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* idx_var = sb.Var(ty.ptr<function, i32>());
+    auto* idx = sb.Load(idx_var);
+    auto* vec_var = sb.Var(ty.ptr<function, vec4<i32>>());
+    sb.Access(ty.ptr<function, i32>(), vec_var, idx);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -319,12 +335,14 @@
 
 TEST_F(SpvGeneratorImplTest_Access, NestedVector_Value_DynamicIndex) {
     auto* val = b.FunctionParam(ty.array(ty.array(ty.vec4(ty.i32()), 4), 4));
-    auto* idx_var = b.Var(ptr(ty.i32()));
-    auto* idx = b.Load(idx_var);
-    auto* access = b.Access(ty.i32(), val, 1_u, 2_u, idx);
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({val});
-    func->StartTarget()->SetInstructions({idx_var, idx, access, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* idx_var = sb.Var(ty.ptr<function, i32>());
+    auto* idx = sb.Load(idx_var);
+    sb.Access(ty.i32(), val, 1_u, 2_u, idx);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -354,21 +372,19 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Struct_Value_ConstantIndex) {
-    auto* str = ty.Get<type::Struct>(
-        mod.symbols.Register("MyStruct"),
-        utils::Vector{
-            ty.Get<type::StructMember>(mod.symbols.Register("a"), ty.f32(), 0u, 0u, 4u, 4u,
-                                       type::StructMemberAttributes{}),
-            ty.Get<type::StructMember>(mod.symbols.Register("b"), ty.vec4(ty.i32()), 1u, 16u, 16u,
-                                       16u, type::StructMemberAttributes{}),
-        },
-        16u, 32u, 32u);
+    auto* str =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("b"), ty.vec4<i32>()},
+                                               });
     auto* str_val = b.FunctionParam(str);
-    auto* access_vec = b.Access(ty.i32(), str_val, 1_u);
-    auto* access_el = b.Access(ty.i32(), str_val, 1_u, 2_u);
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({str_val});
-    func->StartTarget()->SetInstructions({access_vec, access_el, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    sb.Access(ty.i32(), str_val, 1_u);
+    sb.Access(ty.i32(), str_val, 1_u, 2_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -396,21 +412,18 @@
 }
 
 TEST_F(SpvGeneratorImplTest_Access, Struct_Pointer_ConstantIndex) {
-    auto* str = ty.Get<type::Struct>(
-        mod.symbols.Register("MyStruct"),
-        utils::Vector{
-            ty.Get<type::StructMember>(mod.symbols.Register("a"), ty.f32(), 0u, 0u, 4u, 4u,
-                                       type::StructMemberAttributes{}),
-            ty.Get<type::StructMember>(mod.symbols.Register("b"), ty.vec4(ty.i32()), 1u, 16u, 16u,
-                                       16u, type::StructMemberAttributes{}),
-        },
-        16u, 32u, 32u);
-    auto* str_var = b.Var(ptr(str));
-    auto* access_vec = b.Access(ptr(ty.i32()), str_var, 1_u);
-    auto* access_el = b.Access(ptr(ty.i32()), str_var, 1_u, 2_u);
+    auto* str =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("b"), ty.vec4<i32>()},
+                                               });
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{str_var, access_vec, access_el, b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* str_var = sb.Var(ty.ptr(function, str, read_write));
+    sb.Access(ty.ptr<function, i32>(), str_var, 1_u);
+    sb.Access(ty.ptr<function, i32>(), str_var, 1_u, 2_u);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
index 6ca13f7..37b702c 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
@@ -37,10 +37,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, MakeScalarType(params.type),
-                               MakeScalarValue(params.type), MakeScalarValue(params.type)),
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, MakeScalarType(params.type), MakeScalarValue(params.type),
+              MakeScalarValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -51,11 +51,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, MakeVectorType(params.type),
-                               MakeVectorValue(params.type), MakeVectorValue(params.type)),
-
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, MakeVectorType(params.type), MakeVectorValue(params.type),
+              MakeVectorValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -88,10 +87,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, MakeScalarType(params.type),
-                               MakeScalarValue(params.type), MakeScalarValue(params.type)),
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, MakeScalarType(params.type), MakeScalarValue(params.type),
+              MakeScalarValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -102,11 +101,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, MakeVectorType(params.type),
-                               MakeVectorValue(params.type), MakeVectorValue(params.type)),
-
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, MakeVectorType(params.type), MakeVectorValue(params.type),
+              MakeVectorValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -131,25 +129,24 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, ty.bool_(), MakeScalarValue(params.type),
-                               MakeScalarValue(params.type)),
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, ty.bool_(), MakeScalarValue(params.type), MakeScalarValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_THAT(DumpModule(generator_.Module()), ::testing::HasSubstr(params.spirv_inst));
 }
+
 TEST_P(Comparison, Vector) {
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions(
-        utils::Vector{b.Binary(params.kind, ty.vec2(ty.bool_()), MakeVectorValue(params.type),
-                               MakeVectorValue(params.type)),
-
-                      b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Binary(params.kind, ty.vec2(ty.bool_()), MakeVectorValue(params.type),
+              MakeVectorValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -204,8 +201,11 @@
 
 TEST_F(SpvGeneratorImplTest, Binary_Chain) {
     auto* func = b.Function("foo", ty.void_());
-    auto* a = b.Subtract(ty.i32(), 1_i, 2_i);
-    func->StartTarget()->SetInstructions({a, b.Add(ty.i32(), a, a), b.Return(func)});
+
+    auto sb = b.With(func->StartTarget());
+    auto* a = sb.Subtract(ty.i32(), 1_i, 2_i);
+    sb.Add(ty.i32(), a, a);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
index 44d0505..ae45e1c 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_builtin_test.cc
@@ -38,10 +38,9 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({
-        b.Call(MakeScalarType(params.type), params.function, MakeScalarValue(params.type)),
-        b.Return(func),
-    });
+    auto sb = b.With(func->StartTarget());
+    sb.Call(MakeScalarType(params.type), params.function, MakeScalarValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -52,10 +51,9 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({
-        b.Call(MakeVectorType(params.type), params.function, MakeVectorValue(params.type)),
-        b.Return(func),
-    });
+    auto sb = b.With(func->StartTarget());
+    sb.Call(MakeVectorType(params.type), params.function, MakeVectorValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -69,12 +67,10 @@
 
 // Test that abs of an unsigned value just folds away.
 TEST_F(SpvGeneratorImplTest, Builtin_Abs_u32) {
-    auto* result = b.Call(MakeScalarType(kU32), builtin::Function::kAbs, MakeScalarValue(kU32));
     auto* func = b.Function("foo", MakeScalarType(kU32));
-    func->StartTarget()->SetInstructions({
-        result,
-        b.Return(func, result),
-    });
+    auto sb = b.With(func->StartTarget());
+    auto* result = sb.Call(MakeScalarType(kU32), builtin::Function::kAbs, MakeScalarValue(kU32));
+    sb.Return(func, result);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -91,12 +87,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Builtin_Abs_vec2u) {
-    auto* result = b.Call(MakeVectorType(kU32), builtin::Function::kAbs, MakeVectorValue(kU32));
     auto* func = b.Function("foo", MakeVectorType(kU32));
-    func->StartTarget()->SetInstructions({
-        result,
-        b.Return(func, result),
-    });
+    auto sb = b.With(func->StartTarget());
+    auto* result = sb.Call(MakeVectorType(kU32), builtin::Function::kAbs, MakeVectorValue(kU32));
+    sb.Return(func, result);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -121,11 +115,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({
-        b.Call(MakeScalarType(params.type), params.function, MakeScalarValue(params.type),
-               MakeScalarValue(params.type)),
-        b.Return(func),
-    });
+    auto sb = b.With(func->StartTarget());
+    sb.Call(MakeScalarType(params.type), params.function, MakeScalarValue(params.type),
+            MakeScalarValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -136,12 +129,10 @@
     auto params = GetParam();
 
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({
-        b.Call(MakeVectorType(params.type), params.function, MakeVectorValue(params.type),
-               MakeVectorValue(params.type)),
-
-        b.Return(func),
-    });
+    auto sb = b.With(func->StartTarget());
+    sb.Call(MakeVectorType(params.type), params.function, MakeVectorValue(params.type),
+            MakeVectorValue(params.type));
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_construct_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_construct_test.cc
new file mode 100644
index 0000000..732c6ff
--- /dev/null
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_construct_test.cc
@@ -0,0 +1,165 @@
+// 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/writer/spirv/ir/test_helper_ir.h"
+
+namespace tint::writer::spirv {
+namespace {
+
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+TEST_F(SpvGeneratorImplTest, Construct_Vector) {
+    auto* func = b.Function("foo", ty.vec4<i32>());
+    func->SetParams({
+        b.FunctionParam(ty.i32()),
+        b.FunctionParam(ty.i32()),
+        b.FunctionParam(ty.i32()),
+        b.FunctionParam(ty.i32()),
+    });
+
+    auto sb = b.With(func->StartTarget());
+    sb.Return(func, sb.Construct(ty.vec4<i32>(), func->Params()));
+
+    ASSERT_TRUE(IRIsValid()) << Error();
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%3 = OpTypeInt 32 1
+%2 = OpTypeVector %3 4
+%8 = OpTypeFunction %2 %3 %3 %3 %3
+%1 = OpFunction %2 None %8
+%4 = OpFunctionParameter %3
+%5 = OpFunctionParameter %3
+%6 = OpFunctionParameter %3
+%7 = OpFunctionParameter %3
+%9 = OpLabel
+%10 = OpCompositeConstruct %2 %4 %5 %6 %7
+OpReturnValue %10
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Construct_Matrix) {
+    auto* func = b.Function("foo", ty.mat3x4<f32>());
+    func->SetParams({
+        b.FunctionParam(ty.vec4<f32>()),
+        b.FunctionParam(ty.vec4<f32>()),
+        b.FunctionParam(ty.vec4<f32>()),
+    });
+
+    auto sb = b.With(func->StartTarget());
+    sb.Return(func, sb.Construct(ty.mat3x4<f32>(), func->Params()));
+
+    ASSERT_TRUE(IRIsValid()) << Error();
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%4 = OpTypeFloat 32
+%3 = OpTypeVector %4 4
+%2 = OpTypeMatrix %3 3
+%8 = OpTypeFunction %2 %3 %3 %3
+%1 = OpFunction %2 None %8
+%5 = OpFunctionParameter %3
+%6 = OpFunctionParameter %3
+%7 = OpFunctionParameter %3
+%9 = OpLabel
+%10 = OpCompositeConstruct %2 %5 %6 %7
+OpReturnValue %10
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Construct_Array) {
+    auto* func = b.Function("foo", ty.array<f32, 4>());
+    func->SetParams({
+        b.FunctionParam(ty.f32()),
+        b.FunctionParam(ty.f32()),
+        b.FunctionParam(ty.f32()),
+        b.FunctionParam(ty.f32()),
+    });
+
+    auto sb = b.With(func->StartTarget());
+    sb.Return(func, sb.Construct(ty.array<f32, 4>(), func->Params()));
+
+    ASSERT_TRUE(IRIsValid()) << Error();
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+OpDecorate %2 ArrayStride 4
+%3 = OpTypeFloat 32
+%5 = OpTypeInt 32 0
+%4 = OpConstant %5 4
+%2 = OpTypeArray %3 %4
+%10 = OpTypeFunction %2 %3 %3 %3 %3
+%1 = OpFunction %2 None %10
+%6 = OpFunctionParameter %3
+%7 = OpFunctionParameter %3
+%8 = OpFunctionParameter %3
+%9 = OpFunctionParameter %3
+%11 = OpLabel
+%12 = OpCompositeConstruct %2 %6 %7 %8 %9
+OpReturnValue %12
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Construct_Struct) {
+    auto* str =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.Register("a"), ty.i32()},
+                                                   {mod.symbols.Register("b"), ty.u32()},
+                                                   {mod.symbols.Register("c"), ty.vec4<f32>()},
+                                               });
+
+    auto* func = b.Function("foo", str);
+    func->SetParams({
+        b.FunctionParam(ty.i32()),
+        b.FunctionParam(ty.u32()),
+        b.FunctionParam(ty.vec4<f32>()),
+    });
+
+    auto sb = b.With(func->StartTarget());
+    sb.Return(func, sb.Construct(str, func->Params()));
+
+    ASSERT_TRUE(IRIsValid()) << Error();
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+OpMemberName %2 0 "a"
+OpMemberName %2 1 "b"
+OpMemberName %2 2 "c"
+OpName %2 "MyStruct"
+OpMemberDecorate %2 0 Offset 0
+OpMemberDecorate %2 1 Offset 4
+OpMemberDecorate %2 2 Offset 16
+%3 = OpTypeInt 32 1
+%4 = OpTypeInt 32 0
+%6 = OpTypeFloat 32
+%5 = OpTypeVector %6 4
+%2 = OpTypeStruct %3 %4 %5
+%10 = OpTypeFunction %2 %3 %4 %5
+%1 = OpFunction %2 None %10
+%7 = OpFunctionParameter %3
+%8 = OpFunctionParameter %4
+%9 = OpFunctionParameter %5
+%11 = OpLabel
+%12 = OpCompositeConstruct %2 %7 %8 %9
+OpReturnValue %12
+OpFunctionEnd
+)");
+}
+
+}  // namespace
+}  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
index 9c67418..81f1091 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
@@ -19,7 +19,7 @@
 
 TEST_F(SpvGeneratorImplTest, Function_Empty) {
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({b.Return(func)});
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -37,7 +37,7 @@
 // Test that we do not emit the same function type more than once.
 TEST_F(SpvGeneratorImplTest, Function_DeduplicateType) {
     auto* func = b.Function("foo", ty.void_());
-    func->StartTarget()->SetInstructions({b.Return(func)});
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -52,7 +52,7 @@
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Compute) {
     auto* func =
         b.Function("main", ty.void_(), ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
-    func->StartTarget()->SetInstructions({b.Return(func)});
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -71,7 +71,7 @@
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Fragment) {
     auto* func = b.Function("main", ty.void_(), ir::Function::PipelineStage::kFragment);
-    func->StartTarget()->SetInstructions({b.Return(func)});
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -90,7 +90,7 @@
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Vertex) {
     auto* func = b.Function("main", ty.void_(), ir::Function::PipelineStage::kVertex);
-    func->StartTarget()->SetInstructions({b.Return(func)});
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -108,13 +108,13 @@
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Multiple) {
     auto* f1 = b.Function("main1", ty.void_(), ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
-    f1->StartTarget()->SetInstructions({b.Return(f1)});
+    f1->StartTarget()->Append(b.Return(f1));
 
     auto* f2 = b.Function("main2", ty.void_(), ir::Function::PipelineStage::kCompute, {{8, 2, 16}});
-    f2->StartTarget()->SetInstructions({b.Return(f2)});
+    f2->StartTarget()->Append(b.Return(f2));
 
     auto* f3 = b.Function("main3", ty.void_(), ir::Function::PipelineStage::kFragment);
-    f3->StartTarget()->SetInstructions({b.Return(f3)});
+    f3->StartTarget()->Append(b.Return(f3));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -149,7 +149,7 @@
 
 TEST_F(SpvGeneratorImplTest, Function_ReturnValue) {
     auto* func = b.Function("foo", ty.i32());
-    func->StartTarget()->SetInstructions({b.Return(func, i32(42))});
+    func->StartTarget()->Append(b.Return(func, i32(42)));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -169,13 +169,15 @@
     auto* i32 = ty.i32();
     auto* x = b.FunctionParam(i32);
     auto* y = b.FunctionParam(i32);
-    auto* result = b.Add(i32, x, y);
     auto* func = b.Function("foo", i32);
     func->SetParams({x, y});
-    func->StartTarget()->SetInstructions({result, b.Return(func, result)});
     mod.SetName(x, "x");
     mod.SetName(y, "y");
 
+    auto sb = b.With(func->StartTarget());
+    auto* result = sb.Add(i32, x, y);
+    sb.Return(func, result);
+
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
@@ -198,14 +200,21 @@
     auto* i32_ty = ty.i32();
     auto* x = b.FunctionParam(i32_ty);
     auto* y = b.FunctionParam(i32_ty);
-    auto* result = b.Add(i32_ty, x, y);
     auto* foo = b.Function("foo", i32_ty);
     foo->SetParams({x, y});
-    foo->StartTarget()->SetInstructions({result, b.Return(foo, result)});
+
+    {
+        auto sb = b.With(foo->StartTarget());
+        auto* result = sb.Add(i32_ty, x, y);
+        sb.Return(foo, result);
+    }
 
     auto* bar = b.Function("bar", ty.void_());
-    bar->StartTarget()->SetInstructions(
-        utils::Vector{b.Call(i32_ty, foo, i32(2), i32(3)), b.Return(bar)});
+    {
+        auto sb = b.With(bar->StartTarget());
+        sb.Call(i32_ty, foo, i32(2), i32(3));
+        sb.Return(bar);
+    }
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -236,11 +245,12 @@
 
 TEST_F(SpvGeneratorImplTest, Function_Call_Void) {
     auto* foo = b.Function("foo", ty.void_());
-    foo->StartTarget()->SetInstructions({b.Return(foo)});
+    foo->StartTarget()->Append(b.Return(foo));
 
     auto* bar = b.Function("bar", ty.void_());
-    bar->StartTarget()->SetInstructions(
-        utils::Vector{b.Call(ty.void_(), foo, utils::Empty), b.Return(bar)});
+    auto sb = b.With(bar->StartTarget());
+    sb.Call(ty.void_(), foo, utils::Empty);
+    sb.Return(bar);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
index 6534a56..62afeda 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
@@ -23,11 +23,10 @@
     auto* func = b.Function("foo", ty.void_());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.ExitIf(i)});
-    i->False()->SetInstructions({b.ExitIf(i)});
-    i->Merge()->SetInstructions({b.Return(func)});
-
-    func->StartTarget()->SetInstructions({i});
+    i->True()->Append(b.ExitIf(i));
+    i->False()->Append(b.ExitIf(i));
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -51,13 +50,14 @@
     auto* func = b.Function("foo", ty.void_());
 
     auto* i = b.If(true);
-    i->False()->SetInstructions({b.ExitIf(i)});
-    i->Merge()->SetInstructions({b.Return(func)});
+    i->False()->Append(b.ExitIf(i));
 
-    auto* true_block = i->True();
-    true_block->SetInstructions({b.Add(ty.i32(), 1_i, 1_i), b.ExitIf(i)});
+    auto tb = b.With(i->True());
+    tb.Add(ty.i32(), 1_i, 1_i);
+    tb.ExitIf(i);
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -86,13 +86,14 @@
     auto* func = b.Function("foo", ty.void_());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.ExitIf(i)});
-    i->Merge()->SetInstructions({b.Return(func)});
+    i->True()->Append(b.ExitIf(i));
 
-    auto* false_block = i->False();
-    false_block->SetInstructions({b.Add(ty.i32(), 1_i, 1_i), b.ExitIf(i)});
+    auto fb = b.With(i->False());
+    fb.Add(ty.i32(), 1_i, 1_i);
+    fb.ExitIf(i);
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -121,10 +122,11 @@
     auto* func = b.Function("foo", ty.void_());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.Return(func)});
-    i->False()->SetInstructions({b.Return(func)});
+    i->True()->Append(b.Return(func));
+    i->False()->Append(b.Return(func));
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Unreachable());
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -149,29 +151,26 @@
 }
 
 TEST_F(SpvGeneratorImplTest, If_Phi_SingleValue) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param = b.BlockParam(b.ir.Types().i32());
+    auto* func = b.Function("foo", ty.i32());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.ExitIf(i, 10_i)});
-    i->False()->SetInstructions({b.ExitIf(i, 20_i)});
-    i->Merge()->SetParams({merge_param});
-    i->Merge()->SetInstructions({b.Return(func, merge_param)});
+    i->SetResults(b.InstructionResult(ty.i32()));
+    i->True()->Append(b.ExitIf(i, 10_i));
+    i->False()->Append(b.ExitIf(i, 20_i));
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func, i));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%10 = OpTypeInt 32 1
-%12 = OpConstant %10 10
-%13 = OpConstant %10 20
+%11 = OpConstant %2 10
+%12 = OpConstant %2 20
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %5 None
@@ -181,36 +180,33 @@
 %7 = OpLabel
 OpBranch %5
 %5 = OpLabel
-%11 = OpPhi %10 %12 %6 %13 %7
-OpReturnValue %11
+%10 = OpPhi %2 %11 %6 %12 %7
+OpReturnValue %10
 OpFunctionEnd
 )");
 }
 
 TEST_F(SpvGeneratorImplTest, If_Phi_SingleValue_TrueReturn) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param = b.BlockParam(b.ir.Types().i32());
+    auto* func = b.Function("foo", ty.i32());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.Return(func, 42_i)});
-    i->False()->SetInstructions({b.ExitIf(i, 20_i)});
-    i->Merge()->SetParams({merge_param});
-    i->Merge()->SetInstructions({b.Return(func, merge_param)});
+    i->SetResults(b.InstructionResult(ty.i32()));
+    i->True()->Append(b.Return(func, 42_i));
+    i->False()->Append(b.ExitIf(i, 20_i));
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func, i));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%11 = OpTypeInt 32 1
-%10 = OpConstant %11 42
-%13 = OpConstant %11 20
+%10 = OpConstant %2 42
+%12 = OpConstant %2 20
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %5 None
@@ -220,36 +216,33 @@
 %7 = OpLabel
 OpBranch %5
 %5 = OpLabel
-%12 = OpPhi %11 %13 %7
-OpReturnValue %12
+%11 = OpPhi %2 %12 %7
+OpReturnValue %11
 OpFunctionEnd
 )");
 }
 
 TEST_F(SpvGeneratorImplTest, If_Phi_SingleValue_FalseReturn) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param = b.BlockParam(b.ir.Types().i32());
+    auto* func = b.Function("foo", ty.i32());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.ExitIf(i, 10_i)});
-    i->False()->SetInstructions({b.Return(func, 42_i)});
-    i->Merge()->SetParams({merge_param});
-    i->Merge()->SetInstructions({b.Return(func, merge_param)});
+    i->SetResults(b.InstructionResult(ty.i32()));
+    i->True()->Append(b.ExitIf(i, 10_i));
+    i->False()->Append(b.Return(func, 42_i));
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func, i));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%11 = OpTypeInt 32 1
-%10 = OpConstant %11 42
-%13 = OpConstant %11 10
+%10 = OpConstant %2 42
+%12 = OpConstant %2 10
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %5 None
@@ -259,38 +252,34 @@
 %7 = OpLabel
 OpReturnValue %10
 %5 = OpLabel
-%12 = OpPhi %11 %13 %6
-OpReturnValue %12
+%11 = OpPhi %2 %12 %6
+OpReturnValue %11
 OpFunctionEnd
 )");
 }
 
-TEST_F(SpvGeneratorImplTest, If_Phi_MultipleValue) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param_0 = b.BlockParam(b.ir.Types().i32());
-    auto* merge_param_1 = b.BlockParam(b.ir.Types().bool_());
+TEST_F(SpvGeneratorImplTest, If_Phi_MultipleValue_0) {
+    auto* func = b.Function("foo", ty.i32());
 
     auto* i = b.If(true);
-    i->True()->SetInstructions({b.ExitIf(i, 10_i, true)});
-    i->False()->SetInstructions({b.ExitIf(i, 20_i, false)});
-    i->Merge()->SetParams({merge_param_0, merge_param_1});
-    i->Merge()->SetInstructions({b.Return(func, merge_param_0)});
+    i->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
+    i->True()->Append(b.ExitIf(i, 10_i, true));
+    i->False()->Append(b.ExitIf(i, 20_i, false));
 
-    func->StartTarget()->SetInstructions({i});
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func, i->Result(0)));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
 %9 = OpTypeBool
 %8 = OpConstantTrue %9
-%10 = OpTypeInt 32 1
-%12 = OpConstant %10 10
-%13 = OpConstant %10 20
-%15 = OpConstantFalse %9
+%11 = OpConstant %2 10
+%12 = OpConstant %2 20
+%14 = OpConstantFalse %9
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %5 None
@@ -300,9 +289,45 @@
 %7 = OpLabel
 OpBranch %5
 %5 = OpLabel
-%11 = OpPhi %10 %12 %6 %13 %7
-%14 = OpPhi %9 %8 %6 %15 %7
-OpReturnValue %11
+%10 = OpPhi %2 %11 %6 %12 %7
+%13 = OpPhi %9 %8 %6 %14 %7
+OpReturnValue %10
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, If_Phi_MultipleValue_1) {
+    auto* func = b.Function("foo", ty.bool_());
+
+    auto* i = b.If(true);
+    i->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
+    i->True()->Append(b.ExitIf(i, 10_i, true));
+    i->False()->Append(b.ExitIf(i, 20_i, false));
+
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func, i->Result(1)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeBool
+%3 = OpTypeFunction %2
+%8 = OpConstantTrue %2
+%9 = OpTypeInt 32 1
+%11 = OpConstant %9 10
+%12 = OpConstant %9 20
+%14 = OpConstantFalse %2
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+OpSelectionMerge %5 None
+OpBranchConditional %8 %6 %7
+%6 = OpLabel
+OpBranch %5
+%7 = OpLabel
+OpBranch %5
+%5 = OpLabel
+%10 = OpPhi %9 %11 %6 %12 %7
+%13 = OpPhi %2 %8 %6 %14 %7
+OpReturnValue %13
 OpFunctionEnd
 )");
 }
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
index c1e3f00..88a25e1 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_loop_test.cc
@@ -26,9 +26,9 @@
 
     loop->Body()->Append(b.Continue(loop));
     loop->Continuing()->Append(b.BreakIf(true, loop));
-    loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -61,9 +61,9 @@
     auto* loop = b.Loop();
 
     loop->Body()->Append(b.ExitLoop(loop));
-    loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -95,13 +95,13 @@
     auto* cond_break = b.If(true);
     cond_break->True()->Append(b.ExitLoop(loop));
     cond_break->False()->Append(b.ExitIf(cond_break));
-    cond_break->Merge()->Append(b.Continue(loop));
 
     loop->Body()->Append(cond_break);
+    loop->Body()->Append(b.Continue(loop));
     loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -140,13 +140,13 @@
     auto* cond_break = b.If(true);
     cond_break->True()->Append(b.Continue(loop));
     cond_break->False()->Append(b.ExitIf(cond_break));
-    cond_break->Merge()->Append(b.ExitLoop(loop));
 
     loop->Body()->Append(cond_break);
+    loop->Body()->Append(b.ExitLoop(loop));
     loop->Continuing()->Append(b.NextIteration(loop));
-    loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -183,10 +183,10 @@
     auto* func = b.Function("foo", ty.void_());
 
     auto* loop = b.Loop();
-
     loop->Body()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Unreachable());
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -215,13 +215,13 @@
 
     auto* loop = b.Loop();
 
-    auto* result = b.Equal(ty.i32(), 1_i, 2_i);
+    auto* result = loop->Body()->Append(b.Equal(ty.i32(), 1_i, 2_i));
+    loop->Body()->Append(b.Continue(loop, result));
 
-    loop->Body()->Append(result);
     loop->Continuing()->Append(b.BreakIf(result, loop));
-    loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -240,6 +240,7 @@
 OpBranch %6
 %6 = OpLabel
 %9 = OpIEqual %10 %11 %12
+OpBranch %7
 %7 = OpLabel
 OpBranchConditional %9 %8 %5
 %8 = OpLabel
@@ -256,13 +257,13 @@
 
     inner_loop->Body()->Append(b.ExitLoop(inner_loop));
     inner_loop->Continuing()->Append(b.NextIteration(inner_loop));
-    inner_loop->Merge()->Append(b.Continue(outer_loop));
 
     outer_loop->Body()->Append(inner_loop);
+    outer_loop->Body()->Append(b.Continue(outer_loop));
     outer_loop->Continuing()->Append(b.BreakIf(true, outer_loop));
-    outer_loop->Merge()->Append(b.Return(func));
 
     func->StartTarget()->Append(outer_loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -305,13 +306,13 @@
 
     inner_loop->Body()->Append(b.Continue(inner_loop));
     inner_loop->Continuing()->Append(b.BreakIf(true, inner_loop));
-    inner_loop->Merge()->Append(b.BreakIf(true, outer_loop));
 
     outer_loop->Body()->Append(b.Continue(outer_loop));
     outer_loop->Continuing()->Append(inner_loop);
-    outer_loop->Merge()->Append(b.Return(func));
+    outer_loop->Continuing()->Append(b.BreakIf(true, outer_loop));
 
     func->StartTarget()->Append(outer_loop);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -349,22 +350,31 @@
 TEST_F(SpvGeneratorImplTest, Loop_Phi_SingleValue) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* l = b.Loop();
-    func->StartTarget()->Append(l);
+    auto fb = b.With(func->StartTarget());
+    auto* l = fb.Loop();
 
-    l->Initializer()->Append(b.NextIteration(l, 1_i));
+    {
+        auto ib = b.With(l->Initializer());
+        ib.NextIteration(l, 1_i, false);
+    }
 
-    auto* loop_param = b.BlockParam(b.ir.Types().i32());
+    auto* loop_param = b.BlockParam(ty.i32());
     l->Body()->SetParams({loop_param});
-    auto* inc = b.Add(b.ir.Types().i32(), loop_param, 1_i);
-    l->Body()->Append(inc);
-    l->Body()->Append(b.Continue(l, inc));
+    {
+        auto lb = b.With(l->Body());
+        auto* inc = lb.Add(ty.i32(), loop_param, 1_i);
+        lb.Continue(l, inc);
+    }
 
-    auto* cont_param = b.BlockParam(b.ir.Types().i32());
+    auto* cont_param = b.BlockParam(ty.i32());
     l->Continuing()->SetParams({cont_param});
-    auto* cmp = b.GreaterThan(b.ir.Types().bool_(), cont_param, 5_i);
-    l->Continuing()->Append(cmp);
-    l->Continuing()->Append(b.BreakIf(cmp, l, cont_param));
+    {
+        auto cb = b.With(l->Continuing());
+        auto* cmp = cb.GreaterThan(ty.bool_(), cont_param, 5_i);
+        cb.BreakIf(cmp, l, cont_param);
+    }
+
+    fb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -393,7 +403,7 @@
 %15 = OpSGreaterThan %16 %13 %17
 OpBranchConditional %15 %9 %6
 %9 = OpLabel
-OpUnreachable
+OpReturn
 OpFunctionEnd
 )");
 }
@@ -401,26 +411,34 @@
 TEST_F(SpvGeneratorImplTest, Loop_Phi_MultipleValue) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* l = b.Loop();
-    func->StartTarget()->Append(l);
+    auto fb = b.With(func->StartTarget());
+    auto* l = fb.Loop();
 
-    l->Initializer()->Append(b.NextIteration(l, 1_i, false));
+    {
+        auto ib = b.With(l->Initializer());
+        ib.NextIteration(l, 1_i, false);
+    }
 
-    auto* loop_param_a = b.BlockParam(b.ir.Types().i32());
-    auto* loop_param_b = b.BlockParam(b.ir.Types().bool_());
+    auto* loop_param_a = b.BlockParam(ty.i32());
+    auto* loop_param_b = b.BlockParam(ty.bool_());
     l->Body()->SetParams({loop_param_a, loop_param_b});
-    auto* inc = b.Add(b.ir.Types().i32(), loop_param_a, 1_i);
-    l->Body()->Append(inc);
-    l->Body()->Append(b.Continue(l, inc, loop_param_b));
+    {
+        auto lb = b.With(l->Body());
+        auto* inc = lb.Add(ty.i32(), loop_param_a, 1_i);
+        lb.Continue(l, inc, loop_param_b);
+    }
 
-    auto* cont_param_a = b.BlockParam(b.ir.Types().i32());
-    auto* cont_param_b = b.BlockParam(b.ir.Types().bool_());
+    auto* cont_param_a = b.BlockParam(ty.i32());
+    auto* cont_param_b = b.BlockParam(ty.bool_());
     l->Continuing()->SetParams({cont_param_a, cont_param_b});
-    auto* cmp = b.GreaterThan(b.ir.Types().bool_(), cont_param_a, 5_i);
-    l->Continuing()->Append(cmp);
-    auto* not_b = b.Not(b.ir.Types().bool_(), cont_param_b);
-    l->Continuing()->Append(not_b);
-    l->Continuing()->Append(b.BreakIf(cmp, l, cont_param_a, not_b));
+    {
+        auto cb = b.With(l->Continuing());
+        auto* cmp = cb.GreaterThan(ty.bool_(), cont_param_a, 5_i);
+        auto* not_b = cb.Not(ty.bool_(), cont_param_b);
+        cb.BreakIf(cmp, l, cont_param_a, not_b);
+    }
+
+    fb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -453,7 +471,7 @@
 %17 = OpLogicalEqual %14 %19 %16
 OpBranchConditional %20 %9 %6
 %9 = OpLabel
-OpUnreachable
+OpReturn
 OpFunctionEnd
 )");
 }
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
index 215aad8..c0f32cf 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_switch_test.cc
@@ -27,9 +27,8 @@
     auto* def_case = b.Case(swtch, utils::Vector{ir::Switch::CaseSelector()});
     def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->Append(b.Return(func));
-
     func->StartTarget()->Append(swtch);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -65,9 +64,8 @@
     auto* def_case = b.Case(swtch, utils::Vector{ir::Switch::CaseSelector()});
     def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->Append(b.Return(func));
-
     func->StartTarget()->Append(swtch);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -110,9 +108,8 @@
                                                  ir::Switch::CaseSelector()});
     def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->Append(b.Return(func));
-
     func->StartTarget()->Append(swtch);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -153,6 +150,7 @@
     def_case->Append(b.Return(func));
 
     func->StartTarget()->Append(swtch);
+    func->StartTarget()->Append(b.Unreachable());
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -186,17 +184,16 @@
     auto* cond_break = b.If(true);
     cond_break->True()->Append(b.ExitSwitch(swtch));
     cond_break->False()->Append(b.ExitIf(cond_break));
-    cond_break->Merge()->Append(b.Return(func));
 
     auto* case_a = b.Case(swtch, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)}});
     case_a->Append(cond_break);
+    case_a->Append(b.Return(func));
 
     auto* def_case = b.Case(swtch, utils::Vector{ir::Switch::CaseSelector()});
     def_case->Append(b.ExitSwitch(swtch));
 
-    swtch->Merge()->Append(b.Return(func));
-
     func->StartTarget()->Append(swtch);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -228,11 +225,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Switch_Phi_SingleValue) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param = b.BlockParam(b.ir.Types().i32());
+    auto* func = b.Function("foo", ty.i32());
 
     auto* s = b.Switch(42_i);
+    s->SetResults(b.InstructionResult(ty.i32()));
     auto* case_a = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                            ir::Switch::CaseSelector{nullptr}});
     case_a->Append(b.ExitSwitch(s, 10_i));
@@ -240,40 +236,38 @@
     auto* case_b = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
     case_b->Append(b.ExitSwitch(s, 20_i));
 
-    s->Merge()->SetParams({merge_param});
-    s->Merge()->Append(b.Return(func));
-
-    func->StartTarget()->SetInstructions({s});
+    func->StartTarget()->Append(s);
+    func->StartTarget()->Append(b.Return(func, s));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
-%7 = OpTypeInt 32 1
-%6 = OpConstant %7 42
-%11 = OpConstant %7 10
-%12 = OpConstant %7 20
+%6 = OpConstant %2 42
+%10 = OpConstant %2 10
+%11 = OpConstant %2 20
 %1 = OpFunction %2 None %3
 %4 = OpLabel
-OpSelectionMerge %9 None
-OpSwitch %6 %5 1 %5 2 %8
+OpSelectionMerge %8 None
+OpSwitch %6 %5 1 %5 2 %7
 %5 = OpLabel
-OpBranch %9
+OpBranch %8
+%7 = OpLabel
+OpBranch %8
 %8 = OpLabel
-OpBranch %9
-%9 = OpLabel
-%10 = OpPhi %7 %11 %5 %12 %8
-OpReturn
+%9 = OpPhi %2 %10 %5 %11 %7
+OpReturnValue %9
 OpFunctionEnd
 )");
 }
 
 TEST_F(SpvGeneratorImplTest, Switch_Phi_SingleValue_CaseReturn) {
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
 
     auto* s = b.Switch(42_i);
+    s->SetResults(b.InstructionResult(ty.i32()));
     auto* case_a = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                            ir::Switch::CaseSelector{nullptr}});
     case_a->Append(b.Return(func, 10_i));
@@ -281,43 +275,38 @@
     auto* case_b = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
     case_b->Append(b.ExitSwitch(s, 20_i));
 
-    s->Merge()->SetParams({b.BlockParam(b.ir.Types().i32())});
-    s->Merge()->Append(b.Return(func));
-
-    func->StartTarget()->SetInstructions({s});
+    func->StartTarget()->Append(s);
+    func->StartTarget()->Append(b.Return(func, s));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
 %3 = OpTypeFunction %2
-%7 = OpTypeInt 32 1
-%6 = OpConstant %7 42
-%10 = OpConstant %7 10
-%12 = OpConstant %7 20
+%6 = OpConstant %2 42
+%9 = OpConstant %2 10
+%11 = OpConstant %2 20
 %1 = OpFunction %2 None %3
 %4 = OpLabel
-OpSelectionMerge %9 None
-OpSwitch %6 %5 1 %5 2 %8
+OpSelectionMerge %8 None
+OpSwitch %6 %5 1 %5 2 %7
 %5 = OpLabel
-OpReturnValue %10
+OpReturnValue %9
+%7 = OpLabel
+OpBranch %8
 %8 = OpLabel
-OpBranch %9
-%9 = OpLabel
-%11 = OpPhi %7 %12 %8
-OpReturn
+%10 = OpPhi %2 %11 %7
+OpReturnValue %10
 OpFunctionEnd
 )");
 }
 
-TEST_F(SpvGeneratorImplTest, Switch_Phi_MultipleValue) {
-    auto* func = b.Function("foo", ty.void_());
-
-    auto* merge_param_0 = b.BlockParam(b.ir.Types().i32());
-    auto* merge_param_1 = b.BlockParam(b.ir.Types().bool_());
+TEST_F(SpvGeneratorImplTest, Switch_Phi_MultipleValue_0) {
+    auto* func = b.Function("foo", ty.i32());
 
     auto* s = b.Switch(42_i);
+    s->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
     auto* case_a = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
                                            ir::Switch::CaseSelector{nullptr}});
     case_a->Append(b.ExitSwitch(s, 10_i, true));
@@ -325,24 +314,62 @@
     auto* case_b = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
     case_b->Append(b.ExitSwitch(s, 20_i, false));
 
-    s->Merge()->SetParams({merge_param_0, merge_param_1});
-    s->Merge()->Append(b.Return(func, merge_param_0));
-
-    func->StartTarget()->SetInstructions({s});
+    func->StartTarget()->Append(s);
+    func->StartTarget()->Append(b.Return(func, s->Result(0)));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
-%2 = OpTypeVoid
+%2 = OpTypeInt 32 1
+%3 = OpTypeFunction %2
+%6 = OpConstant %2 42
+%10 = OpConstant %2 10
+%11 = OpConstant %2 20
+%12 = OpTypeBool
+%14 = OpConstantTrue %12
+%15 = OpConstantFalse %12
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+OpSelectionMerge %8 None
+OpSwitch %6 %5 1 %5 2 %7
+%5 = OpLabel
+OpBranch %8
+%7 = OpLabel
+OpBranch %8
+%8 = OpLabel
+%9 = OpPhi %2 %10 %5 %11 %7
+%13 = OpPhi %12 %14 %5 %15 %7
+OpReturnValue %9
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Switch_Phi_MultipleValue_1) {
+    auto* func = b.Function("foo", ty.bool_());
+
+    auto* s = b.Switch(b.Constant(42_i));
+    s->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
+    auto* case_a = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(1_i)},
+                                           ir::Switch::CaseSelector{nullptr}});
+    case_a->Append(b.ExitSwitch(s, 10_i, true));
+
+    auto* case_b = b.Case(s, utils::Vector{ir::Switch::CaseSelector{b.Constant(2_i)}});
+    case_b->Append(b.ExitSwitch(s, 20_i, false));
+
+    func->StartTarget()->Append(s);
+    func->StartTarget()->Append(b.Return(func, s->Result(1)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeBool
 %3 = OpTypeFunction %2
 %7 = OpTypeInt 32 1
 %6 = OpConstant %7 42
 %11 = OpConstant %7 10
 %12 = OpConstant %7 20
-%13 = OpTypeBool
-%15 = OpConstantTrue %13
-%16 = OpConstantFalse %13
+%14 = OpConstantTrue %2
+%15 = OpConstantFalse %2
 %1 = OpFunction %2 None %3
 %4 = OpLabel
 OpSelectionMerge %9 None
@@ -353,8 +380,8 @@
 OpBranch %9
 %9 = OpLabel
 %10 = OpPhi %7 %11 %5 %12 %8
-%14 = OpPhi %13 %15 %5 %16 %8
-OpReturnValue %10
+%13 = OpPhi %2 %14 %5 %15 %8
+OpReturnValue %13
 OpFunctionEnd
 )");
 }
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_test.cc
index ffafa60..ef33bfb 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_test.cc
@@ -19,6 +19,8 @@
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::number_suffixes;  // NOLINT
+
 TEST_F(SpvGeneratorImplTest, ModuleHeader) {
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     auto got = Disassemble(generator_.Result());
@@ -27,5 +29,39 @@
 )"));
 }
 
+TEST_F(SpvGeneratorImplTest, Unreachable) {
+    auto* func = b.Function("foo", ty.i32());
+
+    auto* i = b.If(true);
+    i->True()->Append(b.Return(func, 10_i));
+    i->False()->Append(b.Return(func, 20_i));
+
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Unreachable());
+
+    ASSERT_TRUE(IRIsValid()) << Error();
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeInt 32 1
+%3 = OpTypeFunction %2
+%9 = OpTypeBool
+%8 = OpConstantTrue %9
+%10 = OpConstant %2 10
+%11 = OpConstant %2 20
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+OpSelectionMerge %5 None
+OpBranchConditional %8 %6 %7
+%6 = OpLabel
+OpReturnValue %10
+%7 = OpLabel
+OpReturnValue %11
+%5 = OpLabel
+OpUnreachable
+OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::writer::spirv
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
index 5deefe0..7cd64e4 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_type_test.cc
@@ -58,6 +58,11 @@
     auto id = generator_.Type(ty.f16());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeFloat 16\n");
+    EXPECT_EQ(DumpInstructions(generator_.Module().Capabilities()),
+              "OpCapability Float16\n"
+              "OpCapability UniformAndStorageBuffer16BitAccess\n"
+              "OpCapability StorageBuffer16BitAccess\n"
+              "OpCapability StorageInputOutput16\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_Vec2i) {
@@ -181,15 +186,11 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Type_Struct) {
-    auto* str = ty.Get<type::Struct>(
-        mod.symbols.Register("MyStruct"),
-        utils::Vector{
-            ty.Get<type::StructMember>(mod.symbols.Register("a"), ty.f32(), 0u, 0u, 4u, 4u,
-                                       type::StructMemberAttributes{}),
-            ty.Get<type::StructMember>(mod.symbols.Register("b"), ty.vec4(ty.i32()), 1u, 16u, 16u,
-                                       16u, type::StructMemberAttributes{}),
-        },
-        16u, 32u, 32u);
+    auto* str =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("b"), ty.vec4<i32>()},
+                                               });
     auto id = generator_.Type(str);
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeFloat 32
@@ -207,17 +208,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Type_Struct_MatrixLayout) {
-    auto* str = ty.Get<type::Struct>(
-        mod.symbols.Register("MyStruct"),
-        utils::Vector{
-            ty.Get<type::StructMember>(mod.symbols.Register("m"), ty.mat3x3(ty.f32()), 0u, 0u, 16u,
-                                       48u, type::StructMemberAttributes{}),
+    auto* str = ty.Struct(
+        mod.symbols.New("MyStruct"),
+        {
+            {mod.symbols.Register("m"), ty.mat3x3<f32>()},
             // Matrices nested inside arrays need layout decorations on the struct member too.
-            ty.Get<type::StructMember>(mod.symbols.Register("arr"),
-                                       ty.array(ty.array(ty.mat2x4(ty.f16()), 4), 4), 1u, 64u, 8u,
-                                       64u, type::StructMemberAttributes{}),
-        },
-        16u, 128u, 128u);
+            {mod.symbols.Register("arr"), ty.array(ty.array(ty.mat2x4<f16>(), 4), 4)},
+        });
     auto id = generator_.Type(str);
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), R"(%4 = OpTypeFloat 32
@@ -237,7 +234,7 @@
 OpMemberDecorate %1 0 MatrixStride 16
 OpDecorate %6 ArrayStride 16
 OpDecorate %5 ArrayStride 64
-OpMemberDecorate %1 1 Offset 64
+OpMemberDecorate %1 1 Offset 48
 OpMemberDecorate %1 1 ColMajor
 OpMemberDecorate %1 1 MatrixStride 8
 )");
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
index 5f161be..feb7c63 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
@@ -15,17 +15,18 @@
 #include "src/tint/type/pointer.h"
 #include "src/tint/writer/spirv/ir/test_helper_ir.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::spirv {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 TEST_F(SpvGeneratorImplTest, FunctionVar_NoInit) {
     auto* func = b.Function("foo", ty.void_());
 
-    func->StartTarget()->SetInstructions(
-        {b.Var(ty.ptr(builtin::AddressSpace::kFunction, ty.i32(), builtin::Access::kReadWrite)),
-         b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Var(ty.ptr<function, i32>());
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -46,11 +47,11 @@
 TEST_F(SpvGeneratorImplTest, FunctionVar_WithInit) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, ty.i32(), builtin::Access::kReadWrite));
+    auto sb = b.With(func->StartTarget());
+    auto* v = sb.Var(ty.ptr<function, i32>());
     v->SetInitializer(b.Constant(42_i));
 
-    func->StartTarget()->SetInstructions({v, b.Return(func)});
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -73,9 +74,10 @@
 TEST_F(SpvGeneratorImplTest, FunctionVar_Name) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, ty.i32(), builtin::Access::kReadWrite));
-    func->StartTarget()->SetInstructions({v, b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    auto* v = sb.Var(ty.ptr<function, i32>());
+    sb.Return(func);
+
     mod.SetName(v, "myvar");
 
     ASSERT_TRUE(IRIsValid()) << Error();
@@ -98,16 +100,17 @@
 TEST_F(SpvGeneratorImplTest, FunctionVar_DeclInsideBlock) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, ty.i32(), builtin::Access::kReadWrite));
-    v->SetInitializer(b.Constant(42_i));
-
     auto* i = b.If(true);
-    i->True()->SetInstructions({v, b.ExitIf(i)});
-    i->False()->SetInstructions({b.Return(func)});
-    i->Merge()->SetInstructions({b.Return(func)});
 
-    func->StartTarget()->SetInstructions({i});
+    auto tb = b.With(i->True());
+    auto* v = tb.Var(ty.ptr<function, i32>());
+    v->SetInitializer(b.Constant(42_i));
+    tb.ExitIf(i);
+
+    i->False()->Append(b.Return(func));
+
+    func->StartTarget()->Append(i);
+    func->StartTarget()->Append(b.Return(func));
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -139,10 +142,12 @@
 TEST_F(SpvGeneratorImplTest, FunctionVar_Load) {
     auto* func = b.Function("foo", ty.void_());
 
+    auto sb = b.With(func->StartTarget());
+
     auto* store_ty = ty.i32();
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, store_ty, builtin::Access::kReadWrite));
-    func->StartTarget()->SetInstructions({v, b.Load(v), b.Return(func)});
+    auto* v = sb.Var(ty.ptr(function, store_ty));
+    sb.Load(v);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -164,9 +169,10 @@
 TEST_F(SpvGeneratorImplTest, FunctionVar_Store) {
     auto* func = b.Function("foo", ty.void_());
 
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kFunction, ty.i32(), builtin::Access::kReadWrite));
-    func->StartTarget()->SetInstructions({v, b.Store(v, 42_i), b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    auto* v = sb.Var(ty.ptr<function, i32>());
+    sb.Store(v, 42_i);
+    sb.Return(func);
 
     ASSERT_TRUE(IRIsValid()) << Error();
 
@@ -187,8 +193,7 @@
 }
 
 TEST_F(SpvGeneratorImplTest, PrivateVar_NoInit) {
-    b.RootBlock()->SetInstructions(
-        {b.Var(ty.ptr(builtin::AddressSpace::kPrivate, ty.i32(), builtin::Access::kReadWrite))});
+    b.RootBlock()->Append(b.Var(ty.ptr<private_, i32>()));
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -209,9 +214,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, PrivateVar_WithInit) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kPrivate, ty.i32(), builtin::Access::kReadWrite));
-    b.RootBlock()->SetInstructions({v});
+    auto* v = b.Var(ty.ptr<private_, i32>());
     v->SetInitializer(b.Constant(42_i));
+    b.RootBlock()->Append(v);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -233,9 +238,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, PrivateVar_Name) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kPrivate, ty.i32(), builtin::Access::kReadWrite));
-    b.RootBlock()->SetInstructions({v});
+    auto* v = b.Var(ty.ptr<private_, i32>());
     v->SetInitializer(b.Constant(42_i));
+    b.RootBlock()->Append(v);
+
     mod.SetName(v, "myvar");
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
@@ -263,14 +269,15 @@
     mod.functions.Push(func);
 
     auto* store_ty = ty.i32();
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kPrivate, store_ty, builtin::Access::kReadWrite));
-    b.RootBlock()->SetInstructions({v});
+    auto* v = b.Var(ty.ptr(private_, store_ty));
     v->SetInitializer(b.Constant(42_i));
+    b.RootBlock()->Append(v);
 
-    auto* load = b.Load(v);
-    auto* add = b.Add(store_ty, v, 1_i);
-    auto* store = b.Store(v, add);
-    func->StartTarget()->SetInstructions({load, add, store, b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Load(v);
+    auto* add = sb.Add(store_ty, v, 1_i);
+    sb.Store(v, add);
+    sb.Return(func);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -296,8 +303,7 @@
 }
 
 TEST_F(SpvGeneratorImplTest, WorkgroupVar) {
-    b.RootBlock()->SetInstructions(
-        {b.Var(ty.ptr(builtin::AddressSpace::kWorkgroup, ty.i32(), builtin::Access::kReadWrite))});
+    b.RootBlock()->Append(b.Var(ty.ptr<workgroup, i32>()));
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -318,9 +324,7 @@
 }
 
 TEST_F(SpvGeneratorImplTest, WorkgroupVar_Name) {
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kWorkgroup, ty.i32(), builtin::Access::kReadWrite));
-    b.RootBlock()->SetInstructions({v});
+    auto* v = b.RootBlock()->Append(b.Var(ty.ptr<workgroup, i32>()));
     mod.SetName(v, "myvar");
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
@@ -348,14 +352,13 @@
     mod.functions.Push(func);
 
     auto* store_ty = ty.i32();
-    auto* v =
-        b.Var(ty.ptr(builtin::AddressSpace::kWorkgroup, store_ty, builtin::Access::kReadWrite));
-    b.RootBlock()->SetInstructions({v});
+    auto* v = b.RootBlock()->Append(b.Var(ty.ptr(workgroup, store_ty)));
 
-    auto* load = b.Load(v);
-    auto* add = b.Add(store_ty, v, 1_i);
-    auto* store = b.Store(v, add);
-    func->StartTarget()->SetInstructions({load, add, store, b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Load(v);
+    auto* add = sb.Add(store_ty, v, 1_i);
+    sb.Store(v, add);
+    sb.Return(func);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -380,8 +383,7 @@
 }
 
 TEST_F(SpvGeneratorImplTest, WorkgroupVar_ZeroInitializeWithExtension) {
-    b.RootBlock()->SetInstructions(
-        {b.Var(ty.ptr(builtin::AddressSpace::kWorkgroup, ty.i32(), builtin::Access::kReadWrite))});
+    b.RootBlock()->Append(b.Var(ty.ptr<workgroup, i32>()));
 
     // Create a generator with the zero_init_workgroup_memory flag set to `true`.
     spirv::GeneratorImplIr gen(&mod, true);
@@ -405,9 +407,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, StorageVar) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kStorage, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<storage, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -435,9 +437,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, StorageVar_Name) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kStorage, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<storage, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
     mod.SetName(v, "myvar");
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
@@ -466,18 +468,19 @@
 }
 
 TEST_F(SpvGeneratorImplTest, StorageVar_LoadAndStore) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kStorage, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<storage, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
 
     auto* func = b.Function("foo", ty.void_(), ir::Function::PipelineStage::kCompute,
                             std::array{1u, 1u, 1u});
     mod.functions.Push(func);
 
-    auto* load = b.Load(v);
-    auto* add = b.Add(ty.i32(), v, 1_i);
-    auto* store = b.Store(v, add);
-    func->StartTarget()->SetInstructions({load, add, store, b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Load(v);
+    auto* add = sb.Add(ty.i32(), v, 1_i);
+    sb.Store(v, add);
+    sb.Return(func);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -515,9 +518,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, UniformVar) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kUniform, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<uniform, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
@@ -545,9 +548,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, UniformVar_Name) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kUniform, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<uniform, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
     mod.SetName(v, "myvar");
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
@@ -576,16 +579,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, UniformVar_Load) {
-    auto* v = b.Var(ty.ptr(builtin::AddressSpace::kUniform, ty.i32(), builtin::Access::kReadWrite));
+    auto* v = b.Var(ty.ptr<uniform, i32>());
     v->SetBindingPoint(0, 0);
-    b.RootBlock()->SetInstructions({v});
+    b.RootBlock()->Append(v);
 
     auto* func = b.Function("foo", ty.void_(), ir::Function::PipelineStage::kCompute,
                             std::array{1u, 1u, 1u});
     mod.functions.Push(func);
 
-    auto* load = b.Load(v);
-    func->StartTarget()->SetInstructions({load, b.Return(func)});
+    auto sb = b.With(func->StartTarget());
+    sb.Load(v);
+    sb.Return(func);
 
     ASSERT_TRUE(generator_.Generate()) << generator_.Diagnostics().str();
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpCapability Shader
diff --git a/src/tint/writer/spirv/scalar_constant.h b/src/tint/writer/spirv/scalar_constant.h
index dd4dfd0..7d79dab 100644
--- a/src/tint/writer/spirv/scalar_constant.h
+++ b/src/tint/writer/spirv/scalar_constant.h
@@ -20,7 +20,7 @@
 #include <cstring>
 #include <functional>
 
-#include "src/tint/number.h"
+#include "src/tint/builtin/number.h"
 #include "src/tint/utils/hash.h"
 
 // Forward declarations
diff --git a/src/tint/writer/wgsl/generator_impl_cast_test.cc b/src/tint/writer/wgsl/generator_impl_cast_test.cc
index 890c52c..16c20e0 100644
--- a/src/tint/writer/wgsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_cast_test.cc
@@ -17,11 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::wgsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, EmitExpression_Cast_Scalar_F32_From_I32) {
@@ -51,7 +52,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, EmitExpression_Cast_Vector_F32_From_I32) {
-    auto* cast = vec3<f32>(vec3<i32>(1_i, 2_i, 3_i));
+    auto* cast = Call<vec3<f32>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(cast);
 
     GeneratorImpl& gen = Build();
@@ -65,7 +66,7 @@
 TEST_F(WgslGeneratorImplTest, EmitExpression_Cast_Vector_F16_From_I32) {
     Enable(builtin::Extension::kF16);
 
-    auto* cast = vec3<f16>(vec3<i32>(1_i, 2_i, 3_i));
+    auto* cast = Call<vec3<f16>>(Call<vec3<i32>>(1_i, 2_i, 3_i));
     WrapInFunction(cast);
 
     GeneratorImpl& gen = Build();
diff --git a/src/tint/writer/wgsl/generator_impl_constructor_test.cc b/src/tint/writer/wgsl/generator_impl_constructor_test.cc
index fa40ff6..e360be2 100644
--- a/src/tint/writer/wgsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_constructor_test.cc
@@ -18,11 +18,12 @@
 
 using ::testing::HasSubstr;
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::wgsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using WgslGeneratorImplTest_Constructor = TestHelper;
 
 TEST_F(WgslGeneratorImplTest_Constructor, Bool) {
@@ -129,7 +130,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Vec_F32) {
-    WrapInFunction(vec3<f32>(1_f, 2_f, 3_f));
+    WrapInFunction(Call<vec3<f32>>(1_f, 2_f, 3_f));
 
     GeneratorImpl& gen = Build();
 
@@ -141,7 +142,7 @@
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Vec_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(vec3<f16>(1_h, 2_h, 3_h));
+    WrapInFunction(Call<vec3<f16>>(1_h, 2_h, 3_h));
 
     GeneratorImpl& gen = Build();
 
@@ -151,7 +152,8 @@
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Mat_F32) {
-    WrapInFunction(mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(3_f, 4_f, 5_f)));
+    WrapInFunction(
+        Call<mat2x3<f32>>(Call<vec3<f32>>(1_f, 2_f, 3_f), Call<vec3<f32>>(3_f, 4_f, 5_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -164,7 +166,8 @@
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Mat_F16) {
     Enable(builtin::Extension::kF16);
 
-    WrapInFunction(mat2x3<f16>(vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(3_h, 4_h, 5_h)));
+    WrapInFunction(
+        Call<mat2x3<f16>>(Call<vec3<f16>>(1_h, 2_h, 3_h), Call<vec3<f16>>(3_h, 4_h, 5_h)));
 
     GeneratorImpl& gen = Build();
 
@@ -175,8 +178,8 @@
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Array) {
-    WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u), vec3<f32>(1_f, 2_f, 3_f),
-                        vec3<f32>(4_f, 5_f, 6_f), vec3<f32>(7_f, 8_f, 9_f)));
+    WrapInFunction(Call(ty.array<vec3<f32>, 3>(), Call<vec3<f32>>(1_f, 2_f, 3_f),
+                        Call<vec3<f32>>(4_f, 5_f, 6_f), Call<vec3<f32>>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
 
@@ -188,8 +191,8 @@
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_ImplicitArray) {
-    WrapInFunction(Call(ty.array<Infer>(), vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f),
-                        vec3<f32>(7_f, 8_f, 9_f)));
+    WrapInFunction(Call(ty.array<Infer>(), Call<vec3<f32>>(1_f, 2_f, 3_f),
+                        Call<vec3<f32>>(4_f, 5_f, 6_f), Call<vec3<f32>>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
 
diff --git a/src/tint/writer/wgsl/generator_impl_type_test.cc b/src/tint/writer/wgsl/generator_impl_type_test.cc
index cb790cb..22d7bf0 100644
--- a/src/tint/writer/wgsl/generator_impl_type_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_type_test.cc
@@ -22,11 +22,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::wgsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, EmitType_Alias) {
@@ -146,7 +147,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, EmitType_Pointer) {
-    auto type = Alias("make_type_reachable", ty.ptr<f32>(builtin::AddressSpace::kWorkgroup))->type;
+    auto type = Alias("make_type_reachable", ty.ptr<workgroup, f32>())->type;
 
     GeneratorImpl& gen = Build();
 
@@ -157,9 +158,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, EmitType_PointerAccessMode) {
-    auto type = Alias("make_type_reachable",
-                      ty.ptr<f32>(builtin::AddressSpace::kStorage, builtin::Access::kReadWrite))
-                    ->type;
+    auto type = Alias("make_type_reachable", ty.ptr<storage, f32, read_write>())->type;
 
     GeneratorImpl& gen = Build();
 
diff --git a/src/tint/writer/wgsl/generator_impl_variable_decl_statement_test.cc b/src/tint/writer/wgsl/generator_impl_variable_decl_statement_test.cc
index 59d761e..f4c271b 100644
--- a/src/tint/writer/wgsl/generator_impl_variable_decl_statement_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_variable_decl_statement_test.cc
@@ -17,11 +17,12 @@
 
 #include "gmock/gmock.h"
 
-using namespace tint::number_suffixes;  // NOLINT
-
 namespace tint::writer::wgsl {
 namespace {
 
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement) {
@@ -167,7 +168,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_AInt) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1_a, 2_a, 3_a));
+    auto* C = Const("C", Call<vec3<Infer>>(1_a, 2_a, 3_a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -185,7 +186,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_AFloat) {
-    auto* C = Const("C", Call(ty.vec3<Infer>(), 1._a, 2._a, 3._a));
+    auto* C = Const("C", Call<vec3<Infer>>(1._a, 2._a, 3._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -203,7 +204,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_f32) {
-    auto* C = Const("C", vec3<f32>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<vec3<f32>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -223,7 +224,7 @@
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_vec3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", vec3<f16>(1_h, 2_h, 3_h));
+    auto* C = Const("C", Call<vec3<f16>>(1_h, 2_h, 3_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -243,7 +244,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_AFloat) {
-    auto* C = Const("C", Call(ty.mat2x3<Infer>(), 1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
+    auto* C = Const("C", Call<mat2x3<Infer>>(1._a, 2._a, 3._a, 4._a, 5._a, 6._a));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -261,7 +262,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_f32) {
-    auto* C = Const("C", mat2x3<f32>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
+    auto* C = Const("C", Call<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -281,7 +282,7 @@
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_mat2x3_f16) {
     Enable(builtin::Extension::kF16);
 
-    auto* C = Const("C", mat2x3<f16>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
+    auto* C = Const("C", Call<mat2x3<f16>>(1_h, 2_h, 3_h, 4_h, 5_h, 6_h));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -301,7 +302,7 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_arr_f32) {
-    auto* C = Const("C", array<f32, 3>(1_f, 2_f, 3_f));
+    auto* C = Const("C", Call<array<f32, 3>>(1_f, 2_f, 3_f));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),
@@ -319,10 +320,10 @@
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_VariableDeclStatement_Const_arr_vec2_bool) {
-    auto* C = Const("C", Call(ty.array(ty.vec2<bool>(), 3_u),  //
-                              vec2<bool>(true, false),         //
-                              vec2<bool>(false, true),         //
-                              vec2<bool>(true, true)));
+    auto* C = Const("C", Call<array<vec2<bool>, 3>>(         //
+                             Call<vec2<bool>>(true, false),  //
+                             Call<vec2<bool>>(false, true),  //
+                             Call<vec2<bool>>(true, true)));
     Func("f", utils::Empty, ty.void_(),
          utils::Vector{
              Decl(C),