Import Tint changes from Dawn

Changes:
  - 517278ac08a8b275d20352fd0cf131ea7ddc06ef Move text/unicode into utils. by dan sinclair <dsinclair@chromium.org>
  - 12fa30389926a804e6f25414ea987817e2c364e0 Move castable into utils. by dan sinclair <dsinclair@chromium.org>
  - e6e5998e2b4c0178fa73e29acd697b21bcabdfc5 Cleanup GLSL writer. by dan sinclair <dsinclair@chromium.org>
  - c480632ed60e27a782be9077bba56d5d2f006e9c Setup ArrayLength transform for MSL benchmark by dan sinclair <dsinclair@chromium.org>
  - e16ed7ccc28acce334947ad3e9dba589b7724874 Make more SymbolTable methods string_view. by dan sinclair <dsinclair@chromium.org>
  - 055900549456dc9e88486011108a8fc23383d75b Move traits into utils. by dan sinclair <dsinclair@chromium.org>
  - d21e2d64ec279e614038c2ecd835289b6e0079b8 Convert VariableUser vector over to utils::Vector. by dan sinclair <dsinclair@chromium.org>
  - 8fc9b862143991d28ad930e5dd00a3c28a92a018 CMake: Add TINT_EXTERNAL_BENCHMARK_CORPUS_DIR by Ben Clayton <bclayton@google.com>
  - 5f4847c23ea63ec4a606008a7832f8669b1899d2 tint/sem: Make BindingPoint optional by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 517278ac08a8b275d20352fd0cf131ea7ddc06ef
Change-Id: I9d9436cee5bc5f69a99af9d88990c24d63f86a13
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/128200
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/include/tint/tint.h b/include/tint/tint.h
index 847181b..0c1c4d9 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -24,7 +24,6 @@
 #include "src/tint/diagnostic/printer.h"
 #include "src/tint/inspector/inspector.h"
 #include "src/tint/reader/reader.h"
-#include "src/tint/text/unicode.h"
 #include "src/tint/transform/first_index_offset.h"
 #include "src/tint/transform/manager.h"
 #include "src/tint/transform/renamer.h"
@@ -32,6 +31,7 @@
 #include "src/tint/transform/substitute_override.h"
 #include "src/tint/transform/vertex_pulling.h"
 #include "src/tint/type/manager.h"
+#include "src/tint/utils/unicode.h"
 #include "src/tint/writer/array_length_from_uniform_options.h"
 #include "src/tint/writer/binding_point.h"
 #include "src/tint/writer/binding_remapper_options.h"
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 5240dd2..663ba05 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -195,8 +195,6 @@
 
 libtint_source_set("libtint_base_src") {
   sources = [
-    "castable.cc",
-    "castable.h",
     "debug.cc",
     "debug.h",
     "diagnostic/diagnostic.cc",
@@ -218,11 +216,12 @@
     "symbol.h",
     "symbol_table.cc",
     "symbol_table.h",
-    "traits.h",
     "utils/bitcast.h",
     "utils/bitset.h",
     "utils/block_allocator.h",
     "utils/bump_allocator.h",
+    "utils/castable.cc",
+    "utils/castable.h",
     "utils/compiler_macros.h",
     "utils/concat.h",
     "utils/crc32.h",
@@ -243,6 +242,9 @@
     "utils/string.h",
     "utils/string_stream.cc",
     "utils/string_stream.h",
+    "utils/traits.h",
+    "utils/unicode.cc",
+    "utils/unicode.h",
     "utils/unique_allocator.h",
     "utils/unique_vector.h",
     "utils/vector.h",
@@ -255,8 +257,6 @@
   } else {
     sources += [ "diagnostic/printer_other.cc" ]
   }
-
-  deps = [ ":libtint_text_src" ]
 }
 
 libtint_source_set("libtint_clone_context_hdrs") {
@@ -303,7 +303,6 @@
     ":libtint_builtins_src",
     ":libtint_constant_src",
     ":libtint_sem_src",
-    ":libtint_text_src",
     ":libtint_type_src",
   ]
 }
@@ -333,13 +332,6 @@
   ]
 }
 
-libtint_source_set("libtint_text_src") {
-  sources = [
-    "text/unicode.cc",
-    "text/unicode.h",
-  ]
-}
-
 libtint_source_set("libtint_transform_src") {
   sources = [
     "transform/add_block_attribute.cc",
@@ -451,7 +443,6 @@
     ":libtint_builtins_src",
     ":libtint_program_src",
     ":libtint_sem_src",
-    ":libtint_text_src",
     ":libtint_type_src",
   ]
 }
@@ -974,7 +965,6 @@
     ":libtint_builtins_src",
     ":libtint_program_src",
     ":libtint_reader_src",
-    ":libtint_text_src",
     ":libtint_type_src",
   ]
 }
@@ -1089,7 +1079,6 @@
     ":libtint_inspector_src",
     ":libtint_program_src",
     ":libtint_sem_src",
-    ":libtint_text_src",
     ":libtint_transform_src",
     ":libtint_type_src",
     ":libtint_writer_src",
@@ -1511,11 +1500,6 @@
     ]
   }
 
-  tint_unittests_source_set("tint_unittests_text_src") {
-    sources = [ "text/unicode_test.cc" ]
-    deps = [ ":libtint_text_src" ]
-  }
-
   tint_unittests_source_set("tint_unittests_transform_src") {
     sources = [
       "transform/add_block_attribute_test.cc",
@@ -1590,6 +1574,7 @@
       "utils/bitset_test.cc",
       "utils/block_allocator_test.cc",
       "utils/bump_allocator_test.cc",
+      "utils/castable_test.cc",
       "utils/crc32_test.cc",
       "utils/defer_test.cc",
       "utils/enum_set_test.cc",
@@ -1606,7 +1591,9 @@
       "utils/slice_test.cc",
       "utils/string_stream_test.cc",
       "utils/string_test.cc",
+      "utils/traits_test.cc",
       "utils/transform_test.cc",
+      "utils/unicode_test.cc",
       "utils/unique_allocator_test.cc",
       "utils/unique_vector_test.cc",
       "utils/vector_test.cc",
@@ -1981,7 +1968,6 @@
 
   tint_unittests_source_set("tint_unittests_base_src") {
     sources = [
-      "castable_test.cc",
       "debug_test.cc",
       "number_test.cc",
       "reflection_test.cc",
@@ -1990,7 +1976,6 @@
       "switch_test.cc",
       "symbol_table_test.cc",
       "symbol_test.cc",
-      "traits_test.cc",
     ]
     deps = [ ":libtint_base_src" ]
   }
@@ -2036,7 +2021,6 @@
       ":tint_unittests_inspector_src",
       ":tint_unittests_resolver_src",
       ":tint_unittests_sem_src",
-      ":tint_unittests_text_src",
       ":tint_unittests_transform_src",
       ":tint_unittests_type_src",
       ":tint_unittests_utils_src",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 8ac5b15..323ed79 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -66,10 +66,10 @@
   diagnostic/formatter.h
   diagnostic/printer.cc
   diagnostic/printer.h
-  text/unicode.cc
-  text/unicode.h
   utils/debugger.cc
   utils/debugger.h
+  utils/unicode.cc
+  utils/unicode.h
 )
 tint_default_compile_options(tint_diagnostic_utils)
 
@@ -230,8 +230,6 @@
   ast/while_statement.h
   ast/workgroup_attribute.cc
   ast/workgroup_attribute.h
-  castable.cc
-  castable.h
   clone_context.cc
   clone_context.h
   constant/clone_context.h
@@ -348,7 +346,6 @@
   symbol.cc
   symbol.h
   tint.cc
-  traits.h
   transform/add_empty_entry_point.cc
   transform/add_empty_entry_point.h
   transform/add_block_attribute.cc
@@ -518,6 +515,8 @@
   utils/bitset.h
   utils/block_allocator.h
   utils/bump_allocator.h
+  utils/castable.cc
+  utils/castable.h
   utils/compiler_macros.h
   utils/concat.h
   utils/crc32.h
@@ -535,6 +534,7 @@
   utils/string.h
   utils/string_stream.cc
   utils/string_stream.h
+  utils/traits.h
   utils/unique_allocator.h
   utils/unique_vector.h
   utils/vector.h
@@ -882,7 +882,6 @@
     ast/variable_test.cc
     ast/while_statement_test.cc
     ast/workgroup_attribute_test.cc
-    castable_test.cc
     clone_context_test.cc
     constant/composite_test.cc
     constant/scalar_test.cc
@@ -968,8 +967,6 @@
     symbol_table_test.cc
     symbol_test.cc
     test_main.cc
-    text/unicode_test.cc
-    traits_test.cc
     transform/transform_test.cc
     type/array_test.cc
     type/atomic_test.cc
@@ -997,6 +994,7 @@
     utils/bitset_test.cc
     utils/block_allocator_test.cc
     utils/bump_allocator_test.cc
+    utils/castable_test.cc
     utils/crc32_test.cc
     utils/defer_test.cc
     utils/enum_set_test.cc
@@ -1013,7 +1011,9 @@
     utils/slice_test.cc
     utils/string_stream_test.cc
     utils/string_test.cc
+    utils/traits_test.cc
     utils/transform_test.cc
+    utils/unicode_test.cc
     utils/unique_allocator_test.cc
     utils/unique_vector_test.cc
     utils/vector_test.cc
@@ -1494,4 +1494,27 @@
   tint_core_compile_options(tint-benchmark)
 
   target_link_libraries(tint-benchmark PRIVATE benchmark::benchmark libtint)
+
+  if (TINT_EXTERNAL_BENCHMARK_CORPUS_DIR)
+    # Glob all the files at TINT_EXTERNAL_BENCHMARK_CORPUS_DIR, and create a header
+    # that lists these with the TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS() macro
+    set(TINT_BENCHMARK_GEN_DIR "${DAWN_BUILD_GEN_DIR}/src/tint/benchmark/")
+    set(TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER "${TINT_BENCHMARK_GEN_DIR}/external_wgsl_programs.h")
+    message("Globbing ${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}...")
+    file(GLOB_RECURSE
+      TINT_EXTERNAL_BENCHMARK_FILES
+      RELATIVE "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}"
+      "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/**.wgsl")
+    list(TRANSFORM TINT_EXTERNAL_BENCHMARK_FILES REPLACE
+      "(.+)"
+      "    BENCHMARK_CAPTURE\(FUNC, \"\\1\", \"${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/\\1\")")
+    list(JOIN TINT_EXTERNAL_BENCHMARK_FILES "; \\\n" TINT_EXTERNAL_BENCHMARK_FILES)
+    file(CONFIGURE
+      OUTPUT "${TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER}"
+      CONTENT "#define TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS(FUNC) \\
+${TINT_EXTERNAL_BENCHMARK_FILES};")
+    # Define TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER to the generated header path
+    target_compile_definitions(tint-benchmark PRIVATE
+      "TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER=\"${TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER}\"")
+  endif()
 endif(TINT_BUILD_BENCHMARKS)
diff --git a/src/tint/ast/accessor_expression.h b/src/tint/ast/accessor_expression.h
index d668f1f..808ac2f 100644
--- a/src/tint/ast/accessor_expression.h
+++ b/src/tint/ast/accessor_expression.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// Base class for IndexAccessorExpression and MemberAccessorExpression
-class AccessorExpression : public Castable<AccessorExpression, Expression> {
+class AccessorExpression : public utils::Castable<AccessorExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/alias.h b/src/tint/ast/alias.h
index c30d387..519a878 100644
--- a/src/tint/ast/alias.h
+++ b/src/tint/ast/alias.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A type alias type. Holds a name and pointer to another type.
-class Alias final : public Castable<Alias, TypeDecl> {
+class Alias final : public utils::Castable<Alias, TypeDecl> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/assignment_statement.h b/src/tint/ast/assignment_statement.h
index 01a1a75..482ec4e 100644
--- a/src/tint/ast/assignment_statement.h
+++ b/src/tint/ast/assignment_statement.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// An assignment statement
-class AssignmentStatement final : public Castable<AssignmentStatement, Statement> {
+class AssignmentStatement final : public utils::Castable<AssignmentStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/attribute.h b/src/tint/ast/attribute.h
index 1809782..66c309d 100644
--- a/src/tint/ast/attribute.h
+++ b/src/tint/ast/attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// The base class for all attributes
-class Attribute : public Castable<Attribute, Node> {
+class Attribute : public utils::Castable<Attribute, Node> {
   public:
     ~Attribute() override;
 
diff --git a/src/tint/ast/binary_expression.h b/src/tint/ast/binary_expression.h
index b314250..0382a9b 100644
--- a/src/tint/ast/binary_expression.h
+++ b/src/tint/ast/binary_expression.h
@@ -43,7 +43,7 @@
 };
 
 /// An binary expression
-class BinaryExpression final : public Castable<BinaryExpression, Expression> {
+class BinaryExpression final : public utils::Castable<BinaryExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/binding_attribute.h b/src/tint/ast/binding_attribute.h
index 3ff7c97..37c8224 100644
--- a/src/tint/ast/binding_attribute.h
+++ b/src/tint/ast/binding_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A binding attribute
-class BindingAttribute final : public Castable<BindingAttribute, Attribute> {
+class BindingAttribute final : public utils::Castable<BindingAttribute, Attribute> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/bitcast_expression.h b/src/tint/ast/bitcast_expression.h
index 7737721..d004ddf 100644
--- a/src/tint/ast/bitcast_expression.h
+++ b/src/tint/ast/bitcast_expression.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A bitcast expression
-class BitcastExpression final : public Castable<BitcastExpression, Expression> {
+class BitcastExpression final : public utils::Castable<BitcastExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/block_statement.h b/src/tint/ast/block_statement.h
index 2044b5b..8582ed3 100644
--- a/src/tint/ast/block_statement.h
+++ b/src/tint/ast/block_statement.h
@@ -27,7 +27,7 @@
 namespace tint::ast {
 
 /// A block statement
-class BlockStatement final : public Castable<BlockStatement, Statement> {
+class BlockStatement final : public utils::Castable<BlockStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/bool_literal_expression.h b/src/tint/ast/bool_literal_expression.h
index bebd924..fe509df 100644
--- a/src/tint/ast/bool_literal_expression.h
+++ b/src/tint/ast/bool_literal_expression.h
@@ -22,7 +22,8 @@
 namespace tint::ast {
 
 /// A boolean literal
-class BoolLiteralExpression final : public Castable<BoolLiteralExpression, LiteralExpression> {
+class BoolLiteralExpression final
+    : public utils::Castable<BoolLiteralExpression, LiteralExpression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/break_if_statement.h b/src/tint/ast/break_if_statement.h
index a366240..56cbabc 100644
--- a/src/tint/ast/break_if_statement.h
+++ b/src/tint/ast/break_if_statement.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A break-if statement
-class BreakIfStatement final : public Castable<BreakIfStatement, Statement> {
+class BreakIfStatement final : public utils::Castable<BreakIfStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/break_statement.h b/src/tint/ast/break_statement.h
index d9676bb..a22bf03 100644
--- a/src/tint/ast/break_statement.h
+++ b/src/tint/ast/break_statement.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An break statement
-class BreakStatement final : public Castable<BreakStatement, Statement> {
+class BreakStatement final : public utils::Castable<BreakStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/builtin_attribute.h b/src/tint/ast/builtin_attribute.h
index 2ed6a2d..8844757 100644
--- a/src/tint/ast/builtin_attribute.h
+++ b/src/tint/ast/builtin_attribute.h
@@ -27,7 +27,7 @@
 namespace tint::ast {
 
 /// A builtin attribute
-class BuiltinAttribute final : public Castable<BuiltinAttribute, Attribute> {
+class BuiltinAttribute final : public utils::Castable<BuiltinAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/call_expression.h b/src/tint/ast/call_expression.h
index 6660494..8ad4cf1 100644
--- a/src/tint/ast/call_expression.h
+++ b/src/tint/ast/call_expression.h
@@ -29,7 +29,7 @@
 /// * sem::Builtin
 /// * sem::ValueConstructor
 /// * sem::ValueConversion
-class CallExpression final : public Castable<CallExpression, Expression> {
+class CallExpression final : public utils::Castable<CallExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/call_statement.h b/src/tint/ast/call_statement.h
index 9af47a1..ec561a1 100644
--- a/src/tint/ast/call_statement.h
+++ b/src/tint/ast/call_statement.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A call expression
-class CallStatement final : public Castable<CallStatement, Statement> {
+class CallStatement final : public utils::Castable<CallStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/case_selector.h b/src/tint/ast/case_selector.h
index f2bc1b3..e381557 100644
--- a/src/tint/ast/case_selector.h
+++ b/src/tint/ast/case_selector.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A case selector
-class CaseSelector final : public Castable<CaseSelector, Node> {
+class CaseSelector final : public utils::Castable<CaseSelector, Node> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/case_statement.h b/src/tint/ast/case_statement.h
index a06c720..fd0d18d 100644
--- a/src/tint/ast/case_statement.h
+++ b/src/tint/ast/case_statement.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A case statement
-class CaseStatement final : public Castable<CaseStatement, Statement> {
+class CaseStatement final : public utils::Castable<CaseStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/compound_assignment_statement.h b/src/tint/ast/compound_assignment_statement.h
index bb1f34f..f23af15 100644
--- a/src/tint/ast/compound_assignment_statement.h
+++ b/src/tint/ast/compound_assignment_statement.h
@@ -22,7 +22,8 @@
 namespace tint::ast {
 
 /// A compound assignment statement
-class CompoundAssignmentStatement final : public Castable<CompoundAssignmentStatement, Statement> {
+class CompoundAssignmentStatement final
+    : public utils::Castable<CompoundAssignmentStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/const.h b/src/tint/ast/const.h
index 95727ce..b008015 100644
--- a/src/tint/ast/const.h
+++ b/src/tint/ast/const.h
@@ -30,7 +30,7 @@
 ///   const max_f32 : f32 = 0x1.fffffep+127;    // f32 typed constant
 /// ```
 /// @see https://www.w3.org/TR/WGSL/#creation-time-consts
-class Const final : public Castable<Const, Variable> {
+class Const final : public utils::Castable<Const, Variable> {
   public:
     /// Create a 'const' creation-time value variable.
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/const_assert.h b/src/tint/ast/const_assert.h
index 6774f5e..8de59e3 100644
--- a/src/tint/ast/const_assert.h
+++ b/src/tint/ast/const_assert.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A `const_assert` statement
-class ConstAssert final : public Castable<ConstAssert, Statement> {
+class ConstAssert final : public utils::Castable<ConstAssert, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/continue_statement.h b/src/tint/ast/continue_statement.h
index 10af08b..338a26e 100644
--- a/src/tint/ast/continue_statement.h
+++ b/src/tint/ast/continue_statement.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An continue statement
-class ContinueStatement final : public Castable<ContinueStatement, Statement> {
+class ContinueStatement final : public utils::Castable<ContinueStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/diagnostic_attribute.h b/src/tint/ast/diagnostic_attribute.h
index ef1fa02..fa63f83 100644
--- a/src/tint/ast/diagnostic_attribute.h
+++ b/src/tint/ast/diagnostic_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A diagnostic attribute
-class DiagnosticAttribute final : public Castable<DiagnosticAttribute, Attribute> {
+class DiagnosticAttribute final : public utils::Castable<DiagnosticAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/diagnostic_directive.h b/src/tint/ast/diagnostic_directive.h
index 979f32c..b944384 100644
--- a/src/tint/ast/diagnostic_directive.h
+++ b/src/tint/ast/diagnostic_directive.h
@@ -29,7 +29,7 @@
 ///   // Turn off diagnostics for derivative uniformity violations.
 ///   diagnostic(off, derivative_uniformity);
 /// ```
-class DiagnosticDirective final : public Castable<DiagnosticDirective, Node> {
+class DiagnosticDirective final : public utils::Castable<DiagnosticDirective, Node> {
   public:
     /// Create a extension
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/disable_validation_attribute.h b/src/tint/ast/disable_validation_attribute.h
index 9614bc1..4cc0c08 100644
--- a/src/tint/ast/disable_validation_attribute.h
+++ b/src/tint/ast/disable_validation_attribute.h
@@ -53,7 +53,7 @@
 /// violations. Typically generated by transforms that need to produce ASTs that
 /// would otherwise cause validation errors.
 class DisableValidationAttribute final
-    : public Castable<DisableValidationAttribute, InternalAttribute> {
+    : public utils::Castable<DisableValidationAttribute, InternalAttribute> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/discard_statement.h b/src/tint/ast/discard_statement.h
index a764683..99d8ee7 100644
--- a/src/tint/ast/discard_statement.h
+++ b/src/tint/ast/discard_statement.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// A discard statement
-class DiscardStatement final : public Castable<DiscardStatement, Statement> {
+class DiscardStatement final : public utils::Castable<DiscardStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/enable.h b/src/tint/ast/enable.h
index 53f0218..ac7181a 100644
--- a/src/tint/ast/enable.h
+++ b/src/tint/ast/enable.h
@@ -28,7 +28,7 @@
 ///   // Enable an extension named "f16"
 ///   enable f16;
 /// ```
-class Enable final : public Castable<Enable, Node> {
+class Enable final : public utils::Castable<Enable, Node> {
   public:
     /// Create a extension
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/expression.h b/src/tint/ast/expression.h
index e4a94cb..e9fce0b 100644
--- a/src/tint/ast/expression.h
+++ b/src/tint/ast/expression.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// Base expression class
-class Expression : public Castable<Expression, Node> {
+class Expression : public utils::Castable<Expression, Node> {
   public:
     ~Expression() override;
 
diff --git a/src/tint/ast/extension.h b/src/tint/ast/extension.h
index 4988edb..900d491 100644
--- a/src/tint/ast/extension.h
+++ b/src/tint/ast/extension.h
@@ -24,7 +24,7 @@
 /// ```
 ///   enable f16;
 /// ```
-class Extension final : public Castable<Extension, Node> {
+class Extension final : public utils::Castable<Extension, Node> {
   public:
     /// Create a extension
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/float_literal_expression.h b/src/tint/ast/float_literal_expression.h
index 7920c78..40e7e7c 100644
--- a/src/tint/ast/float_literal_expression.h
+++ b/src/tint/ast/float_literal_expression.h
@@ -22,7 +22,8 @@
 namespace tint::ast {
 
 /// A float literal
-class FloatLiteralExpression final : public Castable<FloatLiteralExpression, LiteralExpression> {
+class FloatLiteralExpression final
+    : public utils::Castable<FloatLiteralExpression, LiteralExpression> {
   public:
     /// Literal suffix
     enum class Suffix {
diff --git a/src/tint/ast/for_loop_statement.h b/src/tint/ast/for_loop_statement.h
index 00fcf1d..fa47a29 100644
--- a/src/tint/ast/for_loop_statement.h
+++ b/src/tint/ast/for_loop_statement.h
@@ -22,7 +22,7 @@
 class Expression;
 
 /// A for loop statement
-class ForLoopStatement final : public Castable<ForLoopStatement, Statement> {
+class ForLoopStatement final : public utils::Castable<ForLoopStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/function.h b/src/tint/ast/function.h
index 8fa009f..b47d74f 100644
--- a/src/tint/ast/function.h
+++ b/src/tint/ast/function.h
@@ -38,7 +38,7 @@
 namespace tint::ast {
 
 /// A Function statement.
-class Function final : public Castable<Function, Node> {
+class Function final : public utils::Castable<Function, Node> {
   public:
     /// Create a function
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/group_attribute.h b/src/tint/ast/group_attribute.h
index af57eb5..47cef56 100644
--- a/src/tint/ast/group_attribute.h
+++ b/src/tint/ast/group_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A group attribute
-class GroupAttribute final : public Castable<GroupAttribute, Attribute> {
+class GroupAttribute final : public utils::Castable<GroupAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/id_attribute.h b/src/tint/ast/id_attribute.h
index 5557a06..3e51ce4 100644
--- a/src/tint/ast/id_attribute.h
+++ b/src/tint/ast/id_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// An id attribute for pipeline-overridable constants
-class IdAttribute final : public Castable<IdAttribute, Attribute> {
+class IdAttribute final : public utils::Castable<IdAttribute, Attribute> {
   public:
     /// Create an id attribute.
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/identifier.h b/src/tint/ast/identifier.h
index df138e9..3fc81df 100644
--- a/src/tint/ast/identifier.h
+++ b/src/tint/ast/identifier.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An identifier
-class Identifier : public Castable<Identifier, Node> {
+class Identifier : public utils::Castable<Identifier, Node> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/identifier_expression.h b/src/tint/ast/identifier_expression.h
index c2200bd..839445b 100644
--- a/src/tint/ast/identifier_expression.h
+++ b/src/tint/ast/identifier_expression.h
@@ -25,7 +25,7 @@
 namespace tint::ast {
 
 /// An identifier expression
-class IdentifierExpression final : public Castable<IdentifierExpression, Expression> {
+class IdentifierExpression final : public utils::Castable<IdentifierExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/if_statement.h b/src/tint/ast/if_statement.h
index 255ce2f..3333084 100644
--- a/src/tint/ast/if_statement.h
+++ b/src/tint/ast/if_statement.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// An if statement
-class IfStatement final : public Castable<IfStatement, Statement> {
+class IfStatement final : public utils::Castable<IfStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/increment_decrement_statement.h b/src/tint/ast/increment_decrement_statement.h
index 9046e46..5fa6b93 100644
--- a/src/tint/ast/increment_decrement_statement.h
+++ b/src/tint/ast/increment_decrement_statement.h
@@ -21,7 +21,8 @@
 namespace tint::ast {
 
 /// An increment or decrement statement
-class IncrementDecrementStatement final : public Castable<IncrementDecrementStatement, Statement> {
+class IncrementDecrementStatement final
+    : public utils::Castable<IncrementDecrementStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/index_accessor_expression.h b/src/tint/ast/index_accessor_expression.h
index 09dc975..6dc6f96 100644
--- a/src/tint/ast/index_accessor_expression.h
+++ b/src/tint/ast/index_accessor_expression.h
@@ -20,7 +20,8 @@
 namespace tint::ast {
 
 /// An index accessor expression
-class IndexAccessorExpression final : public Castable<IndexAccessorExpression, AccessorExpression> {
+class IndexAccessorExpression final
+    : public utils::Castable<IndexAccessorExpression, AccessorExpression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/int_literal_expression.h b/src/tint/ast/int_literal_expression.h
index 0f50ad3..bf13f8e 100644
--- a/src/tint/ast/int_literal_expression.h
+++ b/src/tint/ast/int_literal_expression.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An integer literal. The literal may have an 'i', 'u' or no suffix.
-class IntLiteralExpression final : public Castable<IntLiteralExpression, LiteralExpression> {
+class IntLiteralExpression final : public utils::Castable<IntLiteralExpression, LiteralExpression> {
   public:
     /// Literal suffix
     enum class Suffix {
diff --git a/src/tint/ast/internal_attribute.h b/src/tint/ast/internal_attribute.h
index 36f2a98..a06a966 100644
--- a/src/tint/ast/internal_attribute.h
+++ b/src/tint/ast/internal_attribute.h
@@ -30,7 +30,7 @@
 /// An attribute used to indicate that a function is tint-internal.
 /// These attributes are not produced by generators, but instead are usually
 /// created by transforms for consumption by a particular backend.
-class InternalAttribute : public Castable<InternalAttribute, Attribute> {
+class InternalAttribute : public utils::Castable<InternalAttribute, Attribute> {
   public:
     /// Constructor
     /// @param program_id the identifier of the program that owns this node
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
index b3177ed..65cf585 100644
--- a/src/tint/ast/interpolate_attribute.h
+++ b/src/tint/ast/interpolate_attribute.h
@@ -27,7 +27,7 @@
 namespace tint::ast {
 
 /// An interpolate attribute
-class InterpolateAttribute final : public Castable<InterpolateAttribute, Attribute> {
+class InterpolateAttribute final : public utils::Castable<InterpolateAttribute, Attribute> {
   public:
     /// Create an interpolate attribute.
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/invariant_attribute.h b/src/tint/ast/invariant_attribute.h
index 9abb6a4..b4da288 100644
--- a/src/tint/ast/invariant_attribute.h
+++ b/src/tint/ast/invariant_attribute.h
@@ -22,7 +22,7 @@
 namespace tint::ast {
 
 /// The invariant attribute
-class InvariantAttribute final : public Castable<InvariantAttribute, Attribute> {
+class InvariantAttribute final : public utils::Castable<InvariantAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/let.h b/src/tint/ast/let.h
index 127c3d4..1e529c9 100644
--- a/src/tint/ast/let.h
+++ b/src/tint/ast/let.h
@@ -27,7 +27,7 @@
 ///   let twice_depth : i32 = width + width;  // Must have initializer
 /// ```
 /// @see https://www.w3.org/TR/WGSL/#let-decls
-class Let final : public Castable<Let, Variable> {
+class Let final : public utils::Castable<Let, Variable> {
   public:
     /// Create a 'let' variable
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/literal_expression.h b/src/tint/ast/literal_expression.h
index b4b2b09..5cf5c07 100644
--- a/src/tint/ast/literal_expression.h
+++ b/src/tint/ast/literal_expression.h
@@ -22,7 +22,7 @@
 namespace tint::ast {
 
 /// Base class for a literal value expressions
-class LiteralExpression : public Castable<LiteralExpression, Expression> {
+class LiteralExpression : public utils::Castable<LiteralExpression, Expression> {
   public:
     ~LiteralExpression() override;
 
diff --git a/src/tint/ast/location_attribute.h b/src/tint/ast/location_attribute.h
index a41e943..eb10307 100644
--- a/src/tint/ast/location_attribute.h
+++ b/src/tint/ast/location_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A location attribute
-class LocationAttribute final : public Castable<LocationAttribute, Attribute> {
+class LocationAttribute final : public utils::Castable<LocationAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/loop_statement.h b/src/tint/ast/loop_statement.h
index fc764e2..8539345 100644
--- a/src/tint/ast/loop_statement.h
+++ b/src/tint/ast/loop_statement.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// A loop statement
-class LoopStatement final : public Castable<LoopStatement, Statement> {
+class LoopStatement final : public utils::Castable<LoopStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/member_accessor_expression.h b/src/tint/ast/member_accessor_expression.h
index 95fbbd2..5ae36b1 100644
--- a/src/tint/ast/member_accessor_expression.h
+++ b/src/tint/ast/member_accessor_expression.h
@@ -22,7 +22,7 @@
 
 /// A member accessor expression
 class MemberAccessorExpression final
-    : public Castable<MemberAccessorExpression, AccessorExpression> {
+    : public utils::Castable<MemberAccessorExpression, AccessorExpression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/module.h b/src/tint/ast/module.h
index 948f894..130939e 100644
--- a/src/tint/ast/module.h
+++ b/src/tint/ast/module.h
@@ -29,7 +29,7 @@
 
 /// Module holds the top-level AST types, functions and global variables used by
 /// a Program.
-class Module final : public Castable<Module, Node> {
+class Module final : public utils::Castable<Module, Node> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
@@ -80,7 +80,7 @@
     auto& GlobalVariables() { return global_variables_; }
 
     /// @returns the global variable declarations of kind 'T' for the module
-    template <typename T, typename = traits::EnableIfIsType<T, Variable>>
+    template <typename T, typename = utils::traits::EnableIfIsType<T, Variable>>
     auto Globals() const {
         utils::Vector<const T*, 32> out;
         out.Reserve(global_variables_.Length());
diff --git a/src/tint/ast/must_use_attribute.h b/src/tint/ast/must_use_attribute.h
index 6befa54..a135176 100644
--- a/src/tint/ast/must_use_attribute.h
+++ b/src/tint/ast/must_use_attribute.h
@@ -22,7 +22,7 @@
 namespace tint::ast {
 
 /// The must_use attribute
-class MustUseAttribute final : public Castable<MustUseAttribute, Attribute> {
+class MustUseAttribute final : public utils::Castable<MustUseAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/node.h b/src/tint/ast/node.h
index afea2e8..3ad56f3 100644
--- a/src/tint/ast/node.h
+++ b/src/tint/ast/node.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// AST base class node
-class Node : public Castable<Node, Cloneable> {
+class Node : public utils::Castable<Node, Cloneable> {
   public:
     ~Node() override;
 
diff --git a/src/tint/ast/override.h b/src/tint/ast/override.h
index b824ed3..56fc211 100644
--- a/src/tint/ast/override.h
+++ b/src/tint/ast/override.h
@@ -30,7 +30,7 @@
 ///   override scale : f32;            // No default - must be overridden.
 /// ```
 /// @see https://www.w3.org/TR/WGSL/#override-decls
-class Override final : public Castable<Override, Variable> {
+class Override final : public utils::Castable<Override, Variable> {
   public:
     /// Create an 'override' pipeline-overridable constant.
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/parameter.h b/src/tint/ast/parameter.h
index b056afa..b363d33 100644
--- a/src/tint/ast/parameter.h
+++ b/src/tint/ast/parameter.h
@@ -31,7 +31,7 @@
 /// ```
 ///
 /// @see https://www.w3.org/TR/WGSL/#creation-time-consts
-class Parameter final : public Castable<Parameter, Variable> {
+class Parameter final : public utils::Castable<Parameter, Variable> {
   public:
     /// Create a 'parameter' creation-time value variable.
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/phony_expression.h b/src/tint/ast/phony_expression.h
index 7f59c37..764bf23 100644
--- a/src/tint/ast/phony_expression.h
+++ b/src/tint/ast/phony_expression.h
@@ -21,7 +21,7 @@
 
 /// Represents the `_` of a phony assignment `_ = <expr>`
 /// @see https://www.w3.org/TR/WGSL/#phony-assignment-section
-class PhonyExpression final : public Castable<PhonyExpression, Expression> {
+class PhonyExpression final : public utils::Castable<PhonyExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/return_statement.h b/src/tint/ast/return_statement.h
index 7eae780..6beb215 100644
--- a/src/tint/ast/return_statement.h
+++ b/src/tint/ast/return_statement.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A return statement
-class ReturnStatement final : public Castable<ReturnStatement, Statement> {
+class ReturnStatement final : public utils::Castable<ReturnStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/stage_attribute.h b/src/tint/ast/stage_attribute.h
index 0bf9d9e..9b32579 100644
--- a/src/tint/ast/stage_attribute.h
+++ b/src/tint/ast/stage_attribute.h
@@ -23,7 +23,7 @@
 namespace tint::ast {
 
 /// A workgroup attribute
-class StageAttribute final : public Castable<StageAttribute, Attribute> {
+class StageAttribute final : public utils::Castable<StageAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/statement.h b/src/tint/ast/statement.h
index fa434cc..3aeff6a 100644
--- a/src/tint/ast/statement.h
+++ b/src/tint/ast/statement.h
@@ -22,7 +22,7 @@
 namespace tint::ast {
 
 /// Base statement class
-class Statement : public Castable<Statement, Node> {
+class Statement : public utils::Castable<Statement, Node> {
   public:
     ~Statement() override;
 
diff --git a/src/tint/ast/stride_attribute.h b/src/tint/ast/stride_attribute.h
index 9014677..0d6ba18 100644
--- a/src/tint/ast/stride_attribute.h
+++ b/src/tint/ast/stride_attribute.h
@@ -24,7 +24,7 @@
 
 /// A stride attribute used by the SPIR-V reader for strided arrays and
 /// matrices.
-class StrideAttribute final : public Castable<StrideAttribute, Attribute> {
+class StrideAttribute final : public utils::Castable<StrideAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/struct.h b/src/tint/ast/struct.h
index d7c2281..067688d 100644
--- a/src/tint/ast/struct.h
+++ b/src/tint/ast/struct.h
@@ -26,7 +26,7 @@
 namespace tint::ast {
 
 /// A struct statement.
-class Struct final : public Castable<Struct, TypeDecl> {
+class Struct final : public utils::Castable<Struct, TypeDecl> {
   public:
     /// Create a new struct statement
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/struct_member.h b/src/tint/ast/struct_member.h
index 27e5b0c..a44c23d 100644
--- a/src/tint/ast/struct_member.h
+++ b/src/tint/ast/struct_member.h
@@ -28,7 +28,7 @@
 namespace tint::ast {
 
 /// A struct member statement.
-class StructMember final : public Castable<StructMember, Node> {
+class StructMember final : public utils::Castable<StructMember, Node> {
   public:
     /// Create a new struct member statement
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/struct_member_align_attribute.h b/src/tint/ast/struct_member_align_attribute.h
index 1649ae6..b368b79 100644
--- a/src/tint/ast/struct_member_align_attribute.h
+++ b/src/tint/ast/struct_member_align_attribute.h
@@ -24,7 +24,8 @@
 namespace tint::ast {
 
 /// A struct member align attribute
-class StructMemberAlignAttribute final : public Castable<StructMemberAlignAttribute, Attribute> {
+class StructMemberAlignAttribute final
+    : public utils::Castable<StructMemberAlignAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/struct_member_offset_attribute.h b/src/tint/ast/struct_member_offset_attribute.h
index ed74397..edfbb13 100644
--- a/src/tint/ast/struct_member_offset_attribute.h
+++ b/src/tint/ast/struct_member_offset_attribute.h
@@ -32,7 +32,8 @@
 /// trivial for the Resolver to handle `@offset(n)` or `@size(n)` /
 /// `@align(n)` attributes, so this is what we do, keeping all the layout
 /// logic in one place.
-class StructMemberOffsetAttribute final : public Castable<StructMemberOffsetAttribute, Attribute> {
+class StructMemberOffsetAttribute final
+    : public utils::Castable<StructMemberOffsetAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/struct_member_size_attribute.h b/src/tint/ast/struct_member_size_attribute.h
index 2a0c71f..5b2bb4e 100644
--- a/src/tint/ast/struct_member_size_attribute.h
+++ b/src/tint/ast/struct_member_size_attribute.h
@@ -24,7 +24,8 @@
 namespace tint::ast {
 
 /// A struct member size attribute
-class StructMemberSizeAttribute final : public Castable<StructMemberSizeAttribute, Attribute> {
+class StructMemberSizeAttribute final
+    : public utils::Castable<StructMemberSizeAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/switch_statement.h b/src/tint/ast/switch_statement.h
index f2b8201..f376f82 100644
--- a/src/tint/ast/switch_statement.h
+++ b/src/tint/ast/switch_statement.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A switch statement
-class SwitchStatement final : public Castable<SwitchStatement, Statement> {
+class SwitchStatement final : public utils::Castable<SwitchStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/templated_identifier.h b/src/tint/ast/templated_identifier.h
index 74ddb43..92b8345 100644
--- a/src/tint/ast/templated_identifier.h
+++ b/src/tint/ast/templated_identifier.h
@@ -26,7 +26,7 @@
 namespace tint::ast {
 
 /// A templated identifier expression
-class TemplatedIdentifier final : public Castable<TemplatedIdentifier, Identifier> {
+class TemplatedIdentifier final : public utils::Castable<TemplatedIdentifier, Identifier> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/test_helper.h b/src/tint/ast/test_helper.h
index e26c10a..6ef328d 100644
--- a/src/tint/ast/test_helper.h
+++ b/src/tint/ast/test_helper.h
@@ -96,7 +96,7 @@
         const auto* got_arg = got->arguments[arg_idx++];
 
         using T = std::decay_t<decltype(expected_arg)>;
-        if constexpr (traits::IsStringLike<T>) {
+        if constexpr (utils::traits::IsStringLike<T>) {
             ASSERT_TRUE(got_arg->Is<IdentifierExpression>());
             CheckIdentifier(got_arg->As<IdentifierExpression>()->identifier, expected_arg);
         } else if constexpr (IsTemplatedIdentifierMatcher<T>::value) {
diff --git a/src/tint/ast/traverse_expressions.h b/src/tint/ast/traverse_expressions.h
index 84a10c3..780de1a 100644
--- a/src/tint/ast/traverse_expressions.h
+++ b/src/tint/ast/traverse_expressions.h
@@ -62,8 +62,9 @@
 /// @return true on success, false on error
 template <TraverseOrder ORDER = TraverseOrder::LeftToRight, typename CALLBACK>
 bool TraverseExpressions(const Expression* root, diag::List& diags, CALLBACK&& callback) {
-    using EXPR_TYPE = std::remove_pointer_t<traits::ParameterType<CALLBACK, 0>>;
-    constexpr static bool kHasDepthArg = traits::SignatureOfT<CALLBACK>::parameter_count == 2;
+    using EXPR_TYPE = std::remove_pointer_t<utils::traits::ParameterType<CALLBACK, 0>>;
+    constexpr static bool kHasDepthArg =
+        utils::traits::SignatureOfT<CALLBACK>::parameter_count == 2;
 
     struct Pending {
         const Expression* expr;
diff --git a/src/tint/ast/type_decl.h b/src/tint/ast/type_decl.h
index 8ec0fe4..aa0bc14 100644
--- a/src/tint/ast/type_decl.h
+++ b/src/tint/ast/type_decl.h
@@ -25,7 +25,7 @@
 namespace tint::ast {
 
 /// The base class for type declarations.
-class TypeDecl : public Castable<TypeDecl, Node> {
+class TypeDecl : public utils::Castable<TypeDecl, Node> {
   public:
     /// Create a new struct statement
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/unary_op_expression.h b/src/tint/ast/unary_op_expression.h
index 3639447..6004975 100644
--- a/src/tint/ast/unary_op_expression.h
+++ b/src/tint/ast/unary_op_expression.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A unary op expression
-class UnaryOpExpression final : public Castable<UnaryOpExpression, Expression> {
+class UnaryOpExpression final : public utils::Castable<UnaryOpExpression, Expression> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/var.h b/src/tint/ast/var.h
index 6f08a6f..e3cfde0 100644
--- a/src/tint/ast/var.h
+++ b/src/tint/ast/var.h
@@ -39,7 +39,7 @@
 /// ```
 ///
 /// @see https://www.w3.org/TR/WGSL/#var-decls
-class Var final : public Castable<Var, Variable> {
+class Var final : public utils::Castable<Var, Variable> {
   public:
     /// Create a 'var' variable
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/variable.h b/src/tint/ast/variable.h
index 2230ac3..0a1ccb2 100644
--- a/src/tint/ast/variable.h
+++ b/src/tint/ast/variable.h
@@ -40,7 +40,7 @@
 /// declaration, "override" declaration, "const" declaration, or formal parameter to a function.
 ///
 /// @see https://www.w3.org/TR/WGSL/#value-decls
-class Variable : public Castable<Variable, Node> {
+class Variable : public utils::Castable<Variable, Node> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/variable_decl_statement.h b/src/tint/ast/variable_decl_statement.h
index 99a9a5d..7732299 100644
--- a/src/tint/ast/variable_decl_statement.h
+++ b/src/tint/ast/variable_decl_statement.h
@@ -21,7 +21,7 @@
 namespace tint::ast {
 
 /// A variable declaration statement
-class VariableDeclStatement final : public Castable<VariableDeclStatement, Statement> {
+class VariableDeclStatement final : public utils::Castable<VariableDeclStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/while_statement.h b/src/tint/ast/while_statement.h
index 360610a..6545a65 100644
--- a/src/tint/ast/while_statement.h
+++ b/src/tint/ast/while_statement.h
@@ -22,7 +22,7 @@
 class Expression;
 
 /// A while loop statement
-class WhileStatement final : public Castable<WhileStatement, Statement> {
+class WhileStatement final : public utils::Castable<WhileStatement, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/workgroup_attribute.h b/src/tint/ast/workgroup_attribute.h
index 05cbb89..8789df7 100644
--- a/src/tint/ast/workgroup_attribute.h
+++ b/src/tint/ast/workgroup_attribute.h
@@ -28,7 +28,7 @@
 namespace tint::ast {
 
 /// A workgroup attribute
-class WorkgroupAttribute final : public Castable<WorkgroupAttribute, Attribute> {
+class WorkgroupAttribute final : public utils::Castable<WorkgroupAttribute, Attribute> {
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/bench/benchmark.h b/src/tint/bench/benchmark.h
index 6801585..adb28b8 100644
--- a/src/tint/bench/benchmark.h
+++ b/src/tint/bench/benchmark.h
@@ -51,6 +51,15 @@
 /// @returns either the loaded Program or an Error
 std::variant<ProgramAndFile, Error> LoadProgram(std::string name);
 
+// If TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER is defined, include that to
+// declare the TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS() macro, which appends
+// external programs to the TINT_BENCHMARK_WGSL_PROGRAMS() list.
+#ifdef TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER
+#include TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAM_HEADER
+#else
+#define TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS(x)
+#endif
+
 /// Declares a benchmark with the given function and WGSL file name
 #define TINT_BENCHMARK_WGSL_PROGRAM(FUNC, WGSL_NAME) BENCHMARK_CAPTURE(FUNC, WGSL_NAME, WGSL_NAME);
 
@@ -69,7 +78,8 @@
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple-fragment.wgsl");               \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple-vertex.wgsl");                 \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "skinned-shadowed-pbr-fragment.wgsl"); \
-    TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "skinned-shadowed-pbr-vertex.wgsl");
+    TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "skinned-shadowed-pbr-vertex.wgsl");   \
+    TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS(FUNC)
 
 }  // namespace tint::bench
 
diff --git a/src/tint/clone_context.cc b/src/tint/clone_context.cc
index a6f1478..f2988d1 100644
--- a/src/tint/clone_context.cc
+++ b/src/tint/clone_context.cc
@@ -97,7 +97,7 @@
     return object->Clone(this);
 }
 
-void CloneContext::CheckedCastFailure(const Cloneable* got, const TypeInfo& expected) {
+void CloneContext::CheckedCastFailure(const Cloneable* got, const utils::TypeInfo& expected) {
     TINT_ICE(Clone, Diagnostics()) << "Cloned object was not of the expected type\n"
                                    << "got:      " << got->TypeInfo().name << "\n"
                                    << "expected: " << expected.name;
diff --git a/src/tint/clone_context.h b/src/tint/clone_context.h
index 8e0e2ff..5f0073c 100644
--- a/src/tint/clone_context.h
+++ b/src/tint/clone_context.h
@@ -21,14 +21,14 @@
 #include <utility>
 #include <vector>
 
-#include "src/tint/castable.h"
 #include "src/tint/debug.h"
 #include "src/tint/program_id.h"
 #include "src/tint/symbol.h"
-#include "src/tint/traits.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/hashmap.h"
 #include "src/tint/utils/hashset.h"
+#include "src/tint/utils/traits.h"
 #include "src/tint/utils/vector.h"
 
 // Forward declarations
@@ -49,7 +49,7 @@
 ProgramID ProgramIDOf(const ProgramBuilder*);
 
 /// Cloneable is the base class for all objects that can be cloned
-class Cloneable : public Castable<Cloneable> {
+class Cloneable : public utils::Castable<Cloneable> {
   public:
     /// Constructor
     Cloneable();
@@ -74,8 +74,8 @@
     /// ParamTypeIsPtrOf<F, T> is true iff the first parameter of
     /// F is a pointer of (or derives from) type T.
     template <typename F, typename T>
-    static constexpr bool ParamTypeIsPtrOf =
-        traits::IsTypeOrDerived<typename std::remove_pointer<traits::ParameterType<F, 0>>::type, T>;
+    static constexpr bool ParamTypeIsPtrOf = utils::traits::
+        IsTypeOrDerived<typename std::remove_pointer<utils::traits::ParameterType<F, 0>>::type, T>;
 
   public:
     /// SymbolTransform is a function that takes a symbol and returns a new
@@ -303,22 +303,23 @@
     ///        `T* (T*)`, where `T` derives from Cloneable
     /// @returns this CloneContext so calls can be chained
     template <typename F>
-    traits::EnableIf<ParamTypeIsPtrOf<F, Cloneable>, CloneContext>& ReplaceAll(F&& replacer) {
-        using TPtr = traits::ParameterType<F, 0>;
+    utils::traits::EnableIf<ParamTypeIsPtrOf<F, Cloneable>, CloneContext>& ReplaceAll(
+        F&& replacer) {
+        using TPtr = utils::traits::ParameterType<F, 0>;
         using T = typename std::remove_pointer<TPtr>::type;
         for (auto& transform : transforms_) {
-            bool already_registered = transform.typeinfo->Is(&TypeInfo::Of<T>()) ||
-                                      TypeInfo::Of<T>().Is(transform.typeinfo);
+            bool already_registered = transform.typeinfo->Is(&utils::TypeInfo::Of<T>()) ||
+                                      utils::TypeInfo::Of<T>().Is(transform.typeinfo);
             if (TINT_UNLIKELY(already_registered)) {
-                TINT_ICE(Clone, Diagnostics())
-                    << "ReplaceAll() called with a handler for type " << TypeInfo::Of<T>().name
-                    << " that is already handled by a handler for type "
-                    << transform.typeinfo->name;
+                TINT_ICE(Clone, Diagnostics()) << "ReplaceAll() called with a handler for type "
+                                               << utils::TypeInfo::Of<T>().name
+                                               << " that is already handled by a handler for type "
+                                               << transform.typeinfo->name;
                 return *this;
             }
         }
         CloneableTransform transform;
-        transform.typeinfo = &TypeInfo::Of<T>();
+        transform.typeinfo = &utils::TypeInfo::Of<T>();
         transform.function = [=](const Cloneable* in) { return replacer(in->As<T>()); };
         transforms_.Push(std::move(transform));
         return *this;
@@ -355,7 +356,9 @@
     /// references of the original object. A type mismatch will result in an
     /// assertion in debug builds, and undefined behavior in release builds.
     /// @returns this CloneContext so calls can be chained
-    template <typename WHAT, typename WITH, typename = traits::EnableIfIsType<WITH, Cloneable>>
+    template <typename WHAT,
+              typename WITH,
+              typename = utils::traits::EnableIfIsType<WITH, Cloneable>>
     CloneContext& Replace(const WHAT* what, const WITH* with) {
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, with);
@@ -551,8 +554,8 @@
         /// Destructor
         ~CloneableTransform();
 
-        // TypeInfo of the Cloneable that the transform operates on
-        const TypeInfo* typeinfo;
+        // utils::TypeInfo of the Cloneable that the transform operates on
+        const utils::TypeInfo* typeinfo;
         std::function<const Cloneable*(const Cloneable*)> function;
     };
 
@@ -595,7 +598,7 @@
         if (TINT_LIKELY(cast)) {
             return cast;
         }
-        CheckedCastFailure(obj, TypeInfo::Of<TO>());
+        CheckedCastFailure(obj, utils::TypeInfo::Of<TO>());
         return nullptr;
     }
 
@@ -605,7 +608,7 @@
 
     /// Adds an error diagnostic to Diagnostics() that the cloned object was not
     /// of the expected type.
-    void CheckedCastFailure(const Cloneable* got, const TypeInfo& expected);
+    void CheckedCastFailure(const Cloneable* got, const utils::TypeInfo& expected);
 
     /// @returns the diagnostic list of #dst
     diag::List& Diagnostics() const;
diff --git a/src/tint/clone_context_test.cc b/src/tint/clone_context_test.cc
index ec17e0d..4df5ade 100644
--- a/src/tint/clone_context_test.cc
+++ b/src/tint/clone_context_test.cc
@@ -30,7 +30,7 @@
     utils::BlockAllocator<Cloneable> alloc;
 };
 
-struct Node : public Castable<Node, Cloneable> {
+struct Node : public utils::Castable<Node, Cloneable> {
     Node(Allocator* alloc,
          Symbol n,
          const Node* node_a = nullptr,
@@ -55,7 +55,7 @@
     }
 };
 
-struct Replaceable : public Castable<Replaceable, Node> {
+struct Replaceable : public utils::Castable<Replaceable, Node> {
     Replaceable(Allocator* alloc,
                 Symbol n,
                 const Node* node_a = nullptr,
@@ -64,18 +64,18 @@
         : Base(alloc, n, node_a, node_b, node_c) {}
 };
 
-struct Replacement : public Castable<Replacement, Replaceable> {
+struct Replacement : public utils::Castable<Replacement, Replaceable> {
     Replacement(Allocator* alloc, Symbol n) : Base(alloc, n) {}
 };
 
-struct NotANode : public Castable<NotANode, Cloneable> {
+struct NotANode : public utils::Castable<NotANode, Cloneable> {
     explicit NotANode(Allocator* alloc) : allocator(alloc) {}
 
     Allocator* const allocator;
     NotANode* Clone(CloneContext*) const override { return allocator->Create<NotANode>(); }
 };
 
-struct ProgramNode : public Castable<ProgramNode, Cloneable> {
+struct ProgramNode : public utils::Castable<ProgramNode, Cloneable> {
     ProgramNode(Allocator* alloc, ProgramID id, ProgramID cloned_id)
         : allocator(alloc), program_id(id), cloned_program_id(cloned_id) {}
 
@@ -1051,7 +1051,7 @@
 }
 
 TEST_F(CloneContextNodeTest, CloneWithReplaceAll_SameTypeTwice) {
-    std::string node_name = TypeInfo::Of<Node>().name;
+    std::string node_name = utils::TypeInfo::Of<Node>().name;
 
     EXPECT_FATAL_FAILURE(
         {
@@ -1066,8 +1066,8 @@
 }
 
 TEST_F(CloneContextNodeTest, CloneWithReplaceAll_BaseThenDerived) {
-    std::string node_name = TypeInfo::Of<Node>().name;
-    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+    std::string node_name = utils::TypeInfo::Of<Node>().name;
+    std::string replaceable_name = utils::TypeInfo::Of<Replaceable>().name;
 
     EXPECT_FATAL_FAILURE(
         {
@@ -1082,8 +1082,8 @@
 }
 
 TEST_F(CloneContextNodeTest, CloneWithReplaceAll_DerivedThenBase) {
-    std::string node_name = TypeInfo::Of<Node>().name;
-    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+    std::string node_name = utils::TypeInfo::Of<Node>().name;
+    std::string replaceable_name = utils::TypeInfo::Of<Replaceable>().name;
 
     EXPECT_FATAL_FAILURE(
         {
diff --git a/src/tint/cmd/generate_external_texture_bindings.cc b/src/tint/cmd/generate_external_texture_bindings.cc
index 1b6ca74..52650ea 100644
--- a/src/tint/cmd/generate_external_texture_bindings.cc
+++ b/src/tint/cmd/generate_external_texture_bindings.cc
@@ -36,12 +36,13 @@
     std::vector<sem::BindingPoint> ext_tex_bps;
     for (auto* var : program->AST().GlobalVariables()) {
         if (auto* sem_var = program->Sem().Get(var)->As<sem::GlobalVariable>()) {
-            auto bp = sem_var->BindingPoint();
-            auto& n = group_to_next_binding_number[bp.group];
-            n = std::max(n, bp.binding + 1);
+            if (auto bp = sem_var->BindingPoint()) {
+                auto& n = group_to_next_binding_number[bp->group];
+                n = std::max(n, bp->binding + 1);
 
-            if (sem_var->Type()->UnwrapRef()->Is<type::ExternalTexture>()) {
-                ext_tex_bps.emplace_back(bp);
+                if (sem_var->Type()->UnwrapRef()->Is<type::ExternalTexture>()) {
+                    ext_tex_bps.emplace_back(*bp);
+                }
             }
         }
     }
diff --git a/src/tint/constant/composite.h b/src/tint/constant/composite.h
index ef7233c..7a50640 100644
--- a/src/tint/constant/composite.h
+++ b/src/tint/constant/composite.h
@@ -15,10 +15,10 @@
 #ifndef SRC_TINT_CONSTANT_COMPOSITE_H_
 #define SRC_TINT_CONSTANT_COMPOSITE_H_
 
-#include "src/tint/castable.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"
 #include "src/tint/utils/vector.h"
 
@@ -28,7 +28,7 @@
 /// Composite may be of a vector, matrix, array or structure type.
 /// If each element is the same type and value, then a Splat would be a more efficient constant
 /// implementation. Use CreateComposite() to create the appropriate type.
-class Composite : public Castable<Composite, Value> {
+class Composite : public utils::Castable<Composite, Value> {
   public:
     /// Constructor
     /// @param t the compsite type
diff --git a/src/tint/constant/node.h b/src/tint/constant/node.h
index 41d00e0..54ef94d 100644
--- a/src/tint/constant/node.h
+++ b/src/tint/constant/node.h
@@ -15,12 +15,12 @@
 #ifndef SRC_TINT_CONSTANT_NODE_H_
 #define SRC_TINT_CONSTANT_NODE_H_
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 
 namespace tint::constant {
 
 /// Node is the base class for all constant nodes
-class Node : public Castable<Node> {
+class Node : public utils::Castable<Node> {
   public:
     /// Constructor
     Node();
diff --git a/src/tint/constant/scalar.h b/src/tint/constant/scalar.h
index fc6aef0..ab5f852 100644
--- a/src/tint/constant/scalar.h
+++ b/src/tint/constant/scalar.h
@@ -15,17 +15,17 @@
 #ifndef SRC_TINT_CONSTANT_SCALAR_H_
 #define SRC_TINT_CONSTANT_SCALAR_H_
 
-#include "src/tint/castable.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"
 
 namespace tint::constant {
 
 /// Scalar holds a single scalar or abstract-numeric value.
 template <typename T>
-class Scalar : public Castable<Scalar<T>, Value> {
+class Scalar : public utils::Castable<Scalar<T>, Value> {
   public:
     static_assert(!std::is_same_v<UnwrapNumber<T>, T> || std::is_same_v<T, bool>,
                   "T must be a Number or bool");
diff --git a/src/tint/constant/splat.h b/src/tint/constant/splat.h
index a496235..d8e55a6 100644
--- a/src/tint/constant/splat.h
+++ b/src/tint/constant/splat.h
@@ -15,9 +15,9 @@
 #ifndef SRC_TINT_CONSTANT_SPLAT_H_
 #define SRC_TINT_CONSTANT_SPLAT_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/constant/composite.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::constant {
@@ -26,7 +26,7 @@
 ///
 /// Splat is used for zero-initializers, 'splat' initializers, or initializers where each element is
 /// identical. Splat may be of a vector, matrix, array or structure type.
-class Splat : public Castable<Splat, Value> {
+class Splat : public utils::Castable<Splat, Value> {
   public:
     /// Constructor
     /// @param t the splat type
diff --git a/src/tint/constant/value.h b/src/tint/constant/value.h
index eb29998..fe1f78a 100644
--- a/src/tint/constant/value.h
+++ b/src/tint/constant/value.h
@@ -17,16 +17,16 @@
 
 #include <variant>
 
-#include "src/tint/castable.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"
 
 namespace tint::constant {
 
 /// Value is the interface to a compile-time evaluated expression value.
-class Value : public Castable<Value, Node> {
+class Value : public utils::Castable<Value, Node> {
   public:
     /// Constructor
     Value();
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/util.h b/src/tint/fuzzers/tint_ast_fuzzer/util.h
index fe114f4..b428d46 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/util.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/util.h
@@ -19,12 +19,12 @@
 
 #include "src/tint/ast/module.h"
 #include "src/tint/ast/variable_decl_statement.h"
-#include "src/tint/castable.h"
 #include "src/tint/program.h"
 #include "src/tint/sem/block_statement.h"
 #include "src/tint/sem/function.h"
 #include "src/tint/sem/statement.h"
 #include "src/tint/sem/variable.h"
+#include "src/tint/utils/castable.h"
 
 namespace tint::fuzzers::ast_fuzzer::util {
 /// @file
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 6be9b98..7cbc8bf 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -271,12 +271,13 @@
         std::vector<sem::BindingPoint> ext_tex_bps;
         for (auto* var : program.AST().GlobalVariables()) {
             if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-                auto bp = sem_var->BindingPoint();
-                auto& n = group_to_next_binding_number[bp.group];
-                n = std::max(n, bp.binding + 1);
+                if (auto bp = sem_var->BindingPoint()) {
+                    auto& n = group_to_next_binding_number[bp->group];
+                    n = std::max(n, bp->binding + 1);
 
-                if (sem_var->Type()->UnwrapRef()->Is<type::ExternalTexture>()) {
-                    ext_tex_bps.emplace_back(bp);
+                    if (sem_var->Type()->UnwrapRef()->Is<type::ExternalTexture>()) {
+                        ext_tex_bps.emplace_back(*bp);
+                    }
                 }
             }
         }
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index d2330e9..246943d 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -456,7 +456,7 @@
 
 std::vector<ResourceBinding> Inspector::GetTextureResourceBindings(
     const std::string& entry_point,
-    const tint::TypeInfo* texture_type,
+    const tint::utils::TypeInfo* texture_type,
     ResourceBinding::ResourceType resource_type) {
     auto* func = FindEntryPointByName(entry_point);
     if (!func) {
@@ -485,19 +485,20 @@
 
 std::vector<ResourceBinding> Inspector::GetDepthTextureResourceBindings(
     const std::string& entry_point) {
-    return GetTextureResourceBindings(entry_point, &TypeInfo::Of<type::DepthTexture>(),
+    return GetTextureResourceBindings(entry_point, &utils::TypeInfo::Of<type::DepthTexture>(),
                                       ResourceBinding::ResourceType::kDepthTexture);
 }
 
 std::vector<ResourceBinding> Inspector::GetDepthMultisampledTextureResourceBindings(
     const std::string& entry_point) {
-    return GetTextureResourceBindings(entry_point, &TypeInfo::Of<type::DepthMultisampledTexture>(),
+    return GetTextureResourceBindings(entry_point,
+                                      &utils::TypeInfo::Of<type::DepthMultisampledTexture>(),
                                       ResourceBinding::ResourceType::kDepthMultisampledTexture);
 }
 
 std::vector<ResourceBinding> Inspector::GetExternalTextureResourceBindings(
     const std::string& entry_point) {
-    return GetTextureResourceBindings(entry_point, &TypeInfo::Of<type::ExternalTexture>(),
+    return GetTextureResourceBindings(entry_point, &utils::TypeInfo::Of<type::ExternalTexture>(),
                                       ResourceBinding::ResourceType::kExternalTexture);
 }
 
@@ -531,8 +532,8 @@
         auto* texture = pair.first->As<sem::GlobalVariable>();
         auto* sampler = pair.second ? pair.second->As<sem::GlobalVariable>() : nullptr;
         SamplerTexturePair new_pair;
-        new_pair.sampler_binding_point = sampler ? sampler->BindingPoint() : placeholder;
-        new_pair.texture_binding_point = texture->BindingPoint();
+        new_pair.sampler_binding_point = sampler ? *sampler->BindingPoint() : placeholder;
+        new_pair.texture_binding_point = *texture->BindingPoint();
         new_pairs.push_back(new_pair);
     }
     return new_pairs;
@@ -834,8 +835,8 @@
 
         GetOriginatingResources(std::array<const ast::Expression*, 2>{t, s},
                                 [&](std::array<const sem::GlobalVariable*, 2> globals) {
-                                    auto texture_binding_point = globals[0]->BindingPoint();
-                                    auto sampler_binding_point = globals[1]->BindingPoint();
+                                    auto texture_binding_point = *globals[0]->BindingPoint();
+                                    auto sampler_binding_point = *globals[1]->BindingPoint();
 
                                     for (auto* entry_point : entry_points) {
                                         const auto& ep_name =
diff --git a/src/tint/inspector/inspector.h b/src/tint/inspector/inspector.h
index 443bbab..a4cfe6e 100644
--- a/src/tint/inspector/inspector.h
+++ b/src/tint/inspector/inspector.h
@@ -195,7 +195,7 @@
     /// @returns vector of all of the bindings for depth textures.
     std::vector<ResourceBinding> GetTextureResourceBindings(
         const std::string& entry_point,
-        const tint::TypeInfo* texture_type,
+        const tint::utils::TypeInfo* texture_type,
         ResourceBinding::ResourceType resource_type);
 
     /// @param entry_point name of the entry point to get information about.
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index bb74ea8..e39758f 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_BINARY_H_
 #define SRC_TINT_IR_BINARY_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// An instruction in the IR.
-class Binary : public Castable<Binary, Instruction> {
+class Binary : public utils::Castable<Binary, Instruction> {
   public:
     /// The kind of instruction.
     enum class Kind {
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index 7e9f7db..7dcb495 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_BITCAST_H_
 #define SRC_TINT_IR_BITCAST_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A bitcast instruction in the IR.
-class Bitcast : public Castable<Bitcast, Instruction> {
+class Bitcast : public utils::Castable<Bitcast, Instruction> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 4258167..e83d43a 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -25,7 +25,7 @@
 /// A flow node comprising a block of statements. The instructions in the block are a linear list of
 /// instructions to execute. The block will branch at the end. The only blocks which do not branch
 /// are the end blocks of functions.
-class Block : public Castable<Block, FlowNode> {
+class Block : public utils::Castable<Block, FlowNode> {
   public:
     /// Constructor
     Block();
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 5a39dbb..b090bdf 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -90,7 +90,8 @@
     /// @param args the arguments
     /// @returns the new constant value
     template <typename T, typename... ARGS>
-    traits::EnableIf<traits::IsTypeOrDerived<T, constant::Value>, const T>* create(ARGS&&... args) {
+    utils::traits::EnableIf<utils::traits::IsTypeOrDerived<T, constant::Value>, const T>* create(
+        ARGS&&... args) {
         return ir.constants.Create<T>(std::forward<ARGS>(args)...);
     }
 
diff --git a/src/tint/ir/builtin.h b/src/tint/ir/builtin.h
index 4fc8a76..ee73ec6 100644
--- a/src/tint/ir/builtin.h
+++ b/src/tint/ir/builtin.h
@@ -16,16 +16,16 @@
 #define SRC_TINT_IR_BUILTIN_H_
 
 #include "src/tint/builtin/function.h"
-#include "src/tint/castable.h"
 #include "src/tint/ir/call.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A value conversion instruction in the IR.
-class Builtin : public Castable<Builtin, Call> {
+class Builtin : public utils::Castable<Builtin, Call> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
index e1f9d48..89c07cd 100644
--- a/src/tint/ir/call.h
+++ b/src/tint/ir/call.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_CALL_H_
 #define SRC_TINT_IR_CALL_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A Call instruction in the IR.
-class Call : public Castable<Call, Instruction> {
+class Call : public utils::Castable<Call, Instruction> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index 7be3688..dd50fac 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -23,7 +23,7 @@
 namespace tint::ir {
 
 /// Constant in the IR.
-class Constant : public Castable<Constant, Value> {
+class Constant : public utils::Castable<Constant, Value> {
   public:
     /// Constructor
     /// @param val the value stored in the constant
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
index 24449b6..f6c7a0a 100644
--- a/src/tint/ir/construct.h
+++ b/src/tint/ir/construct.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_CONSTRUCT_H_
 #define SRC_TINT_IR_CONSTRUCT_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/call.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A constructor instruction in the IR.
-class Construct : public Castable<Construct, Call> {
+class Construct : public utils::Castable<Construct, Call> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
index 3b8878b..15d118e 100644
--- a/src/tint/ir/convert.h
+++ b/src/tint/ir/convert.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_CONVERT_H_
 #define SRC_TINT_IR_CONVERT_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/call.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A value conversion instruction in the IR.
-class Convert : public Castable<Convert, Call> {
+class Convert : public utils::Castable<Convert, Call> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/flow_node.h b/src/tint/ir/flow_node.h
index 019a8a0..2a91674 100644
--- a/src/tint/ir/flow_node.h
+++ b/src/tint/ir/flow_node.h
@@ -15,13 +15,13 @@
 #ifndef SRC_TINT_IR_FLOW_NODE_H_
 #define SRC_TINT_IR_FLOW_NODE_H_
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::ir {
 
 /// Base class for flow nodes
-class FlowNode : public Castable<FlowNode> {
+class FlowNode : public utils::Castable<FlowNode> {
   public:
     ~FlowNode() override;
 
diff --git a/src/tint/ir/function.h b/src/tint/ir/function.h
index c6032ba..ab3c6eb 100644
--- a/src/tint/ir/function.h
+++ b/src/tint/ir/function.h
@@ -27,7 +27,7 @@
 namespace tint::ir {
 
 /// An IR representation of a function
-class Function : public Castable<Function, FlowNode> {
+class Function : public utils::Castable<Function, FlowNode> {
   public:
     /// Constructor
     Function();
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index 823700e..255b165 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -27,7 +27,7 @@
 namespace tint::ir {
 
 /// A flow node representing an if statement.
-class If : public Castable<If, FlowNode> {
+class If : public utils::Castable<If, FlowNode> {
   public:
     /// Constructor
     If();
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 8ae2de4..8f58e54 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -15,15 +15,15 @@
 #ifndef SRC_TINT_IR_INSTRUCTION_H_
 #define SRC_TINT_IR_INSTRUCTION_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// An instruction in the IR.
-class Instruction : public Castable<Instruction> {
+class Instruction : public utils::Castable<Instruction> {
   public:
     Instruction(const Instruction& instr) = delete;
     Instruction(Instruction&& instr) = delete;
diff --git a/src/tint/ir/loop.h b/src/tint/ir/loop.h
index 2b026e6..e0066f4 100644
--- a/src/tint/ir/loop.h
+++ b/src/tint/ir/loop.h
@@ -22,7 +22,7 @@
 namespace tint::ir {
 
 /// Flow node describing a loop.
-class Loop : public Castable<Loop, FlowNode> {
+class Loop : public utils::Castable<Loop, FlowNode> {
   public:
     /// Constructor
     Loop();
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 6ba1d4c..2ff927e 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -24,7 +24,7 @@
 namespace tint::ir {
 
 /// Flow node representing a switch statement
-class Switch : public Castable<Switch, FlowNode> {
+class Switch : public utils::Castable<Switch, FlowNode> {
   public:
     /// A case selector
     struct CaseSelector {
diff --git a/src/tint/ir/temp.h b/src/tint/ir/temp.h
index 9433099..31bc284 100644
--- a/src/tint/ir/temp.h
+++ b/src/tint/ir/temp.h
@@ -22,7 +22,7 @@
 namespace tint::ir {
 
 /// Temporary value in the IR.
-class Temp : public Castable<Temp, Value> {
+class Temp : public utils::Castable<Temp, Value> {
   public:
     /// A value id.
     using Id = uint32_t;
diff --git a/src/tint/ir/terminator.h b/src/tint/ir/terminator.h
index 9f9aba0..35fbcee 100644
--- a/src/tint/ir/terminator.h
+++ b/src/tint/ir/terminator.h
@@ -21,7 +21,7 @@
 
 /// Flow node used as the end of a function. Must only be used as the `end_target` in a function
 /// flow node. There are no instructions and no branches from this node.
-class Terminator : public Castable<Terminator, FlowNode> {
+class Terminator : public utils::Castable<Terminator, FlowNode> {
   public:
     /// Constructor
     Terminator();
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
index 34215ed..8c99d89 100644
--- a/src/tint/ir/user_call.h
+++ b/src/tint/ir/user_call.h
@@ -15,16 +15,16 @@
 #ifndef SRC_TINT_IR_USER_CALL_H_
 #define SRC_TINT_IR_USER_CALL_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/ir/call.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
 /// A user call instruction in the IR.
-class UserCall : public Castable<UserCall, Call> {
+class UserCall : public utils::Castable<UserCall, Call> {
   public:
     /// Constructor
     /// @param result the result value
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index 2d4fcb0..b498e9d 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -15,9 +15,9 @@
 #ifndef SRC_TINT_IR_VALUE_H_
 #define SRC_TINT_IR_VALUE_H_
 
-#include "src/tint/castable.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
@@ -29,7 +29,7 @@
 namespace tint::ir {
 
 /// Value in the IR.
-class Value : public Castable<Value> {
+class Value : public utils::Castable<Value> {
   public:
     /// Destructor
     ~Value() override;
diff --git a/src/tint/number.h b/src/tint/number.h
index 7bc1eea..06ab7fd 100644
--- a/src/tint/number.h
+++ b/src/tint/number.h
@@ -21,10 +21,10 @@
 #include <limits>
 #include <optional>
 
-#include "src/tint/traits.h"
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/result.h"
 #include "src/tint/utils/string_stream.h"
+#include "src/tint/utils/traits.h"
 
 // Forward declaration
 namespace tint {
@@ -274,7 +274,7 @@
 /// However since C++ don't have native binary16 type, the value is stored as float.
 using f16 = Number<detail::NumberKindF16>;
 
-template <typename T, traits::EnableIf<IsFloatingPoint<T>>* = nullptr>
+template <typename T, utils::traits::EnableIf<IsFloatingPoint<T>>* = nullptr>
 inline const auto kPi = T(UnwrapNumber<T>(3.14159265358979323846));
 
 /// True iff T is an abstract number type
@@ -282,7 +282,7 @@
 constexpr bool IsAbstract = std::is_same_v<T, AInt> || std::is_same_v<T, AFloat>;
 
 /// @returns the friendly name of Number type T
-template <typename T, traits::EnableIf<IsNumber<T>>* = nullptr>
+template <typename T, utils::traits::EnableIf<IsNumber<T>>* = nullptr>
 const char* FriendlyName() {
     if constexpr (std::is_same_v<T, AInt>) {
         return "abstract-int";
@@ -302,7 +302,7 @@
 }
 
 /// @returns the friendly name of T when T is bool
-template <typename T, traits::EnableIf<std::is_same_v<T, bool>>* = nullptr>
+template <typename T, utils::traits::EnableIf<std::is_same_v<T, bool>>* = nullptr>
 const char* FriendlyName() {
     return "bool";
 }
@@ -438,7 +438,8 @@
 }
 
 /// @returns a + b, or an empty optional if the resulting value overflowed the float value
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedAdd(FloatingPointT a, FloatingPointT b) {
     auto result = FloatingPointT{a.value + b.value};
     if (!std::isfinite(result.value)) {
@@ -470,7 +471,8 @@
 }
 
 /// @returns a + b, or an empty optional if the resulting value overflowed the float value
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedSub(FloatingPointT a, FloatingPointT b) {
     auto result = FloatingPointT{a.value - b.value};
     if (!std::isfinite(result.value)) {
@@ -514,7 +516,8 @@
 }
 
 /// @returns a * b, or an empty optional if the resulting value overflowed the float value
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedMul(FloatingPointT a, FloatingPointT b) {
     auto result = FloatingPointT{a.value * b.value};
     if (!std::isfinite(result.value)) {
@@ -537,7 +540,8 @@
 }
 
 /// @returns a / b, or an empty optional if the resulting value overflowed the float value
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedDiv(FloatingPointT a, FloatingPointT b) {
     if (b == FloatingPointT{0.0} || b == FloatingPointT{-0.0}) {
         return {};
@@ -577,7 +581,8 @@
 
 /// @returns the remainder of a / b, or an empty optional if the resulting value overflowed the
 /// float value
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedMod(FloatingPointT a, FloatingPointT b) {
     if (b == FloatingPointT{0.0} || b == FloatingPointT{-0.0}) {
         return {};
@@ -599,7 +604,8 @@
 
 /// @returns the value of `base` raised to the power `exp`, or an empty optional if the operation
 /// cannot be performed.
-template <typename FloatingPointT, typename = traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
+template <typename FloatingPointT,
+          typename = utils::traits::EnableIf<IsFloatingPoint<FloatingPointT>>>
 inline std::optional<FloatingPointT> CheckedPow(FloatingPointT base, FloatingPointT exp) {
     static_assert(IsNumber<FloatingPointT>);
     if ((base < 0) || (base == 0 && exp <= 0)) {
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 0c00452..b7fda95 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -170,53 +170,53 @@
 
     /// Evaluates to true if T can be converted to an identifier.
     template <typename T>
-    static constexpr const bool IsIdentifierLike = std::is_same_v<T, Symbol> ||  // Symbol
-                                                   std::is_enum_v<T> ||          // Enum
-                                                   traits::IsStringLike<T>;      // String
+    static constexpr const bool IsIdentifierLike = std::is_same_v<T, Symbol> ||     // Symbol
+                                                   std::is_enum_v<T> ||             // Enum
+                                                   utils::traits::IsStringLike<T>;  // String
 
     /// A helper used to disable overloads if the first type in `TYPES` is a Source. Used to avoid
     /// ambiguities in overloads that take a Source as the first parameter and those that
     /// perfectly-forward the first argument.
     template <typename... TYPES>
-    using DisableIfSource =
-        traits::EnableIf<!IsSource<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using DisableIfSource = utils::traits::EnableIf<
+        !IsSource<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// A helper used to disable overloads if the first type in `TYPES` is a scalar type. Used to
     /// avoid ambiguities in overloads that take a scalar as the first parameter and those that
     /// perfectly-forward the first argument.
     template <typename... TYPES>
-    using DisableIfScalar =
-        traits::EnableIf<!IsScalar<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using DisableIfScalar = utils::traits::EnableIf<
+        !IsScalar<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// A helper used to enable overloads if the first type in `TYPES` is a scalar type. Used to
     /// avoid ambiguities in overloads that take a scalar as the first parameter and those that
     /// perfectly-forward the first argument.
     template <typename... TYPES>
-    using EnableIfScalar =
-        traits::EnableIf<IsScalar<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using EnableIfScalar = utils::traits::EnableIf<
+        IsScalar<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// A helper used to disable overloads if the first type in `TYPES` is a utils::Vector,
     /// utils::VectorRef or utils::VectorRef.
     template <typename... TYPES>
-    using DisableIfVectorLike = traits::EnableIf<
-        !detail::IsVectorLike<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>::value>;
+    using DisableIfVectorLike = utils::traits::EnableIf<!detail::IsVectorLike<
+        utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>::value>;
 
     /// A helper used to enable overloads if the first type in `TYPES` is identifier-like.
     template <typename... TYPES>
-    using EnableIfIdentifierLike =
-        traits::EnableIf<IsIdentifierLike<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using EnableIfIdentifierLike = utils::traits::EnableIf<
+        IsIdentifierLike<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// A helper used to disable overloads if the first type in `TYPES` is Infer or an abstract
     /// numeric.
     template <typename... TYPES>
-    using DisableIfInferOrAbstract =
-        traits::EnableIf<!IsInferOrAbstract<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using DisableIfInferOrAbstract = utils::traits::EnableIf<
+        !IsInferOrAbstract<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// A helper used to enable overloads if the first type in `TYPES` is Infer or an abstract
     /// numeric.
     template <typename... TYPES>
-    using EnableIfInferOrAbstract =
-        traits::EnableIf<IsInferOrAbstract<traits::Decay<traits::NthTypeOf<0, TYPES..., void>>>>;
+    using EnableIfInferOrAbstract = utils::traits::EnableIf<
+        IsInferOrAbstract<utils::traits::Decay<utils::traits::NthTypeOf<0, TYPES..., void>>>>;
 
     /// VarOptions is a helper for accepting an arbitrary number of order independent options for
     /// constructing an ast::Var.
@@ -258,7 +258,8 @@
         template <typename... ARGS>
         explicit LetOptions(ARGS&&... args) {
             static constexpr bool has_init =
-                (traits::IsTypeOrDerived<traits::PtrElTy<ARGS>, ast::Expression> || ...);
+                (utils::traits::IsTypeOrDerived<utils::traits::PtrElTy<ARGS>, ast::Expression> ||
+                 ...);
             static_assert(has_init, "Let() must be constructed with an initializer expression");
             (Set(std::forward<ARGS>(args)), ...);
         }
@@ -281,7 +282,8 @@
         template <typename... ARGS>
         explicit ConstOptions(ARGS&&... args) {
             static constexpr bool has_init =
-                (traits::IsTypeOrDerived<traits::PtrElTy<ARGS>, ast::Expression> || ...);
+                (utils::traits::IsTypeOrDerived<utils::traits::PtrElTy<ARGS>, ast::Expression> ||
+                 ...);
             static_assert(has_init, "Const() must be constructed with an initializer expression");
             (Set(std::forward<ARGS>(args)), ...);
         }
@@ -476,7 +478,7 @@
     /// @param args the arguments to pass to the constructor
     /// @returns the node pointer
     template <typename T, typename... ARGS>
-    traits::EnableIfIsType<T, ast::Node>* create(const Source& source, ARGS&&... args) {
+    utils::traits::EnableIfIsType<T, ast::Node>* create(const Source& source, ARGS&&... args) {
         AssertNotMoved();
         return ast_nodes_.Create<T>(id_, AllocateNodeID(), source, std::forward<ARGS>(args)...);
     }
@@ -488,7 +490,7 @@
     /// destructed.
     /// @returns the node pointer
     template <typename T>
-    traits::EnableIfIsType<T, ast::Node>* create() {
+    utils::traits::EnableIfIsType<T, ast::Node>* create() {
         AssertNotMoved();
         return ast_nodes_.Create<T>(id_, AllocateNodeID(), source_);
     }
@@ -502,10 +504,10 @@
     /// @param args the remaining arguments to pass to the constructor
     /// @returns the node pointer
     template <typename T, typename ARG0, typename... ARGS>
-    traits::EnableIf</* T is ast::Node and ARG0 is not Source */
-                     traits::IsTypeOrDerived<T, ast::Node> &&
-                         !traits::IsTypeOrDerived<ARG0, Source>,
-                     T>*
+    utils::traits::EnableIf</* T is ast::Node and ARG0 is not Source */
+                            utils::traits::IsTypeOrDerived<T, ast::Node> &&
+                                !utils::traits::IsTypeOrDerived<ARG0, Source>,
+                            T>*
     create(ARG0&& arg0, ARGS&&... args) {
         AssertNotMoved();
         return ast_nodes_.Create<T>(id_, AllocateNodeID(), source_, std::forward<ARG0>(arg0),
@@ -517,9 +519,9 @@
     /// @param args the arguments to pass to the constructor
     /// @returns the node pointer
     template <typename T, typename... ARGS>
-    traits::EnableIf<traits::IsTypeOrDerived<T, sem::Node> &&
-                         !traits::IsTypeOrDerived<T, type::Node>,
-                     T>*
+    utils::traits::EnableIf<utils::traits::IsTypeOrDerived<T, sem::Node> &&
+                                !utils::traits::IsTypeOrDerived<T, type::Node>,
+                            T>*
     create(ARGS&&... args) {
         AssertNotMoved();
         return sem_nodes_.Create<T>(std::forward<ARGS>(args)...);
@@ -530,10 +532,10 @@
     /// @param args the arguments to pass to the constructor
     /// @returns the node pointer
     template <typename T, typename... ARGS>
-    traits::EnableIf<traits::IsTypeOrDerived<T, constant::Value> &&
-                         !traits::IsTypeOrDerived<T, constant::Composite> &&
-                         !traits::IsTypeOrDerived<T, constant::Splat>,
-                     T>*
+    utils::traits::EnableIf<utils::traits::IsTypeOrDerived<T, constant::Value> &&
+                                !utils::traits::IsTypeOrDerived<T, constant::Composite> &&
+                                !utils::traits::IsTypeOrDerived<T, constant::Splat>,
+                            T>*
     create(ARGS&&... args) {
         AssertNotMoved();
         return constant_nodes_.Create<T>(std::forward<ARGS>(args)...);
@@ -547,9 +549,10 @@
     /// @param type the composite type
     /// @param elements the composite elements
     /// @returns the node pointer
-    template <typename T,
-              typename = traits::EnableIf<traits::IsTypeOrDerived<T, constant::Composite> ||
-                                          traits::IsTypeOrDerived<T, constant::Splat>>>
+    template <
+        typename T,
+        typename = utils::traits::EnableIf<utils::traits::IsTypeOrDerived<T, constant::Composite> ||
+                                           utils::traits::IsTypeOrDerived<T, constant::Splat>>>
     const constant::Value* create(const type::Type* type,
                                   utils::VectorRef<const constant::Value*> elements) {
         AssertNotMoved();
@@ -561,7 +564,9 @@
     /// @param element the splat element
     /// @param n the number of elements
     /// @returns the node pointer
-    template <typename T, typename = traits::EnableIf<traits::IsTypeOrDerived<T, constant::Splat>>>
+    template <
+        typename T,
+        typename = utils::traits::EnableIf<utils::traits::IsTypeOrDerived<T, constant::Splat>>>
     const constant::Splat* create(const type::Type* type,
                                   const constant::Value* element,
                                   size_t n) {
@@ -576,7 +581,7 @@
     /// @param args the arguments to pass to the constructor
     /// @returns the new, or existing node
     template <typename T, typename... ARGS>
-    traits::EnableIfIsType<T, type::Node>* create(ARGS&&... args) {
+    utils::traits::EnableIfIsType<T, type::Node>* create(ARGS&&... args) {
         AssertNotMoved();
         return types_.Get<T>(std::forward<ARGS>(args)...);
     }
@@ -615,7 +620,8 @@
                   typename = DisableIfSource<NAME>,
                   typename = std::enable_if_t<!std::is_same_v<std::decay_t<NAME>, ast::Type>>>
         ast::Type operator()(NAME&& name, ARGS&&... args) const {
-            if constexpr (traits::IsTypeOrDerived<traits::PtrElTy<NAME>, ast::Expression>) {
+            if constexpr (utils::traits::IsTypeOrDerived<utils::traits::PtrElTy<NAME>,
+                                                         ast::Expression>) {
                 static_assert(sizeof...(ARGS) == 0);
                 return {name};
             } else {
@@ -1479,7 +1485,8 @@
     /// @return an ast::Identifier with the given symbol
     template <typename IDENTIFIER>
     const ast::Identifier* Ident(IDENTIFIER&& identifier) {
-        if constexpr (traits::IsTypeOrDerived<traits::PtrElTy<IDENTIFIER>, ast::Identifier>) {
+        if constexpr (utils::traits::IsTypeOrDerived<utils::traits::PtrElTy<IDENTIFIER>,
+                                                     ast::Identifier>) {
             return identifier;  // Passthrough
         } else {
             return Ident(source_, std::forward<IDENTIFIER>(identifier));
@@ -1518,7 +1525,7 @@
 
     /// @param expr the expression
     /// @return expr (passthrough)
-    template <typename T, typename = traits::EnableIfIsType<T, ast::Expression>>
+    template <typename T, typename = utils::traits::EnableIfIsType<T, ast::Expression>>
     const T* Expr(const T* expr) {
         return expr;
     }
@@ -2734,8 +2741,9 @@
     const ast::MemberAccessorExpression* MemberAccessor(const Source& source,
                                                         OBJECT&& object,
                                                         MEMBER&& member) {
-        static_assert(!traits::IsType<traits::PtrElTy<MEMBER>, ast::TemplatedIdentifier>,
-                      "it is currently invalid for a structure to hold a templated member");
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<MEMBER>, ast::TemplatedIdentifier>,
+            "it is currently invalid for a structure to hold a templated member");
         return create<ast::MemberAccessorExpression>(source, Expr(std::forward<OBJECT>(object)),
                                                      Ident(std::forward<MEMBER>(member)));
     }
@@ -2882,8 +2890,8 @@
         utils::VectorRef<const ast::Attribute*> attributes = utils::Empty,
         utils::VectorRef<const ast::Attribute*> return_type_attributes = utils::Empty) {
         const ast::BlockStatement* block = nullptr;
-        using BODY_T = traits::PtrElTy<BODY>;
-        if constexpr (traits::IsTypeOrDerived<BODY_T, ast::BlockStatement> ||
+        using BODY_T = utils::traits::PtrElTy<BODY>;
+        if constexpr (utils::traits::IsTypeOrDerived<BODY_T, ast::BlockStatement> ||
                       std::is_same_v<BODY_T, std::nullptr_t>) {
             block = body;
         } else {
@@ -3753,8 +3761,9 @@
     const ast::DiagnosticAttribute* DiagnosticAttribute(const Source& source,
                                                         builtin::DiagnosticSeverity severity,
                                                         NAME&& rule_name) {
-        static_assert(!traits::IsType<traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
-                      "it is invalid for a diagnostic rule name to be templated");
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule name to be templated");
         return create<ast::DiagnosticAttribute>(
             source, ast::DiagnosticControl(severity, Ident(std::forward<NAME>(rule_name))));
     }
@@ -3872,7 +3881,7 @@
     /// @param args a mix of ast::Expression, ast::Statement, ast::Variables.
     /// @returns the function
     template <typename... ARGS,
-              typename = traits::EnableIf<(CanWrapInStatement<ARGS>::value && ...)>>
+              typename = utils::traits::EnableIf<(CanWrapInStatement<ARGS>::value && ...)>>
     const ast::Function* WrapInFunction(ARGS&&... args) {
         utils::Vector stmts{
             WrapInStatement(std::forward<ARGS>(args))...,
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index e729a17..8061f74 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -693,7 +693,8 @@
 
 /// A StatementBuilder for ast::SwitchStatement
 /// @see StatementBuilder
-struct SwitchStatementBuilder final : public Castable<SwitchStatementBuilder, StatementBuilder> {
+struct SwitchStatementBuilder final
+    : public utils::Castable<SwitchStatementBuilder, StatementBuilder> {
     /// Constructor
     /// @param cond the switch statement condition
     explicit SwitchStatementBuilder(const ast::Expression* cond) : condition(cond) {}
@@ -717,7 +718,7 @@
 
 /// A StatementBuilder for ast::IfStatement
 /// @see StatementBuilder
-struct IfStatementBuilder final : public Castable<IfStatementBuilder, StatementBuilder> {
+struct IfStatementBuilder final : public utils::Castable<IfStatementBuilder, StatementBuilder> {
     /// Constructor
     /// @param c the if-statement condition
     explicit IfStatementBuilder(const ast::Expression* c) : cond(c) {}
@@ -738,7 +739,7 @@
 
 /// A StatementBuilder for ast::LoopStatement
 /// @see StatementBuilder
-struct LoopStatementBuilder final : public Castable<LoopStatementBuilder, StatementBuilder> {
+struct LoopStatementBuilder final : public utils::Castable<LoopStatementBuilder, StatementBuilder> {
     /// @param builder the program builder
     /// @returns the built ast::LoopStatement
     ast::LoopStatement* Build(ProgramBuilder* builder) const override {
diff --git a/src/tint/reader/spirv/function.h b/src/tint/reader/spirv/function.h
index 11fc92c..19452b1 100644
--- a/src/tint/reader/spirv/function.h
+++ b/src/tint/reader/spirv/function.h
@@ -407,7 +407,7 @@
 /// become immutable. The builders may hold mutable state while the
 /// StatementBlock is being constructed, which becomes an immutable node on
 /// StatementBlock::Finalize().
-class StatementBuilder : public Castable<StatementBuilder, ast::Statement> {
+class StatementBuilder : public utils::Castable<StatementBuilder, ast::Statement> {
   public:
     /// Constructor
     StatementBuilder() : Base(ProgramID(), ast::NodeID(), Source{}) {}
diff --git a/src/tint/reader/spirv/parser_type.h b/src/tint/reader/spirv/parser_type.h
index 8623be5..d3295ee 100644
--- a/src/tint/reader/spirv/parser_type.h
+++ b/src/tint/reader/spirv/parser_type.h
@@ -23,11 +23,11 @@
 #include "src/tint/builtin/access.h"
 #include "src/tint/builtin/address_space.h"
 #include "src/tint/builtin/texel_format.h"
-#include "src/tint/castable.h"
 #include "src/tint/symbol.h"
 #include "src/tint/type/sampler_kind.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/block_allocator.h"
+#include "src/tint/utils/castable.h"
 
 // Forward declarations
 namespace tint {
@@ -37,7 +37,7 @@
 namespace tint::reader::spirv {
 
 /// Type is the base class for all types
-class Type : public Castable<Type> {
+class Type : public utils::Castable<Type> {
   public:
     /// Constructor
     Type();
@@ -97,7 +97,7 @@
 using TypeList = std::vector<const Type*>;
 
 /// `void` type
-struct Void final : public Castable<Void, Type> {
+struct Void final : public utils::Castable<Void, Type> {
     /// @param b the ProgramBuilder used to construct the AST types
     /// @returns the constructed ast::Type node for the given type
     ast::Type Build(ProgramBuilder& b) const override;
@@ -109,7 +109,7 @@
 };
 
 /// `bool` type
-struct Bool final : public Castable<Bool, Type> {
+struct Bool final : public utils::Castable<Bool, Type> {
     /// @param b the ProgramBuilder used to construct the AST types
     /// @returns the constructed ast::Type node for the given type
     ast::Type Build(ProgramBuilder& b) const override;
@@ -121,7 +121,7 @@
 };
 
 /// `u32` type
-struct U32 final : public Castable<U32, Type> {
+struct U32 final : public utils::Castable<U32, Type> {
     /// @param b the ProgramBuilder used to construct the AST types
     /// @returns the constructed ast::Type node for the given type
     ast::Type Build(ProgramBuilder& b) const override;
@@ -133,7 +133,7 @@
 };
 
 /// `f32` type
-struct F32 final : public Castable<F32, Type> {
+struct F32 final : public utils::Castable<F32, Type> {
     /// @param b the ProgramBuilder used to construct the AST types
     /// @returns the constructed ast::Type node for the given type
     ast::Type Build(ProgramBuilder& b) const override;
@@ -145,7 +145,7 @@
 };
 
 /// `i32` type
-struct I32 final : public Castable<I32, Type> {
+struct I32 final : public utils::Castable<I32, Type> {
     /// @param b the ProgramBuilder used to construct the AST types
     /// @returns the constructed ast::Type node for the given type
     ast::Type Build(ProgramBuilder& b) const override;
@@ -157,7 +157,7 @@
 };
 
 /// `ptr<SC, T, AM>` type
-struct Pointer final : public Castable<Pointer, Type> {
+struct Pointer final : public utils::Castable<Pointer, Type> {
     /// Constructor
     /// @param ty the store type
     /// @param sc the pointer address space
@@ -188,7 +188,7 @@
 /// `ref<SC, T, AM>` type
 /// Note this has no AST representation, but is used for type tracking in the
 /// reader.
-struct Reference final : public Castable<Reference, Type> {
+struct Reference final : public utils::Castable<Reference, Type> {
     /// Constructor
     /// @param ty the referenced type
     /// @param sc the reference address space
@@ -217,7 +217,7 @@
 };
 
 /// `vecN<T>` type
-struct Vector final : public Castable<Vector, Type> {
+struct Vector final : public utils::Castable<Vector, Type> {
     /// Constructor
     /// @param ty the element type
     /// @param sz the number of elements in the vector
@@ -243,7 +243,7 @@
 };
 
 /// `matNxM<T>` type
-struct Matrix final : public Castable<Matrix, Type> {
+struct Matrix final : public utils::Castable<Matrix, Type> {
     /// Constructor
     /// @param ty the matrix element type
     /// @param c the number of columns in the matrix
@@ -272,7 +272,7 @@
 };
 
 /// `array<T, N>` type
-struct Array final : public Castable<Array, Type> {
+struct Array final : public utils::Castable<Array, Type> {
     /// Constructor
     /// @param el the element type
     /// @param sz the number of elements in the array. 0 represents runtime-sized
@@ -302,7 +302,7 @@
 };
 
 /// `sampler` type
-struct Sampler final : public Castable<Sampler, Type> {
+struct Sampler final : public utils::Castable<Sampler, Type> {
     /// Constructor
     /// @param k the sampler kind
     explicit Sampler(type::SamplerKind k);
@@ -325,7 +325,7 @@
 };
 
 /// Base class for texture types
-struct Texture : public Castable<Texture, Type> {
+struct Texture : public utils::Castable<Texture, Type> {
     ~Texture() override;
 
     /// Constructor
@@ -341,7 +341,7 @@
 };
 
 /// `texture_depth_D` type
-struct DepthTexture final : public Castable<DepthTexture, Texture> {
+struct DepthTexture final : public utils::Castable<DepthTexture, Texture> {
     /// Constructor
     /// @param d the texture dimensions
     explicit DepthTexture(type::TextureDimension d);
@@ -361,7 +361,7 @@
 };
 
 /// `texture_depth_multisampled_D` type
-struct DepthMultisampledTexture final : public Castable<DepthMultisampledTexture, Texture> {
+struct DepthMultisampledTexture final : public utils::Castable<DepthMultisampledTexture, Texture> {
     /// Constructor
     /// @param d the texture dimensions
     explicit DepthMultisampledTexture(type::TextureDimension d);
@@ -381,7 +381,7 @@
 };
 
 /// `texture_multisampled_D<T>` type
-struct MultisampledTexture final : public Castable<MultisampledTexture, Texture> {
+struct MultisampledTexture final : public utils::Castable<MultisampledTexture, Texture> {
     /// Constructor
     /// @param d the texture dimensions
     /// @param t the multisampled texture type
@@ -405,7 +405,7 @@
 };
 
 /// `texture_D<T>` type
-struct SampledTexture final : public Castable<SampledTexture, Texture> {
+struct SampledTexture final : public utils::Castable<SampledTexture, Texture> {
     /// Constructor
     /// @param d the texture dimensions
     /// @param t the sampled texture type
@@ -429,7 +429,7 @@
 };
 
 /// `texture_storage_D<F>` type
-struct StorageTexture final : public Castable<StorageTexture, Texture> {
+struct StorageTexture final : public utils::Castable<StorageTexture, Texture> {
     /// Constructor
     /// @param d the texture dimensions
     /// @param f the storage image format
@@ -457,7 +457,7 @@
 };
 
 /// Base class for named types
-struct Named : public Castable<Named, Type> {
+struct Named : public utils::Castable<Named, Type> {
     /// Constructor
     /// @param n the type name
     explicit Named(Symbol n);
@@ -479,7 +479,7 @@
 };
 
 /// `type T = N` type
-struct Alias final : public Castable<Alias, Named> {
+struct Alias final : public utils::Castable<Alias, Named> {
     /// Constructor
     /// @param n the alias name
     /// @param t the aliased type
@@ -498,7 +498,7 @@
 };
 
 /// `struct N { ... };` type
-struct Struct final : public Castable<Struct, Named> {
+struct Struct final : public utils::Castable<Struct, Named> {
     /// Constructor
     /// @param n the struct name
     /// @param m the member types
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index f795053..1fc7e84 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -28,7 +28,7 @@
 #include "absl/strings/charconv.h"
 #include "src/tint/debug.h"
 #include "src/tint/number.h"
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 namespace tint::reader::wgsl {
 namespace {
@@ -45,16 +45,16 @@
     // See https://www.w3.org/TR/WGSL/#blankspace
 
     auto* utf8 = reinterpret_cast<const uint8_t*>(&str[i]);
-    auto [cp, n] = text::utf8::Decode(utf8, str.size() - i);
+    auto [cp, n] = utils::utf8::Decode(utf8, str.size() - i);
 
     if (n == 0) {
         return false;
     }
 
-    static const auto kSpace = text::CodePoint(0x0020);  // space
-    static const auto kHTab = text::CodePoint(0x0009);   // horizontal tab
-    static const auto kL2R = text::CodePoint(0x200E);    // left-to-right mark
-    static const auto kR2L = text::CodePoint(0x200F);    // right-to-left mark
+    static const auto kSpace = utils::CodePoint(0x0020);  // space
+    static const auto kHTab = utils::CodePoint(0x0009);   // horizontal tab
+    static const auto kL2R = utils::CodePoint(0x200E);    // left-to-right mark
+    static const auto kR2L = utils::CodePoint(0x200F);    // right-to-left mark
 
     if (cp == kSpace || cp == kHTab || cp == kL2R || cp == kR2L) {
         *is_blankspace = true;
@@ -959,12 +959,12 @@
     // Must begin with an XID_Source unicode character, or underscore
     {
         auto* utf8 = reinterpret_cast<const uint8_t*>(&at(pos()));
-        auto [code_point, n] = text::utf8::Decode(utf8, length() - pos());
+        auto [code_point, n] = utils::utf8::Decode(utf8, length() - pos());
         if (n == 0) {
             advance();  // Skip the bad byte.
             return {Token::Type::kError, source, "invalid UTF-8"};
         }
-        if (code_point != text::CodePoint('_') && !code_point.IsXIDStart()) {
+        if (code_point != utils::CodePoint('_') && !code_point.IsXIDStart()) {
             return {};
         }
         // Consume start codepoint
@@ -974,7 +974,7 @@
     while (!is_eol()) {
         // Must continue with an XID_Continue unicode character
         auto* utf8 = reinterpret_cast<const uint8_t*>(&at(pos()));
-        auto [code_point, n] = text::utf8::Decode(utf8, line().size() - pos());
+        auto [code_point, n] = utils::utf8::Decode(utf8, line().size() - pos());
         if (n == 0) {
             advance();  // Skip the bad byte.
             return {Token::Type::kError, source, "invalid UTF-8"};
diff --git a/src/tint/reflection.h b/src/tint/reflection.h
index 0591838..d341f98 100644
--- a/src/tint/reflection.h
+++ b/src/tint/reflection.h
@@ -15,7 +15,8 @@
 #ifndef SRC_TINT_REFLECTION_H_
 #define SRC_TINT_REFLECTION_H_
 
-#include "src/tint/traits.h"
+#include <type_traits>
+
 #include "src/tint/utils/concat.h"
 #include "src/tint/utils/foreach_macro.h"
 
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 45dca0b..d6a2f0f 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -2185,7 +2185,7 @@
 namespace InternalAttributeDeps {
 namespace {
 
-class TestAttribute : public Castable<TestAttribute, ast::InternalAttribute> {
+class TestAttribute : public utils::Castable<TestAttribute, ast::InternalAttribute> {
   public:
     TestAttribute(ProgramID pid, ast::NodeID nid, const ast::IdentifierExpression* dep)
         : Base(pid, nid, utils::Vector{dep}) {}
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index b90ea00..2aa755a 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -447,7 +447,8 @@
     auto* ty = First(cs...)->Type();
     auto* el_ty = type::Type::ElementOf(ty, &n);
     if (el_ty == ty) {
-        constexpr bool kHasIndexParam = traits::IsType<size_t, traits::LastParameterType<F>>;
+        constexpr bool kHasIndexParam =
+            utils::traits::IsType<size_t, utils::traits::LastParameterType<F>>;
         if constexpr (kHasIndexParam) {
             return f(cs..., index);
         } else {
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index 62dde26..6aa050b 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -58,7 +58,7 @@
 constexpr static const size_t kNumFixedCandidates = 8;
 
 /// A special type that matches all TypeMatchers
-class Any final : public Castable<Any, type::Type> {
+class Any final : public utils::Castable<Any, type::Type> {
   public:
     Any() : Base(0u, type::Flags{}) {}
     ~Any() override = default;
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 5aac00c..1ec680d 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -276,7 +276,7 @@
         sem = builder_->create<sem::GlobalVariable>(
             v, ty, sem::EvaluationStage::kRuntime, builtin::AddressSpace::kUndefined,
             builtin::Access::kUndefined,
-            /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
+            /* constant_value */ nullptr, std::nullopt, std::nullopt);
     } else {
         sem = builder_->create<sem::LocalVariable>(v, ty, sem::EvaluationStage::kRuntime,
                                                    builtin::AddressSpace::kUndefined,
@@ -336,7 +336,7 @@
     auto* sem = builder_->create<sem::GlobalVariable>(
         v, ty, sem::EvaluationStage::kOverride, builtin::AddressSpace::kUndefined,
         builtin::Access::kUndefined,
-        /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
+        /* constant_value */ nullptr, std::nullopt, std::nullopt);
     sem->SetInitializer(rhs);
 
     if (auto* id_attr = ast::GetAttribute<ast::IdAttribute>(v->attributes)) {
@@ -430,7 +430,7 @@
     auto* sem = is_global
                     ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
                           c, ty, sem::EvaluationStage::kConstant, builtin::AddressSpace::kUndefined,
-                          builtin::Access::kUndefined, value, sem::BindingPoint{}, std::nullopt))
+                          builtin::Access::kUndefined, value, std::nullopt, std::nullopt))
                     : static_cast<sem::Variable*>(builder_->create<sem::LocalVariable>(
                           c, ty, sem::EvaluationStage::kConstant, builtin::AddressSpace::kUndefined,
                           builtin::Access::kUndefined, current_statement_, value));
@@ -528,7 +528,7 @@
 
     sem::Variable* sem = nullptr;
     if (is_global) {
-        sem::BindingPoint binding_point;
+        std::optional<sem::BindingPoint> binding_point;
         if (var->HasBindingPoint()) {
             uint32_t binding = 0;
             {
@@ -640,8 +640,9 @@
         }
     }
 
-    sem::BindingPoint binding_point;
+    std::optional<sem::BindingPoint> binding_point;
     if (param->HasBindingPoint()) {
+        binding_point = sem::BindingPoint{};
         {
             ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding value"};
             TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
@@ -651,7 +652,7 @@
             if (!materialized) {
                 return nullptr;
             }
-            binding_point.binding = materialized->ConstantValue()->ValueAs<u32>();
+            binding_point->binding = materialized->ConstantValue()->ValueAs<u32>();
         }
         {
             ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@group value"};
@@ -662,7 +663,7 @@
             if (!materialized) {
                 return nullptr;
             }
-            binding_point.group = materialized->ConstantValue()->ValueAs<u32>();
+            binding_point->group = materialized->ConstantValue()->ValueAs<u32>();
         }
     }
 
@@ -770,7 +771,7 @@
 
 void Resolver::SetShadows() {
     for (auto it : dependencies_.shadows) {
-        CastableBase* b = sem_.Get(it.value);
+        utils::CastableBase* b = sem_.Get(it.value);
         if (TINT_UNLIKELY(!b)) {
             TINT_ICE(Resolver, diagnostics_)
                 << "AST node '" << it.value->TypeInfo().name << "' had no semantic info\n"
@@ -4264,7 +4265,8 @@
 SEM* Resolver::StatementScope(const ast::Statement* ast, SEM* sem, F&& callback) {
     builder_->Sem().Add(ast, sem);
 
-    auto* as_compound = As<sem::CompoundStatement, CastFlags::kDontErrorOnImpossibleCast>(sem);
+    auto* as_compound =
+        As<sem::CompoundStatement, utils::CastFlags::kDontErrorOnImpossibleCast>(sem);
 
     // Helper to handle attributes that are supported on certain types of statement.
     auto handle_attributes = [&](auto* stmt, sem::Statement* sem_stmt, const char* use) {
diff --git a/src/tint/resolver/resolver_test_helper.h b/src/tint/resolver/resolver_test_helper.h
index 1afa838..c4aec90 100644
--- a/src/tint/resolver/resolver_test_helper.h
+++ b/src/tint/resolver/resolver_test_helper.h
@@ -29,9 +29,9 @@
 #include "src/tint/sem/statement.h"
 #include "src/tint/sem/value_expression.h"
 #include "src/tint/sem/variable.h"
-#include "src/tint/traits.h"
 #include "src/tint/type/abstract_float.h"
 #include "src/tint/type/abstract_int.h"
+#include "src/tint/utils/traits.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::resolver {
@@ -97,11 +97,11 @@
     /// @return true if all users are as expected
     bool CheckVarUsers(const ast::Variable* var,
                        utils::VectorRef<const ast::Expression*> expected_users) {
-        auto& var_users = Sem().Get(var)->Users();
-        if (var_users.size() != expected_users.Length()) {
+        auto var_users = Sem().Get(var)->Users();
+        if (var_users.Length() != expected_users.Length()) {
             return false;
         }
-        for (size_t i = 0; i < var_users.size(); i++) {
+        for (size_t i = 0; i < var_users.Length(); i++) {
             if (var_users[i]->Declaration() != expected_users[i]) {
                 return false;
             }
@@ -587,7 +587,7 @@
     /// @param args the value nested elements will be initialized with
     /// @return a new AST expression of the alias type
     template <bool IS_COMPOSITE = is_composite>
-    static inline traits::EnableIf<!IS_COMPOSITE, const ast::Expression*> Expr(
+    static inline utils::traits::EnableIf<!IS_COMPOSITE, const ast::Expression*> Expr(
         ProgramBuilder& b,
         utils::VectorRef<Scalar> args) {
         // Cast
@@ -598,7 +598,7 @@
     /// @param args the value nested elements will be initialized with
     /// @return a new AST expression of the alias type
     template <bool IS_COMPOSITE = is_composite>
-    static inline traits::EnableIf<IS_COMPOSITE, const ast::Expression*> Expr(
+    static inline utils::traits::EnableIf<IS_COMPOSITE, const ast::Expression*> Expr(
         ProgramBuilder& b,
         utils::VectorRef<Scalar> args) {
         // Construct
@@ -819,7 +819,7 @@
 /// Creates a Value of DataType<T> from a scalar `v`
 template <typename T>
 Value Val(T v) {
-    static_assert(traits::IsTypeIn<T, Scalar>, "v must be a Number of bool");
+    static_assert(utils::traits::IsTypeIn<T, Scalar>, "v must be a Number of bool");
     return Value::Create<T>(utils::Vector<Scalar, 1>{v});
 }
 
diff --git a/src/tint/resolver/validation_test.cc b/src/tint/resolver/validation_test.cc
index 6485a6a..6b84d85 100644
--- a/src/tint/resolver/validation_test.cc
+++ b/src/tint/resolver/validation_test.cc
@@ -50,13 +50,13 @@
 
 using ResolverValidationTest = ResolverTest;
 
-class FakeStmt final : public Castable<FakeStmt, ast::Statement> {
+class FakeStmt final : public utils::Castable<FakeStmt, ast::Statement> {
   public:
     FakeStmt(ProgramID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
     FakeStmt* Clone(CloneContext*) const override { return nullptr; }
 };
 
-class FakeExpr final : public Castable<FakeExpr, ast::Expression> {
+class FakeExpr final : public utils::Castable<FakeExpr, ast::Expression> {
   public:
     FakeExpr(ProgramID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
     FakeExpr* Clone(CloneContext*) const override { return nullptr; }
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 65d7310..f72000b 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -1346,11 +1346,14 @@
     utils::Hashmap<sem::BindingPoint, const ast::Variable*, 8> binding_points;
     for (auto* global : func->TransitivelyReferencedGlobals()) {
         auto* var_decl = global->Declaration()->As<ast::Var>();
-        if (!var_decl || !var_decl->HasBindingPoint()) {
+        if (!var_decl) {
             continue;
         }
         auto bp = global->BindingPoint();
-        if (auto added = binding_points.Add(bp, var_decl);
+        if (!bp) {
+            continue;
+        }
+        if (auto added = binding_points.Add(*bp, var_decl);
             !added &&
             IsValidationEnabled(decl->attributes,
                                 ast::DisabledValidation::kBindingPointCollision) &&
@@ -1364,7 +1367,7 @@
             AddError(
                 "entry point '" + func_name +
                     "' references multiple variables that use the same resource binding @group(" +
-                    std::to_string(bp.group) + "), @binding(" + std::to_string(bp.binding) + ")",
+                    std::to_string(bp->group) + "), @binding(" + std::to_string(bp->binding) + ")",
                 var_decl->source);
             AddNote("first resource binding usage declared here", (*added.value)->source);
             return false;
@@ -2505,7 +2508,7 @@
 }
 
 bool Validator::NoDuplicateAttributes(utils::VectorRef<const ast::Attribute*> attributes) const {
-    utils::Hashmap<const TypeInfo*, Source, 8> seen;
+    utils::Hashmap<const utils::TypeInfo*, Source, 8> seen;
     utils::Vector<const ast::DiagnosticControl*, 8> diagnostic_controls;
     for (auto* d : attributes) {
         if (auto* diag = d->As<ast::DiagnosticAttribute>()) {
diff --git a/src/tint/sem/array_count.cc b/src/tint/sem/array_count.cc
index 90abb69..2f7d1e1 100644
--- a/src/tint/sem/array_count.cc
+++ b/src/tint/sem/array_count.cc
@@ -22,7 +22,7 @@
 namespace tint::sem {
 
 NamedOverrideArrayCount::NamedOverrideArrayCount(const GlobalVariable* var)
-    : Base(static_cast<size_t>(TypeInfo::Of<NamedOverrideArrayCount>().full_hashcode)),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<NamedOverrideArrayCount>().full_hashcode)),
       variable(var) {}
 NamedOverrideArrayCount::~NamedOverrideArrayCount() = default;
 
@@ -43,7 +43,8 @@
 }
 
 UnnamedOverrideArrayCount::UnnamedOverrideArrayCount(const ValueExpression* e)
-    : Base(static_cast<size_t>(TypeInfo::Of<UnnamedOverrideArrayCount>().full_hashcode)), expr(e) {}
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<UnnamedOverrideArrayCount>().full_hashcode)),
+      expr(e) {}
 UnnamedOverrideArrayCount::~UnnamedOverrideArrayCount() = default;
 
 bool UnnamedOverrideArrayCount::Equals(const UniqueNode& other) const {
diff --git a/src/tint/sem/array_count.h b/src/tint/sem/array_count.h
index a4143d8..9f0cd53 100644
--- a/src/tint/sem/array_count.h
+++ b/src/tint/sem/array_count.h
@@ -29,7 +29,8 @@
 /// override N : i32;
 /// type arr = array<i32, N>
 /// ```
-class NamedOverrideArrayCount final : public Castable<NamedOverrideArrayCount, type::ArrayCount> {
+class NamedOverrideArrayCount final
+    : public utils::Castable<NamedOverrideArrayCount, type::ArrayCount> {
   public:
     /// Constructor
     /// @param var the `override` variable
@@ -58,7 +59,7 @@
 /// type arr = array<i32, N*2>
 /// ```
 class UnnamedOverrideArrayCount final
-    : public Castable<UnnamedOverrideArrayCount, type::ArrayCount> {
+    : public utils::Castable<UnnamedOverrideArrayCount, type::ArrayCount> {
   public:
     /// Constructor
     /// @param e the override expression
diff --git a/src/tint/sem/block_statement.h b/src/tint/sem/block_statement.h
index e5d4969..9cf1e3e 100644
--- a/src/tint/sem/block_statement.h
+++ b/src/tint/sem/block_statement.h
@@ -31,7 +31,7 @@
 
 /// Holds semantic information about a block, such as parent block and variables
 /// declared in the block.
-class BlockStatement : public Castable<BlockStatement, CompoundStatement> {
+class BlockStatement : public utils::Castable<BlockStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this block statement
@@ -50,7 +50,8 @@
 };
 
 /// The root block statement for a function
-class FunctionBlockStatement final : public Castable<FunctionBlockStatement, BlockStatement> {
+class FunctionBlockStatement final
+    : public utils::Castable<FunctionBlockStatement, BlockStatement> {
   public:
     /// Constructor
     /// @param function the owning function
@@ -61,7 +62,7 @@
 };
 
 /// Holds semantic information about a loop body block or for-loop body block
-class LoopBlockStatement final : public Castable<LoopBlockStatement, BlockStatement> {
+class LoopBlockStatement final : public utils::Castable<LoopBlockStatement, BlockStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this block statement
diff --git a/src/tint/sem/break_if_statement.h b/src/tint/sem/break_if_statement.h
index c5b62de..dcab9bb 100644
--- a/src/tint/sem/break_if_statement.h
+++ b/src/tint/sem/break_if_statement.h
@@ -28,7 +28,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about a break-if statement
-class BreakIfStatement final : public Castable<BreakIfStatement, CompoundStatement> {
+class BreakIfStatement final : public utils::Castable<BreakIfStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this break-if statement
diff --git a/src/tint/sem/builtin.h b/src/tint/sem/builtin.h
index b535e72..07c3ad1 100644
--- a/src/tint/sem/builtin.h
+++ b/src/tint/sem/builtin.h
@@ -77,7 +77,7 @@
 bool IsDP4aBuiltin(builtin::Function i);
 
 /// Builtin holds the semantic information for a builtin function.
-class Builtin final : public Castable<Builtin, CallTarget> {
+class Builtin final : public utils::Castable<Builtin, CallTarget> {
   public:
     /// Constructor
     /// @param type the builtin type
diff --git a/src/tint/sem/builtin_enum_expression.h b/src/tint/sem/builtin_enum_expression.h
index 3a403fb..86c70c3 100644
--- a/src/tint/sem/builtin_enum_expression.h
+++ b/src/tint/sem/builtin_enum_expression.h
@@ -26,7 +26,7 @@
 
 /// Base class for BuiltinEnumExpression.
 /// Useful for Is() queries.
-class BuiltinEnumExpressionBase : public Castable<BuiltinEnumExpressionBase, Expression> {
+class BuiltinEnumExpressionBase : public utils::Castable<BuiltinEnumExpressionBase, Expression> {
   public:
     /// Constructor
     /// @param declaration the AST node
@@ -41,7 +41,7 @@
 /// builtin enumerator value.
 template <typename ENUM>
 class BuiltinEnumExpression
-    : public Castable<BuiltinEnumExpression<ENUM>, BuiltinEnumExpressionBase> {
+    : public utils::Castable<BuiltinEnumExpression<ENUM>, BuiltinEnumExpressionBase> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/call.h b/src/tint/sem/call.h
index 3079eb1..f24899d 100644
--- a/src/tint/sem/call.h
+++ b/src/tint/sem/call.h
@@ -26,7 +26,7 @@
 
 /// Call is the base class for semantic nodes that hold semantic information for
 /// ast::CallExpression nodes.
-class Call final : public Castable<Call, ValueExpression> {
+class Call final : public utils::Castable<Call, ValueExpression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/call_target.h b/src/tint/sem/call_target.h
index 096aa8f..0bcb5d2 100644
--- a/src/tint/sem/call_target.h
+++ b/src/tint/sem/call_target.h
@@ -67,7 +67,7 @@
 
 /// CallTarget is the base for callable functions, builtins, value constructors and value
 /// conversions.
-class CallTarget : public Castable<CallTarget, Node> {
+class CallTarget : public utils::Castable<CallTarget, Node> {
   public:
     /// Constructor
     /// @param stage the earliest evaluation stage for a call to this target
diff --git a/src/tint/sem/expression.h b/src/tint/sem/expression.h
index 884ff4d..36b4b71 100644
--- a/src/tint/sem/expression.h
+++ b/src/tint/sem/expression.h
@@ -26,7 +26,7 @@
 namespace tint::sem {
 
 /// Expression holds the semantic information for expression nodes.
-class Expression : public Castable<Expression, Node> {
+class Expression : public utils::Castable<Expression, Node> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/for_loop_statement.h b/src/tint/sem/for_loop_statement.h
index 7e22912..154e00d 100644
--- a/src/tint/sem/for_loop_statement.h
+++ b/src/tint/sem/for_loop_statement.h
@@ -28,7 +28,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about a for-loop statement
-class ForLoopStatement final : public Castable<ForLoopStatement, CompoundStatement> {
+class ForLoopStatement final : public utils::Castable<ForLoopStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this for-loop statement
diff --git a/src/tint/sem/function.cc b/src/tint/sem/function.cc
index ec51b7b..eab78e0 100644
--- a/src/tint/sem/function.cc
+++ b/src/tint/sem/function.cc
@@ -60,8 +60,8 @@
             continue;
         }
 
-        if (global->Declaration()->HasBindingPoint()) {
-            ret.push_back({global, global->BindingPoint()});
+        if (auto bp = global->BindingPoint()) {
+            ret.push_back({global, *bp});
         }
     }
     return ret;
@@ -75,8 +75,8 @@
             continue;
         }
 
-        if (global->Declaration()->HasBindingPoint()) {
-            ret.push_back({global, global->BindingPoint()});
+        if (auto bp = global->BindingPoint()) {
+            ret.push_back({global, *bp});
         }
     }
     return ret;
@@ -114,13 +114,13 @@
 }
 
 Function::VariableBindings Function::TransitivelyReferencedVariablesOfType(
-    const tint::TypeInfo* type) const {
+    const tint::utils::TypeInfo* type) const {
     VariableBindings ret;
     for (auto* global : TransitivelyReferencedGlobals()) {
         auto* unwrapped_type = global->Type()->UnwrapRef();
         if (unwrapped_type->TypeInfo().Is(type)) {
-            if (global->Declaration()->HasBindingPoint()) {
-                ret.push_back({global, global->BindingPoint()});
+            if (auto bp = global->BindingPoint()) {
+                ret.push_back({global, *bp});
             }
         }
     }
@@ -147,8 +147,8 @@
             continue;
         }
 
-        if (global->Declaration()->HasBindingPoint()) {
-            ret.push_back({global, global->BindingPoint()});
+        if (auto bp = global->BindingPoint()) {
+            ret.push_back({global, *bp});
         }
     }
     return ret;
@@ -172,8 +172,8 @@
             continue;
         }
 
-        if (global->Declaration()->HasBindingPoint()) {
-            ret.push_back({global, global->BindingPoint()});
+        if (auto bp = global->BindingPoint()) {
+            ret.push_back({global, *bp});
         }
     }
 
diff --git a/src/tint/sem/function.h b/src/tint/sem/function.h
index b157e7d..31390f7 100644
--- a/src/tint/sem/function.h
+++ b/src/tint/sem/function.h
@@ -47,7 +47,7 @@
 using WorkgroupSize = std::array<std::optional<uint32_t>, 3>;
 
 /// Function holds the semantic information for function nodes.
-class Function final : public Castable<Function, CallTarget> {
+class Function final : public utils::Castable<Function, CallTarget> {
   public:
     /// A vector of [Variable*, sem::BindingPoint] pairs
     using VariableBindings = std::vector<std::pair<const Variable*, sem::BindingPoint>>;
@@ -219,14 +219,14 @@
     /// must be decorated with both binding and group attributes.
     /// @param type the type of the variables to find
     /// @returns the referenced variables
-    VariableBindings TransitivelyReferencedVariablesOfType(const tint::TypeInfo* type) const;
+    VariableBindings TransitivelyReferencedVariablesOfType(const tint::utils::TypeInfo* type) const;
 
     /// Retrieves any referenced variables of the given type. Note, the variables
     /// must be decorated with both binding and group attributes.
     /// @returns the referenced variables
     template <typename T>
     VariableBindings TransitivelyReferencedVariablesOfType() const {
-        return TransitivelyReferencedVariablesOfType(&TypeInfo::Of<T>());
+        return TransitivelyReferencedVariablesOfType(&utils::TypeInfo::Of<T>());
     }
 
     /// Checks if the given entry point is an ancestor
diff --git a/src/tint/sem/function_expression.h b/src/tint/sem/function_expression.h
index de5ac5a..9d9e8ec 100644
--- a/src/tint/sem/function_expression.h
+++ b/src/tint/sem/function_expression.h
@@ -26,7 +26,7 @@
 
 /// FunctionExpression holds the semantic information for expression nodes that resolve to
 /// functions.
-class FunctionExpression : public Castable<FunctionExpression, Expression> {
+class FunctionExpression : public utils::Castable<FunctionExpression, Expression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/if_statement.h b/src/tint/sem/if_statement.h
index 5b27fba..42146fc 100644
--- a/src/tint/sem/if_statement.h
+++ b/src/tint/sem/if_statement.h
@@ -28,7 +28,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about an if statement
-class IfStatement final : public Castable<IfStatement, CompoundStatement> {
+class IfStatement final : public utils::Castable<IfStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this if statement
diff --git a/src/tint/sem/index_accessor_expression.h b/src/tint/sem/index_accessor_expression.h
index 8118659..c2379f5 100644
--- a/src/tint/sem/index_accessor_expression.h
+++ b/src/tint/sem/index_accessor_expression.h
@@ -23,7 +23,8 @@
 namespace tint::sem {
 
 /// IndexAccessorExpression holds the semantic information for a ast::IndexAccessorExpression node.
-class IndexAccessorExpression final : public Castable<IndexAccessorExpression, ValueExpression> {
+class IndexAccessorExpression final
+    : public utils::Castable<IndexAccessorExpression, ValueExpression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/info.h b/src/tint/sem/info.h
index 29b3bc4..9e5a139 100644
--- a/src/tint/sem/info.h
+++ b/src/tint/sem/info.h
@@ -77,11 +77,11 @@
     /// @param ast_node the AST node
     /// @returns a pointer to the semantic node if found, otherwise nullptr
     template <typename SEM = InferFromAST,
-              typename AST = CastableBase,
+              typename AST = utils::CastableBase,
               typename RESULT = GetResultType<SEM, AST>>
     const RESULT* Get(const AST* ast_node) const {
         static_assert(std::is_same_v<SEM, InferFromAST> ||
-                          !traits::IsTypeOrDerived<SemanticNodeTypeFor<AST>, SEM>,
+                          !utils::traits::IsTypeOrDerived<SemanticNodeTypeFor<AST>, SEM>,
                       "explicit template argument is unnecessary");
         if (ast_node && ast_node->node_id.value < nodes_.size()) {
             return As<RESULT>(nodes_[ast_node->node_id.value]);
@@ -141,7 +141,8 @@
     /// Records that this variable (transitively) references the given override variable.
     /// @param from the item the variable is referenced from
     /// @param var the module-scope override variable
-    void AddTransitivelyReferencedOverride(const CastableBase* from, const GlobalVariable* var) {
+    void AddTransitivelyReferencedOverride(const utils::CastableBase* from,
+                                           const GlobalVariable* var) {
         if (referenced_overrides_.count(from) == 0) {
             referenced_overrides_.insert({from, TransitivelyReferenced{}});
         }
@@ -150,7 +151,8 @@
 
     /// @param from the key to look up
     /// @returns all transitively referenced override variables or nullptr if none set
-    const TransitivelyReferenced* TransitivelyReferencedOverrides(const CastableBase* from) const {
+    const TransitivelyReferenced* TransitivelyReferencedOverrides(
+        const utils::CastableBase* from) const {
         if (referenced_overrides_.count(from) == 0) {
             return nullptr;
         }
@@ -166,9 +168,9 @@
 
   private:
     // AST node index to semantic node
-    std::vector<const CastableBase*> nodes_;
+    std::vector<const utils::CastableBase*> nodes_;
     // Lists transitively referenced overrides for the given item
-    std::unordered_map<const CastableBase*, TransitivelyReferenced> referenced_overrides_;
+    std::unordered_map<const utils::CastableBase*, TransitivelyReferenced> referenced_overrides_;
     // The semantic module
     sem::Module* module_ = nullptr;
 };
diff --git a/src/tint/sem/load.h b/src/tint/sem/load.h
index e02e050..674b631 100644
--- a/src/tint/sem/load.h
+++ b/src/tint/sem/load.h
@@ -23,7 +23,7 @@
 /// Load is a semantic expression which represents the load of a reference to a non-reference value.
 /// Loads from reference types are implicit in WGSL, so the Load semantic node shares the same AST
 /// node as the inner semantic node.
-class Load final : public Castable<Load, ValueExpression> {
+class Load final : public utils::Castable<Load, ValueExpression> {
   public:
     /// Constructor
     /// @param reference the reference expression being loaded
diff --git a/src/tint/sem/loop_statement.h b/src/tint/sem/loop_statement.h
index 6dc7037..c09bbb5 100644
--- a/src/tint/sem/loop_statement.h
+++ b/src/tint/sem/loop_statement.h
@@ -25,7 +25,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about a loop statement
-class LoopStatement final : public Castable<LoopStatement, CompoundStatement> {
+class LoopStatement final : public utils::Castable<LoopStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this loop statement
@@ -41,7 +41,7 @@
 
 /// Holds semantic information about a loop continuing block
 class LoopContinuingBlockStatement final
-    : public Castable<LoopContinuingBlockStatement, BlockStatement> {
+    : public utils::Castable<LoopContinuingBlockStatement, BlockStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this block statement
diff --git a/src/tint/sem/materialize.h b/src/tint/sem/materialize.h
index 4ce1190..c7b612f 100644
--- a/src/tint/sem/materialize.h
+++ b/src/tint/sem/materialize.h
@@ -25,7 +25,7 @@
 /// the same AST node as the inner semantic node.
 /// Abstract numerics types may only be used by compile-time expressions, so a Materialize semantic
 /// node must have a valid Constant value.
-class Materialize final : public Castable<Materialize, ValueExpression> {
+class Materialize final : public utils::Castable<Materialize, ValueExpression> {
   public:
     /// Constructor
     /// @param expr the inner expression, being materialized
diff --git a/src/tint/sem/member_accessor_expression.h b/src/tint/sem/member_accessor_expression.h
index 7ed0a41..cea9f2d 100644
--- a/src/tint/sem/member_accessor_expression.h
+++ b/src/tint/sem/member_accessor_expression.h
@@ -30,7 +30,7 @@
 
 /// MemberAccessorExpression holds the semantic information for a
 /// ast::MemberAccessorExpression node.
-class MemberAccessorExpression : public Castable<MemberAccessorExpression, ValueExpression> {
+class MemberAccessorExpression : public utils::Castable<MemberAccessorExpression, ValueExpression> {
   public:
     /// Destructor
     ~MemberAccessorExpression() override;
@@ -64,7 +64,8 @@
 /// StructMemberAccess holds the semantic information for a
 /// ast::MemberAccessorExpression node that represents an access to a structure
 /// member.
-class StructMemberAccess final : public Castable<StructMemberAccess, MemberAccessorExpression> {
+class StructMemberAccess final
+    : public utils::Castable<StructMemberAccess, MemberAccessorExpression> {
   public:
     /// Constructor
     /// @param declaration the AST node
@@ -96,7 +97,7 @@
 
 /// Swizzle holds the semantic information for a ast::MemberAccessorExpression
 /// node that represents a vector swizzle.
-class Swizzle final : public Castable<Swizzle, MemberAccessorExpression> {
+class Swizzle final : public utils::Castable<Swizzle, MemberAccessorExpression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/module.h b/src/tint/sem/module.h
index ba4e0a9..172c22b 100644
--- a/src/tint/sem/module.h
+++ b/src/tint/sem/module.h
@@ -29,7 +29,7 @@
 
 /// Module holds the top-level semantic types, functions and global variables
 /// used by a Program.
-class Module final : public Castable<Module, Node> {
+class Module final : public utils::Castable<Module, Node> {
   public:
     /// Constructor
     /// @param dep_ordered_decls the dependency-ordered module-scope declarations
diff --git a/src/tint/sem/node.h b/src/tint/sem/node.h
index 3f2df55..9a1c706 100644
--- a/src/tint/sem/node.h
+++ b/src/tint/sem/node.h
@@ -15,12 +15,12 @@
 #ifndef SRC_TINT_SEM_NODE_H_
 #define SRC_TINT_SEM_NODE_H_
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 
 namespace tint::sem {
 
 /// Node is the base class for all semantic nodes
-class Node : public Castable<Node> {
+class Node : public utils::Castable<Node> {
   public:
     /// Constructor
     Node();
diff --git a/src/tint/sem/statement.h b/src/tint/sem/statement.h
index 373b9f6..a0eeb44 100644
--- a/src/tint/sem/statement.h
+++ b/src/tint/sem/statement.h
@@ -57,7 +57,7 @@
 }  // namespace detail
 
 /// Statement holds the semantic information for a statement.
-class Statement : public Castable<Statement, Node> {
+class Statement : public utils::Castable<Statement, Node> {
   public:
     /// Constructor
     /// @param declaration the AST node for this statement
@@ -133,7 +133,7 @@
 
 /// CompoundStatement is the base class of statements that can hold other
 /// statements.
-class CompoundStatement : public Castable<Statement, Statement> {
+class CompoundStatement : public utils::Castable<Statement, Statement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this statement
diff --git a/src/tint/sem/struct.h b/src/tint/sem/struct.h
index d1d20cc..2b437af 100644
--- a/src/tint/sem/struct.h
+++ b/src/tint/sem/struct.h
@@ -38,7 +38,7 @@
 namespace tint::sem {
 
 /// Struct holds the semantic information for structures.
-class Struct final : public Castable<Struct, type::Struct> {
+class Struct final : public utils::Castable<Struct, type::Struct> {
   public:
     /// Constructor
     /// @param declaration the AST structure declaration
@@ -73,7 +73,7 @@
 };
 
 /// StructMember holds the semantic information for structure members.
-class StructMember final : public Castable<StructMember, type::StructMember> {
+class StructMember final : public utils::Castable<StructMember, type::StructMember> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
diff --git a/src/tint/sem/switch_statement.h b/src/tint/sem/switch_statement.h
index 503d0a5..68c754f 100644
--- a/src/tint/sem/switch_statement.h
+++ b/src/tint/sem/switch_statement.h
@@ -37,7 +37,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about an switch statement
-class SwitchStatement final : public Castable<SwitchStatement, CompoundStatement> {
+class SwitchStatement final : public utils::Castable<SwitchStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this switch statement
@@ -64,7 +64,7 @@
 };
 
 /// Holds semantic information about a switch case statement
-class CaseStatement final : public Castable<CaseStatement, CompoundStatement> {
+class CaseStatement final : public utils::Castable<CaseStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this case statement
@@ -98,7 +98,7 @@
 };
 
 /// Holds semantic information about a switch case selector
-class CaseSelector final : public Castable<CaseSelector, Node> {
+class CaseSelector final : public utils::Castable<CaseSelector, Node> {
   public:
     /// Constructor
     /// @param decl the selector declaration
diff --git a/src/tint/sem/type_expression.h b/src/tint/sem/type_expression.h
index 0d4e43b..981be69 100644
--- a/src/tint/sem/type_expression.h
+++ b/src/tint/sem/type_expression.h
@@ -25,7 +25,7 @@
 namespace tint::sem {
 
 /// TypeExpression holds the semantic information for expression nodes that resolve to types.
-class TypeExpression : public Castable<TypeExpression, Expression> {
+class TypeExpression : public utils::Castable<TypeExpression, Expression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/type_mappings.h b/src/tint/sem/type_mappings.h
index efcd3a6..4f31254 100644
--- a/src/tint/sem/type_mappings.h
+++ b/src/tint/sem/type_mappings.h
@@ -20,9 +20,9 @@
 #include "src/tint/sem/builtin_enum_expression.h"
 
 // Forward declarations
-namespace tint {
+namespace tint::utils {
 class CastableBase;
-}  // namespace tint
+}  // namespace tint::utils
 namespace tint::ast {
 class AccessorExpression;
 class BinaryExpression;
@@ -78,7 +78,7 @@
 struct TypeMappings {
     //! @cond Doxygen_Suppress
     BuiltinEnumExpression<builtin::BuiltinValue>* operator()(ast::BuiltinAttribute*);
-    CastableBase* operator()(ast::Node*);
+    utils::CastableBase* operator()(ast::Node*);
     Expression* operator()(ast::Expression*);
     ForLoopStatement* operator()(ast::ForLoopStatement*);
     Function* operator()(ast::Function*);
diff --git a/src/tint/sem/value_constructor.h b/src/tint/sem/value_constructor.h
index 34c3b43..02fff3b 100644
--- a/src/tint/sem/value_constructor.h
+++ b/src/tint/sem/value_constructor.h
@@ -21,7 +21,7 @@
 namespace tint::sem {
 
 /// ValueConstructor is the CallTarget for a value constructor.
-class ValueConstructor final : public Castable<ValueConstructor, CallTarget> {
+class ValueConstructor final : public utils::Castable<ValueConstructor, CallTarget> {
   public:
     /// Constructor
     /// @param type the type that's being constructed
diff --git a/src/tint/sem/value_conversion.h b/src/tint/sem/value_conversion.h
index 2d2ab38..890856e 100644
--- a/src/tint/sem/value_conversion.h
+++ b/src/tint/sem/value_conversion.h
@@ -20,7 +20,7 @@
 namespace tint::sem {
 
 /// ValueConversion is the CallTarget for a value conversion (cast).
-class ValueConversion final : public Castable<ValueConversion, CallTarget> {
+class ValueConversion final : public utils::Castable<ValueConversion, CallTarget> {
   public:
     /// Constructor
     /// @param type the target type of the cast
diff --git a/src/tint/sem/value_expression.h b/src/tint/sem/value_expression.h
index a6fb624..2059ae3 100644
--- a/src/tint/sem/value_expression.h
+++ b/src/tint/sem/value_expression.h
@@ -29,7 +29,7 @@
 namespace tint::sem {
 
 /// ValueExpression holds the semantic information for expression nodes.
-class ValueExpression : public Castable<ValueExpression, Expression> {
+class ValueExpression : public utils::Castable<ValueExpression, Expression> {
   public:
     /// Constructor
     /// @param declaration the AST node
diff --git a/src/tint/sem/variable.cc b/src/tint/sem/variable.cc
index 8f8248c..2e3cb88 100644
--- a/src/tint/sem/variable.cc
+++ b/src/tint/sem/variable.cc
@@ -61,7 +61,7 @@
                                builtin::AddressSpace address_space,
                                builtin::Access access,
                                const constant::Value* constant_value,
-                               sem::BindingPoint binding_point,
+                               std::optional<sem::BindingPoint> binding_point,
                                std::optional<uint32_t> location)
     : Base(declaration, type, stage, address_space, access, constant_value),
       binding_point_(binding_point),
@@ -75,7 +75,7 @@
                      builtin::AddressSpace address_space,
                      builtin::Access access,
                      const ParameterUsage usage /* = ParameterUsage::kNone */,
-                     sem::BindingPoint binding_point /* = {} */,
+                     std::optional<sem::BindingPoint> binding_point /* = {} */,
                      std::optional<uint32_t> location /* = std::nullopt */)
     : Base(declaration, type, EvaluationStage::kRuntime, address_space, access, nullptr),
       index_(index),
diff --git a/src/tint/sem/variable.h b/src/tint/sem/variable.h
index 701a3f1..4cc0b1e 100644
--- a/src/tint/sem/variable.h
+++ b/src/tint/sem/variable.h
@@ -45,7 +45,7 @@
 
 /// Variable is the base class for local variables, global variables and
 /// parameters.
-class Variable : public Castable<Variable, Node> {
+class Variable : public utils::Castable<Variable, Node> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
@@ -91,10 +91,10 @@
     void SetInitializer(const ValueExpression* initializer) { initializer_ = initializer; }
 
     /// @returns the expressions that use the variable
-    const std::vector<const VariableUser*>& Users() const { return users_; }
+    utils::VectorRef<const VariableUser*> Users() const { return users_; }
 
     /// @param user the user to add
-    void AddUser(const VariableUser* user) { users_.emplace_back(user); }
+    void AddUser(const VariableUser* user) { users_.Push(user); }
 
   private:
     const ast::Variable* const declaration_;
@@ -104,11 +104,11 @@
     const builtin::Access access_;
     const constant::Value* constant_value_;
     const ValueExpression* initializer_ = nullptr;
-    std::vector<const VariableUser*> users_;
+    utils::Vector<const VariableUser*, 8> users_;
 };
 
 /// LocalVariable is a function-scope variable
-class LocalVariable final : public Castable<LocalVariable, Variable> {
+class LocalVariable final : public utils::Castable<LocalVariable, Variable> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
@@ -133,19 +133,19 @@
     const sem::Statement* Statement() const { return statement_; }
 
     /// @returns the Type, Function or Variable that this local variable shadows
-    const CastableBase* Shadows() const { return shadows_; }
+    const utils::CastableBase* Shadows() const { return shadows_; }
 
     /// Sets the Type, Function or Variable that this local variable shadows
     /// @param shadows the Type, Function or Variable that this variable shadows
-    void SetShadows(const CastableBase* shadows) { shadows_ = shadows; }
+    void SetShadows(const utils::CastableBase* shadows) { shadows_ = shadows; }
 
   private:
     const sem::Statement* const statement_;
-    const CastableBase* shadows_ = nullptr;
+    const utils::CastableBase* shadows_ = nullptr;
 };
 
 /// GlobalVariable is a module-scope variable
-class GlobalVariable final : public Castable<GlobalVariable, Variable> {
+class GlobalVariable final : public utils::Castable<GlobalVariable, Variable> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
@@ -165,14 +165,14 @@
                    builtin::AddressSpace address_space,
                    builtin::Access access,
                    const constant::Value* constant_value,
-                   sem::BindingPoint binding_point = {},
+                   std::optional<sem::BindingPoint> binding_point = std::nullopt,
                    std::optional<uint32_t> location = std::nullopt);
 
     /// Destructor
     ~GlobalVariable() override;
 
     /// @returns the resource binding point for the variable
-    sem::BindingPoint BindingPoint() const { return binding_point_; }
+    std::optional<sem::BindingPoint> BindingPoint() const { return binding_point_; }
 
     /// @param id the constant identifier to assign to this variable
     void SetOverrideId(OverrideId id) { override_id_ = id; }
@@ -184,14 +184,14 @@
     std::optional<uint32_t> Location() const { return location_; }
 
   private:
-    const sem::BindingPoint binding_point_;
+    const std::optional<sem::BindingPoint> binding_point_;
 
     tint::OverrideId override_id_;
     std::optional<uint32_t> location_;
 };
 
 /// Parameter is a function parameter
-class Parameter final : public Castable<Parameter, Variable> {
+class Parameter final : public utils::Castable<Parameter, Variable> {
   public:
     /// Constructor for function parameters
     /// @param declaration the AST declaration node
@@ -208,7 +208,7 @@
               builtin::AddressSpace address_space,
               builtin::Access access,
               const ParameterUsage usage = ParameterUsage::kNone,
-              sem::BindingPoint binding_point = {},
+              std::optional<sem::BindingPoint> binding_point = {},
               std::optional<uint32_t> location = std::nullopt);
 
     /// Destructor
@@ -232,14 +232,14 @@
     void SetOwner(CallTarget const* owner) { owner_ = owner; }
 
     /// @returns the Type, Function or Variable that this local variable shadows
-    const CastableBase* Shadows() const { return shadows_; }
+    const utils::CastableBase* Shadows() const { return shadows_; }
 
     /// Sets the Type, Function or Variable that this local variable shadows
     /// @param shadows the Type, Function or Variable that this variable shadows
-    void SetShadows(const CastableBase* shadows) { shadows_ = shadows; }
+    void SetShadows(const utils::CastableBase* shadows) { shadows_ = shadows; }
 
     /// @returns the resource binding point for the parameter
-    sem::BindingPoint BindingPoint() const { return binding_point_; }
+    std::optional<sem::BindingPoint> BindingPoint() const { return binding_point_; }
 
     /// @returns the location value for the parameter, if set
     std::optional<uint32_t> Location() const { return location_; }
@@ -248,14 +248,14 @@
     const uint32_t index_;
     const ParameterUsage usage_;
     CallTarget const* owner_ = nullptr;
-    const CastableBase* shadows_ = nullptr;
-    const sem::BindingPoint binding_point_;
+    const utils::CastableBase* shadows_ = nullptr;
+    const std::optional<sem::BindingPoint> binding_point_;
     const std::optional<uint32_t> location_;
 };
 
 /// VariableUser holds the semantic information for an identifier expression
 /// node that resolves to a variable.
-class VariableUser final : public Castable<VariableUser, ValueExpression> {
+class VariableUser final : public utils::Castable<VariableUser, ValueExpression> {
   public:
     /// Constructor
     /// @param declaration the AST identifier node
diff --git a/src/tint/sem/while_statement.h b/src/tint/sem/while_statement.h
index fa136bd..75d9d93 100644
--- a/src/tint/sem/while_statement.h
+++ b/src/tint/sem/while_statement.h
@@ -28,7 +28,7 @@
 namespace tint::sem {
 
 /// Holds semantic information about a while statement
-class WhileStatement final : public Castable<WhileStatement, CompoundStatement> {
+class WhileStatement final : public utils::Castable<WhileStatement, CompoundStatement> {
   public:
     /// Constructor
     /// @param declaration the AST node for this while statement
diff --git a/src/tint/source.cc b/src/tint/source.cc
index 3f3ec7c..3ed939d 100644
--- a/src/tint/source.cc
+++ b/src/tint/source.cc
@@ -18,7 +18,7 @@
 #include <string_view>
 #include <utility>
 
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 namespace tint {
 namespace {
@@ -27,19 +27,19 @@
     // See https://www.w3.org/TR/WGSL/#blankspace
 
     auto* utf8 = reinterpret_cast<const uint8_t*>(&str[i]);
-    auto [cp, n] = text::utf8::Decode(utf8, str.size() - i);
+    auto [cp, n] = utils::utf8::Decode(utf8, str.size() - i);
 
     if (n == 0) {
         return false;
     }
 
-    static const auto kLF = text::CodePoint(0x000A);    // line feed
-    static const auto kVTab = text::CodePoint(0x000B);  // vertical tab
-    static const auto kFF = text::CodePoint(0x000C);    // form feed
-    static const auto kNL = text::CodePoint(0x0085);    // next line
-    static const auto kCR = text::CodePoint(0x000D);    // carriage return
-    static const auto kLS = text::CodePoint(0x2028);    // line separator
-    static const auto kPS = text::CodePoint(0x2029);    // parargraph separator
+    static const auto kLF = utils::CodePoint(0x000A);    // line feed
+    static const auto kVTab = utils::CodePoint(0x000B);  // vertical tab
+    static const auto kFF = utils::CodePoint(0x000C);    // form feed
+    static const auto kNL = utils::CodePoint(0x0085);    // next line
+    static const auto kCR = utils::CodePoint(0x000D);    // carriage return
+    static const auto kLS = utils::CodePoint(0x2028);    // line separator
+    static const auto kPS = utils::CodePoint(0x2029);    // parargraph separator
 
     if (cp == kLF || cp == kVTab || cp == kFF || cp == kNL || cp == kPS || cp == kLS) {
         *is_line_break = true;
@@ -54,7 +54,7 @@
 
         if (auto next_i = i + n; next_i < str.size()) {
             auto* next_utf8 = reinterpret_cast<const uint8_t*>(&str[next_i]);
-            auto [next_cp, next_n] = text::utf8::Decode(next_utf8, str.size() - next_i);
+            auto [next_cp, next_n] = utils::utf8::Decode(next_utf8, str.size() - next_i);
 
             if (next_n == 0) {
                 return false;
diff --git a/src/tint/switch.h b/src/tint/switch.h
index 41bb99a..df5bd95 100644
--- a/src/tint/switch.h
+++ b/src/tint/switch.h
@@ -18,8 +18,8 @@
 #include <tuple>
 #include <utility>
 
-#include "src/tint/castable.h"
 #include "src/tint/utils/bitcast.h"
+#include "src/tint/utils/castable.h"
 #include "src/tint/utils/defer.h"
 
 namespace tint {
@@ -43,13 +43,14 @@
 /// @note does not handle the Default case
 /// @see Switch().
 template <typename FN>
-using SwitchCaseType = std::remove_pointer_t<traits::ParameterType<std::remove_reference_t<FN>, 0>>;
+using SwitchCaseType =
+    std::remove_pointer_t<utils::traits::ParameterType<std::remove_reference_t<FN>, 0>>;
 
 /// Evaluates to true if the function `FN` has the signature of a Default case in a Switch().
 /// @see Switch().
 template <typename FN>
 inline constexpr bool IsDefaultCase =
-    std::is_same_v<traits::ParameterType<std::remove_reference_t<FN>, 0>, Default>;
+    std::is_same_v<utils::traits::ParameterType<std::remove_reference_t<FN>, 0>, Default>;
 
 /// Searches the list of Switch cases for a Default case, returning the index of the Default case.
 /// If the a Default case is not found in the tuple, then -1 is returned.
@@ -66,7 +67,7 @@
 
 /// Resolves to T if T is not nullptr_t, otherwise resolves to Ignore.
 template <typename T>
-using NullptrToIgnore = std::conditional_t<std::is_same_v<T, std::nullptr_t>, Ignore, T>;
+using NullptrToIgnore = std::conditional_t<std::is_same_v<T, std::nullptr_t>, utils::Ignore, T>;
 
 /// Resolves to `const TYPE` if any of `CASE_RETURN_TYPES` are const or pointer-to-const, otherwise
 /// resolves to TYPE.
@@ -91,7 +92,7 @@
 
 /// SwitchReturnTypeImpl specialization for non-castable case types and an inferred return type.
 template <typename... CASE_RETURN_TYPES>
-struct SwitchReturnTypeImpl</*IS_CASTABLE*/ false, Infer, CASE_RETURN_TYPES...> {
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ false, utils::detail::Infer, CASE_RETURN_TYPES...> {
     /// Resolves to the common type for all the cases return types.
     using type = std::common_type_t<CASE_RETURN_TYPES...>;
 };
@@ -107,10 +108,10 @@
 
 /// SwitchReturnTypeImpl specialization for castable case types and an inferred return type.
 template <typename... CASE_RETURN_TYPES>
-struct SwitchReturnTypeImpl</*IS_CASTABLE*/ true, Infer, CASE_RETURN_TYPES...> {
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ true, utils::detail::Infer, CASE_RETURN_TYPES...> {
   private:
-    using InferredType =
-        CastableCommonBase<detail::NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>;
+    using InferredType = utils::CastableCommonBase<
+        detail::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.
@@ -122,7 +123,7 @@
 /// from the case return types.
 template <typename REQUESTED_TYPE, typename... CASE_RETURN_TYPES>
 using SwitchReturnType = typename SwitchReturnTypeImpl<
-    IsCastable<NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>,
+    utils::IsCastable<NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>,
     REQUESTED_TYPE,
     CASE_RETURN_TYPES...>::type;
 
@@ -161,9 +162,11 @@
 /// @param cases the switch cases
 /// @return the value returned by the called case. If no cases matched, then the zero value for the
 /// consistent case type.
-template <typename RETURN_TYPE = detail::Infer, typename T = CastableBase, typename... CASES>
+template <typename RETURN_TYPE = utils::detail::Infer,
+          typename T = utils::CastableBase,
+          typename... CASES>
 inline auto Switch(T* object, CASES&&... cases) {
-    using ReturnType = detail::SwitchReturnType<RETURN_TYPE, traits::ReturnType<CASES>...>;
+    using ReturnType = detail::SwitchReturnType<RETURN_TYPE, utils::traits::ReturnType<CASES>...>;
     static constexpr int kDefaultIndex = detail::IndexOfDefaultCase<std::tuple<CASES...>>();
     static constexpr bool kHasDefaultCase = kDefaultIndex >= 0;
     static constexpr bool kHasReturnType = !std::is_same_v<ReturnType, void>;
@@ -202,7 +205,7 @@
     ReturnStorage storage;
     auto* result = utils::Bitcast<ReturnTypeOrU8*>(&storage);
 
-    const TypeInfo& type_info = object->TypeInfo();
+    const utils::TypeInfo& type_info = object->TypeInfo();
 
     // Examines the parameter type of the case function.
     // If the parameter is a pointer type that `object` is of, or derives from, then that case
diff --git a/src/tint/switch_bench.cc b/src/tint/switch_bench.cc
index ea6098b..e73d982 100644
--- a/src/tint/switch_bench.cc
+++ b/src/tint/switch_bench.cc
@@ -21,46 +21,46 @@
 namespace tint {
 namespace {
 
-struct Base : public tint::Castable<Base> {};
-struct A : public tint::Castable<A, Base> {};
-struct AA : public tint::Castable<AA, A> {};
-struct AAA : public tint::Castable<AAA, AA> {};
-struct AAB : public tint::Castable<AAB, AA> {};
-struct AAC : public tint::Castable<AAC, AA> {};
-struct AB : public tint::Castable<AB, A> {};
-struct ABA : public tint::Castable<ABA, AB> {};
-struct ABB : public tint::Castable<ABB, AB> {};
-struct ABC : public tint::Castable<ABC, AB> {};
-struct AC : public tint::Castable<AC, A> {};
-struct ACA : public tint::Castable<ACA, AC> {};
-struct ACB : public tint::Castable<ACB, AC> {};
-struct ACC : public tint::Castable<ACC, AC> {};
-struct B : public tint::Castable<B, Base> {};
-struct BA : public tint::Castable<BA, B> {};
-struct BAA : public tint::Castable<BAA, BA> {};
-struct BAB : public tint::Castable<BAB, BA> {};
-struct BAC : public tint::Castable<BAC, BA> {};
-struct BB : public tint::Castable<BB, B> {};
-struct BBA : public tint::Castable<BBA, BB> {};
-struct BBB : public tint::Castable<BBB, BB> {};
-struct BBC : public tint::Castable<BBC, BB> {};
-struct BC : public tint::Castable<BC, B> {};
-struct BCA : public tint::Castable<BCA, BC> {};
-struct BCB : public tint::Castable<BCB, BC> {};
-struct BCC : public tint::Castable<BCC, BC> {};
-struct C : public tint::Castable<C, Base> {};
-struct CA : public tint::Castable<CA, C> {};
-struct CAA : public tint::Castable<CAA, CA> {};
-struct CAB : public tint::Castable<CAB, CA> {};
-struct CAC : public tint::Castable<CAC, CA> {};
-struct CB : public tint::Castable<CB, C> {};
-struct CBA : public tint::Castable<CBA, CB> {};
-struct CBB : public tint::Castable<CBB, CB> {};
-struct CBC : public tint::Castable<CBC, CB> {};
-struct CC : public tint::Castable<CC, C> {};
-struct CCA : public tint::Castable<CCA, CC> {};
-struct CCB : public tint::Castable<CCB, CC> {};
-struct CCC : public tint::Castable<CCC, CC> {};
+struct Base : public tint::utils::Castable<Base> {};
+struct A : public tint::utils::Castable<A, Base> {};
+struct AA : public tint::utils::Castable<AA, A> {};
+struct AAA : public tint::utils::Castable<AAA, AA> {};
+struct AAB : public tint::utils::Castable<AAB, AA> {};
+struct AAC : public tint::utils::Castable<AAC, AA> {};
+struct AB : public tint::utils::Castable<AB, A> {};
+struct ABA : public tint::utils::Castable<ABA, AB> {};
+struct ABB : public tint::utils::Castable<ABB, AB> {};
+struct ABC : public tint::utils::Castable<ABC, AB> {};
+struct AC : public tint::utils::Castable<AC, A> {};
+struct ACA : public tint::utils::Castable<ACA, AC> {};
+struct ACB : public tint::utils::Castable<ACB, AC> {};
+struct ACC : public tint::utils::Castable<ACC, AC> {};
+struct B : public tint::utils::Castable<B, Base> {};
+struct BA : public tint::utils::Castable<BA, B> {};
+struct BAA : public tint::utils::Castable<BAA, BA> {};
+struct BAB : public tint::utils::Castable<BAB, BA> {};
+struct BAC : public tint::utils::Castable<BAC, BA> {};
+struct BB : public tint::utils::Castable<BB, B> {};
+struct BBA : public tint::utils::Castable<BBA, BB> {};
+struct BBB : public tint::utils::Castable<BBB, BB> {};
+struct BBC : public tint::utils::Castable<BBC, BB> {};
+struct BC : public tint::utils::Castable<BC, B> {};
+struct BCA : public tint::utils::Castable<BCA, BC> {};
+struct BCB : public tint::utils::Castable<BCB, BC> {};
+struct BCC : public tint::utils::Castable<BCC, BC> {};
+struct C : public tint::utils::Castable<C, Base> {};
+struct CA : public tint::utils::Castable<CA, C> {};
+struct CAA : public tint::utils::Castable<CAA, CA> {};
+struct CAB : public tint::utils::Castable<CAB, CA> {};
+struct CAC : public tint::utils::Castable<CAC, CA> {};
+struct CB : public tint::utils::Castable<CB, C> {};
+struct CBA : public tint::utils::Castable<CBA, CB> {};
+struct CBB : public tint::utils::Castable<CBB, CB> {};
+struct CBC : public tint::utils::Castable<CBC, CB> {};
+struct CC : public tint::utils::Castable<CC, C> {};
+struct CCA : public tint::utils::Castable<CCA, CC> {};
+struct CCB : public tint::utils::Castable<CCB, CC> {};
+struct CCC : public tint::utils::Castable<CCC, CC> {};
 
 using AllTypes = std::tuple<Base,
                             A,
diff --git a/src/tint/switch_test.cc b/src/tint/switch_test.cc
index d93f830..f35192e 100644
--- a/src/tint/switch_test.cc
+++ b/src/tint/switch_test.cc
@@ -22,15 +22,15 @@
 namespace tint {
 namespace {
 
-struct Animal : public tint::Castable<Animal> {};
-struct Amphibian : public tint::Castable<Amphibian, Animal> {};
-struct Mammal : public tint::Castable<Mammal, Animal> {};
-struct Reptile : public tint::Castable<Reptile, Animal> {};
-struct Frog : public tint::Castable<Frog, Amphibian> {};
-struct Bear : public tint::Castable<Bear, Mammal> {};
-struct Lizard : public tint::Castable<Lizard, Reptile> {};
-struct Gecko : public tint::Castable<Gecko, Lizard> {};
-struct Iguana : public tint::Castable<Iguana, Lizard> {};
+struct Animal : public tint::utils::Castable<Animal> {};
+struct Amphibian : public tint::utils::Castable<Amphibian, Animal> {};
+struct Mammal : public tint::utils::Castable<Mammal, Animal> {};
+struct Reptile : public tint::utils::Castable<Reptile, Animal> {};
+struct Frog : public tint::utils::Castable<Frog, Amphibian> {};
+struct Bear : public tint::utils::Castable<Bear, Mammal> {};
+struct Lizard : public tint::utils::Castable<Lizard, Reptile> {};
+struct Gecko : public tint::utils::Castable<Gecko, Lizard> {};
+struct Iguana : public tint::utils::Castable<Iguana, Lizard> {};
 
 TEST(Castable, SwitchNoDefault) {
     std::unique_ptr<Animal> frog = std::make_unique<Frog>();
@@ -331,8 +331,8 @@
             gecko.get(),                     //
             [](Mammal* p) { return p; },     //
             [](Amphibian* p) { return p; },  //
-            [](Default) -> CastableBase* { return nullptr; });
-        static_assert(std::is_same_v<decltype(result), CastableBase*>);
+            [](Default) -> utils::CastableBase* { return nullptr; });
+        static_assert(std::is_same_v<decltype(result), utils::CastableBase*>);
         EXPECT_EQ(result, nullptr);
     }
 }
@@ -444,12 +444,12 @@
         EXPECT_EQ(result, nullptr);
     }
     {
-        auto* result = Switch<CastableBase>(
+        auto* result = Switch<utils::CastableBase>(
             bear.get(),                   //
             [](Mammal* p) { return p; },  //
             [](Amphibian* p) { return const_cast<const Amphibian*>(p); },
             [](Default) { return nullptr; });
-        static_assert(std::is_same_v<decltype(result), const CastableBase*>);
+        static_assert(std::is_same_v<decltype(result), const utils::CastableBase*>);
         EXPECT_EQ(result, bear.get());
     }
     {
@@ -476,11 +476,11 @@
         EXPECT_EQ(result, nullptr);
     }
     {
-        auto* result = Switch<CastableBase>(
+        auto* result = Switch<utils::CastableBase>(
             bear.get(),                                                     //
             [](Mammal* p) { return p; },                                    //
             [](Amphibian* p) { return const_cast<const Amphibian*>(p); });  //
-        static_assert(std::is_same_v<decltype(result), const CastableBase*>);
+        static_assert(std::is_same_v<decltype(result), const utils::CastableBase*>);
         EXPECT_EQ(result, bear.get());
     }
     {
diff --git a/src/tint/symbol.cc b/src/tint/symbol.cc
index b64947f..7e69b5d 100644
--- a/src/tint/symbol.cc
+++ b/src/tint/symbol.cc
@@ -52,6 +52,10 @@
     return "$" + std::to_string(val_);
 }
 
+std::string_view Symbol::NameView() const {
+    return name_;
+}
+
 std::string Symbol::Name() const {
     return std::string(name_);
 }
diff --git a/src/tint/symbol.h b/src/tint/symbol.h
index a31388d..16d8409 100644
--- a/src/tint/symbol.h
+++ b/src/tint/symbol.h
@@ -77,6 +77,10 @@
     std::string to_str() const;
 
     /// Converts the symbol to the registered name
+    /// @returns the string_view representing the name of the symbol
+    std::string_view NameView() const;
+
+    /// Converts the symbol to the registered name
     /// @returns the string representing the name of the symbol
     std::string Name() const;
 
diff --git a/src/tint/symbol_table.cc b/src/tint/symbol_table.cc
index aae76ff..cdea0a4 100644
--- a/src/tint/symbol_table.cc
+++ b/src/tint/symbol_table.cc
@@ -26,14 +26,17 @@
 
 SymbolTable& SymbolTable::operator=(SymbolTable&&) = default;
 
-Symbol SymbolTable::Register(const std::string& name) {
+Symbol SymbolTable::Register(std::string_view name) {
     TINT_ASSERT(Symbol, !name.empty());
 
     auto it = name_to_symbol_.Find(name);
     if (it) {
         return *it;
     }
+    return RegisterInternal(name);
+}
 
+Symbol SymbolTable::RegisterInternal(std::string_view name) {
     char* name_mem = name_allocator_.Allocate(name.length() + 1);
     if (name_mem == nullptr) {
         return Symbol();
@@ -43,26 +46,28 @@
     std::string_view nv(name_mem, name.length());
 
     Symbol sym(next_symbol_, program_id_, nv);
-
     ++next_symbol_;
-
-    name_to_symbol_.Add(name_mem, sym);
+    name_to_symbol_.Add(sym.NameView(), sym);
 
     return sym;
 }
 
-Symbol SymbolTable::Get(const std::string& name) const {
+Symbol SymbolTable::Get(std::string_view name) const {
     auto it = name_to_symbol_.Find(name);
     return it ? *it : Symbol();
 }
 
-Symbol SymbolTable::New(std::string prefix /* = "" */) {
-    if (prefix.empty()) {
+Symbol SymbolTable::New(std::string_view prefix_view /* = "" */) {
+    std::string prefix;
+    if (prefix_view.empty()) {
         prefix = "tint_symbol";
+    } else {
+        prefix = std::string(prefix_view);
     }
+
     auto it = name_to_symbol_.Find(prefix);
     if (!it) {
-        return Register(prefix);
+        return RegisterInternal(prefix);
     }
 
     size_t i = 0;
@@ -77,13 +82,13 @@
         name = prefix + "_" + std::to_string(i);
     } while (name_to_symbol_.Contains(name));
 
+    auto sym = RegisterInternal(name);
     if (last_prefix) {
         *last_prefix = i;
     } else {
         last_prefix_to_index_.Add(prefix, i);
     }
-
-    return Register(name);
+    return sym;
 }
 
 }  // namespace tint
diff --git a/src/tint/symbol_table.h b/src/tint/symbol_table.h
index 5eeaec2..723b81b 100644
--- a/src/tint/symbol_table.h
+++ b/src/tint/symbol_table.h
@@ -56,12 +56,12 @@
     /// Registers a name into the symbol table, returning the Symbol.
     /// @param name the name to register
     /// @returns the symbol representing the given name
-    Symbol Register(const std::string& name);
+    Symbol Register(std::string_view name);
 
     /// Returns the symbol for the given `name`
     /// @param name the name to lookup
     /// @returns the symbol for the name or Symbol() if not found.
-    Symbol Get(const std::string& name) const;
+    Symbol Get(std::string_view name) const;
 
     /// Returns a new unique symbol with the given name, possibly suffixed with a
     /// unique number.
@@ -69,7 +69,7 @@
     /// @returns a new, unnamed symbol with the given name. If the name is already
     /// taken then this will be suffixed with an underscore and a unique numerical
     /// value
-    Symbol New(std::string name = "");
+    Symbol New(std::string_view name = "");
 
     /// Foreach calls the callback function `F` for each symbol in the table.
     /// @param callback must be a function or function-like object with the
@@ -88,6 +88,8 @@
     SymbolTable(const SymbolTable&) = delete;
     SymbolTable& operator=(const SymbolTable& other) = delete;
 
+    Symbol RegisterInternal(std::string_view name);
+
     // The value to be associated to the next registered symbol table entry.
     uint32_t next_symbol_ = 1;
 
diff --git a/src/tint/transform/add_block_attribute.h b/src/tint/transform/add_block_attribute.h
index d5e8c4e..3eeb148 100644
--- a/src/tint/transform/add_block_attribute.h
+++ b/src/tint/transform/add_block_attribute.h
@@ -24,11 +24,11 @@
 
 /// AddBlockAttribute is a transform that wrap the store type of a buffer into a struct if possible,
 /// then adds an `@internal(block)` attribute to the wrapper struct.
-class AddBlockAttribute final : public Castable<AddBlockAttribute, Transform> {
+class AddBlockAttribute final : public utils::Castable<AddBlockAttribute, Transform> {
   public:
     /// BlockAttribute is an InternalAttribute that is used to decorate a
     // structure that is used as a buffer in SPIR-V or GLSL.
-    class BlockAttribute final : public Castable<BlockAttribute, ast::InternalAttribute> {
+    class BlockAttribute final : public utils::Castable<BlockAttribute, ast::InternalAttribute> {
       public:
         /// Constructor
         /// @param program_id the identifier of the program that owns this node
diff --git a/src/tint/transform/add_empty_entry_point.h b/src/tint/transform/add_empty_entry_point.h
index 828f3b5..709b574 100644
--- a/src/tint/transform/add_empty_entry_point.h
+++ b/src/tint/transform/add_empty_entry_point.h
@@ -20,7 +20,7 @@
 namespace tint::transform {
 
 /// Add an empty entry point to the module, if no other entry points exist.
-class AddEmptyEntryPoint final : public Castable<AddEmptyEntryPoint, Transform> {
+class AddEmptyEntryPoint final : public utils::Castable<AddEmptyEntryPoint, Transform> {
   public:
     /// Constructor
     AddEmptyEntryPoint();
diff --git a/src/tint/transform/array_length_from_uniform.cc b/src/tint/transform/array_length_from_uniform.cc
index a396965..98d4a6b 100644
--- a/src/tint/transform/array_length_from_uniform.cc
+++ b/src/tint/transform/array_length_from_uniform.cc
@@ -65,9 +65,10 @@
     ApplyResult Run() {
         auto* cfg = inputs.Get<Config>();
         if (cfg == nullptr) {
-            b.Diagnostics().add_error(diag::System::Transform,
-                                      "missing transform data for " +
-                                          std::string(TypeInfo::Of<ArrayLengthFromUniform>().name));
+            b.Diagnostics().add_error(
+                diag::System::Transform,
+                "missing transform data for " +
+                    std::string(utils::TypeInfo::Of<ArrayLengthFromUniform>().name));
             return Program(std::move(b));
         }
 
@@ -82,13 +83,14 @@
 
         IterateArrayLengthOnStorageVar([&](const ast::CallExpression*, const sem::VariableUser*,
                                            const sem::GlobalVariable* var) {
-            auto binding = var->BindingPoint();
-            auto idx_itr = cfg->bindpoint_to_size_index.find(binding);
-            if (idx_itr == cfg->bindpoint_to_size_index.end()) {
-                return;
-            }
-            if (idx_itr->second > max_buffer_size_index) {
-                max_buffer_size_index = idx_itr->second;
+            if (auto binding = var->BindingPoint()) {
+                auto idx_itr = cfg->bindpoint_to_size_index.find(*binding);
+                if (idx_itr == cfg->bindpoint_to_size_index.end()) {
+                    return;
+                }
+                if (idx_itr->second > max_buffer_size_index) {
+                    max_buffer_size_index = idx_itr->second;
+                }
             }
         });
 
@@ -120,7 +122,10 @@
                                            const sem::VariableUser* storage_buffer_sem,
                                            const sem::GlobalVariable* var) {
             auto binding = var->BindingPoint();
-            auto idx_itr = cfg->bindpoint_to_size_index.find(binding);
+            if (!binding) {
+                return;
+            }
+            auto idx_itr = cfg->bindpoint_to_size_index.find(*binding);
             if (idx_itr == cfg->bindpoint_to_size_index.end()) {
                 return;
             }
diff --git a/src/tint/transform/array_length_from_uniform.h b/src/tint/transform/array_length_from_uniform.h
index 507ea37..bb50dc0 100644
--- a/src/tint/transform/array_length_from_uniform.h
+++ b/src/tint/transform/array_length_from_uniform.h
@@ -52,7 +52,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class ArrayLengthFromUniform final : public Castable<ArrayLengthFromUniform, Transform> {
+class ArrayLengthFromUniform final : public utils::Castable<ArrayLengthFromUniform, Transform> {
   public:
     /// Constructor
     ArrayLengthFromUniform();
@@ -60,7 +60,7 @@
     ~ArrayLengthFromUniform() override;
 
     /// Configuration options for the ArrayLengthFromUniform transform.
-    struct Config final : public Castable<Data, transform::Data> {
+    struct Config final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer.
         explicit Config(sem::BindingPoint ubo_bp);
@@ -85,7 +85,7 @@
     /// Information produced about what the transform did.
     /// If there were no calls to the arrayLength() builtin, then no Result will
     /// be emitted.
-    struct Result final : public Castable<Result, transform::Data> {
+    struct Result final : public utils::Castable<Result, transform::Data> {
         /// Constructor
         /// @param used_size_indices Indices into the UBO that are statically used.
         explicit Result(std::unordered_set<uint32_t> used_size_indices);
diff --git a/src/tint/transform/binding_remapper.cc b/src/tint/transform/binding_remapper.cc
index ecbd5cd..6b1888d 100644
--- a/src/tint/transform/binding_remapper.cc
+++ b/src/tint/transform/binding_remapper.cc
@@ -71,10 +71,8 @@
             auto* func = src->Sem().Get(func_ast);
             std::unordered_map<sem::BindingPoint, int> binding_point_counts;
             for (auto* global : func->TransitivelyReferencedGlobals()) {
-                if (global->Declaration()->HasBindingPoint()) {
-                    BindingPoint from = global->BindingPoint();
-
-                    auto bp_it = remappings->binding_points.find(from);
+                if (auto from = global->BindingPoint()) {
+                    auto bp_it = remappings->binding_points.find(*from);
                     if (bp_it != remappings->binding_points.end()) {
                         // Remapped
                         BindingPoint to = bp_it->second;
@@ -83,8 +81,8 @@
                         }
                     } else {
                         // No remapping
-                        if (binding_point_counts[from]++) {
-                            add_collision_attr.emplace(from);
+                        if (binding_point_counts[*from]++) {
+                            add_collision_attr.emplace(*from);
                         }
                     }
                 }
@@ -97,7 +95,7 @@
             auto* global_sem = src->Sem().Get<sem::GlobalVariable>(var);
 
             // The original binding point
-            BindingPoint from = global_sem->BindingPoint();
+            BindingPoint from = *global_sem->BindingPoint();
 
             // The binding point after remapping
             BindingPoint bp = from;
diff --git a/src/tint/transform/binding_remapper.h b/src/tint/transform/binding_remapper.h
index 8fdba2a..9eaedad 100644
--- a/src/tint/transform/binding_remapper.h
+++ b/src/tint/transform/binding_remapper.h
@@ -28,7 +28,7 @@
 
 /// BindingRemapper is a transform used to remap resource binding points and
 /// access controls.
-class BindingRemapper final : public Castable<BindingRemapper, Transform> {
+class BindingRemapper final : public utils::Castable<BindingRemapper, Transform> {
   public:
     /// BindingPoints is a map of old binding point to new binding point
     using BindingPoints = std::unordered_map<BindingPoint, BindingPoint>;
@@ -38,7 +38,7 @@
 
     /// Remappings is consumed by the BindingRemapper transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Remappings final : public Castable<Data, transform::Data> {
+    struct Remappings final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param bp a map of new binding points
         /// @param ac a map of new access controls
diff --git a/src/tint/transform/builtin_polyfill.cc b/src/tint/transform/builtin_polyfill.cc
index cc770ab..9cbb65f 100644
--- a/src/tint/transform/builtin_polyfill.cc
+++ b/src/tint/transform/builtin_polyfill.cc
@@ -1251,7 +1251,8 @@
                     auto* src_ty = conv->Source();
                     if (tint::Is<type::F32>(type::Type::ElementOf(src_ty))) {
                         auto* dst_ty = conv->Target();
-                        if (tint::IsAnyOf<type::I32, type::U32>(type::Type::ElementOf(dst_ty))) {
+                        if (tint::utils::IsAnyOf<type::I32, type::U32>(
+                                type::Type::ElementOf(dst_ty))) {
                             return f32_conv_polyfills.GetOrCreate(dst_ty, [&] {  //
                                 return ConvF32ToIU32(src_ty, dst_ty);
                             });
diff --git a/src/tint/transform/builtin_polyfill.h b/src/tint/transform/builtin_polyfill.h
index dee0bec..e42ba8f 100644
--- a/src/tint/transform/builtin_polyfill.h
+++ b/src/tint/transform/builtin_polyfill.h
@@ -20,7 +20,7 @@
 namespace tint::transform {
 
 /// Implements builtins for backends that do not have a native implementation.
-class BuiltinPolyfill final : public Castable<BuiltinPolyfill, Transform> {
+class BuiltinPolyfill final : public utils::Castable<BuiltinPolyfill, Transform> {
   public:
     /// Constructor
     BuiltinPolyfill();
@@ -89,7 +89,7 @@
 
     /// Config is consumed by the BuiltinPolyfill transform.
     /// Config specifies the builtins that should be polyfilled.
-    struct Config final : public Castable<Data, transform::Data> {
+    struct Config final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param b the list of builtins to polyfill
         explicit Config(const Builtins& b);
diff --git a/src/tint/transform/calculate_array_length.h b/src/tint/transform/calculate_array_length.h
index e5714a8..6ebb65c 100644
--- a/src/tint/transform/calculate_array_length.h
+++ b/src/tint/transform/calculate_array_length.h
@@ -32,11 +32,12 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class CalculateArrayLength final : public Castable<CalculateArrayLength, Transform> {
+class CalculateArrayLength final : public utils::Castable<CalculateArrayLength, Transform> {
   public:
     /// BufferSizeIntrinsic is an InternalAttribute that's applied to intrinsic
     /// functions used to obtain the runtime size of a storage buffer.
-    class BufferSizeIntrinsic final : public Castable<BufferSizeIntrinsic, ast::InternalAttribute> {
+    class BufferSizeIntrinsic final
+        : public utils::Castable<BufferSizeIntrinsic, ast::InternalAttribute> {
       public:
         /// Constructor
         /// @param program_id the identifier of the program that owns this node
diff --git a/src/tint/transform/canonicalize_entry_point_io.h b/src/tint/transform/canonicalize_entry_point_io.h
index fbfed5e..85e9b22 100644
--- a/src/tint/transform/canonicalize_entry_point_io.h
+++ b/src/tint/transform/canonicalize_entry_point_io.h
@@ -82,7 +82,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * Unshadow
-class CanonicalizeEntryPointIO final : public Castable<CanonicalizeEntryPointIO, Transform> {
+class CanonicalizeEntryPointIO final : public utils::Castable<CanonicalizeEntryPointIO, Transform> {
   public:
     /// ShaderStyle is an enumerator of different ways to emit shader IO.
     enum class ShaderStyle {
@@ -97,7 +97,7 @@
     };
 
     /// Configuration options for the transform.
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         /// @param style the approach to use for emitting shader IO.
         /// @param sample_mask an optional sample mask to combine with shader masks
diff --git a/src/tint/transform/clamp_frag_depth.h b/src/tint/transform/clamp_frag_depth.h
index 3e3f168..88a7ee1 100644
--- a/src/tint/transform/clamp_frag_depth.h
+++ b/src/tint/transform/clamp_frag_depth.h
@@ -54,7 +54,7 @@
 ///     return clamp_frag_depth(0.0);
 ///   }
 /// ```
-class ClampFragDepth final : public Castable<ClampFragDepth, Transform> {
+class ClampFragDepth final : public utils::Castable<ClampFragDepth, Transform> {
   public:
     /// Constructor
     ClampFragDepth();
diff --git a/src/tint/transform/combine_samplers.cc b/src/tint/transform/combine_samplers.cc
index aa1a686..ebf8126 100644
--- a/src/tint/transform/combine_samplers.cc
+++ b/src/tint/transform/combine_samplers.cc
@@ -107,10 +107,10 @@
                                               const sem::Variable* sampler_var,
                                               std::string name) {
         SamplerTexturePair bp_pair;
-        bp_pair.texture_binding_point = texture_var->As<sem::GlobalVariable>()->BindingPoint();
-        bp_pair.sampler_binding_point = sampler_var
-                                            ? sampler_var->As<sem::GlobalVariable>()->BindingPoint()
-                                            : binding_info->placeholder_binding_point;
+        bp_pair.texture_binding_point = *texture_var->As<sem::GlobalVariable>()->BindingPoint();
+        bp_pair.sampler_binding_point =
+            sampler_var ? *sampler_var->As<sem::GlobalVariable>()->BindingPoint()
+                        : binding_info->placeholder_binding_point;
         auto it = binding_info->binding_map.find(bp_pair);
         if (it != binding_info->binding_map.end()) {
             name = it->second;
@@ -158,12 +158,11 @@
         for (auto* global : ctx.src->AST().GlobalVariables()) {
             auto* global_sem = sem.Get(global)->As<sem::GlobalVariable>();
             auto* type = ctx.src->TypeOf(global->type);
-            if (tint::IsAnyOf<type::Texture, type::Sampler>(type) &&
+            if (tint::utils::IsAnyOf<type::Texture, type::Sampler>(type) &&
                 !type->Is<type::StorageTexture>()) {
                 ctx.Remove(ctx.src->AST().GlobalDeclarations(), global);
-            } else if (global->HasBindingPoint()) {
-                auto binding_point = global_sem->BindingPoint();
-                if (binding_point.group == 0 && binding_point.binding == 0) {
+            } else if (auto binding_point = global_sem->BindingPoint()) {
+                if (binding_point->group == 0 && binding_point->binding == 0) {
                     auto* attribute =
                         ctx.dst->Disable(ast::DisabledValidation::kBindingPointCollision);
                     ctx.InsertFront(global->attributes, attribute);
diff --git a/src/tint/transform/combine_samplers.h b/src/tint/transform/combine_samplers.h
index 6834abe..b521a55 100644
--- a/src/tint/transform/combine_samplers.h
+++ b/src/tint/transform/combine_samplers.h
@@ -52,7 +52,7 @@
 /// information needed to represent a combined sampler in GLSL
 /// (dimensionality, component type, etc). The GLSL writer outputs such
 /// (Tint) Textures as (GLSL) Samplers.
-class CombineSamplers final : public Castable<CombineSamplers, Transform> {
+class CombineSamplers final : public utils::Castable<CombineSamplers, Transform> {
   public:
     /// A pair of binding points.
     using SamplerTexturePair = sem::SamplerTexturePair;
@@ -62,7 +62,7 @@
 
     /// The client-provided mapping from separate texture and sampler binding
     /// points to combined sampler binding point.
-    struct BindingInfo final : public Castable<Data, transform::Data> {
+    struct BindingInfo final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param map the map of all (texture, sampler) -> (combined) pairs
         /// @param placeholder the binding point to use for placeholder samplers.
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index fb8dc62..564451c 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -62,7 +62,7 @@
 
 /// Offset is a simple ast::Expression builder interface, used to build byte
 /// offsets for storage and uniform buffer accesses.
-struct Offset : Castable<Offset> {
+struct Offset : utils::Castable<Offset> {
     /// @returns builds and returns the ast::Expression in `ctx.dst`
     virtual const ast::Expression* Build(CloneContext& ctx) const = 0;
 };
@@ -86,7 +86,7 @@
 
 /// OffsetLiteral is an implementation of Offset that constructs a u32 literal
 /// value.
-struct OffsetLiteral final : Castable<OffsetLiteral, Offset> {
+struct OffsetLiteral final : utils::Castable<OffsetLiteral, Offset> {
     uint32_t const literal = 0;
 
     explicit OffsetLiteral(uint32_t lit) : literal(lit) {}
diff --git a/src/tint/transform/decompose_memory_access.h b/src/tint/transform/decompose_memory_access.h
index f85ad6d..8f06726 100644
--- a/src/tint/transform/decompose_memory_access.h
+++ b/src/tint/transform/decompose_memory_access.h
@@ -29,13 +29,13 @@
 
 /// DecomposeMemoryAccess is a transform used to replace storage and uniform buffer accesses with a
 /// combination of load, store or atomic functions on primitive types.
-class DecomposeMemoryAccess final : public Castable<DecomposeMemoryAccess, Transform> {
+class DecomposeMemoryAccess final : public utils::Castable<DecomposeMemoryAccess, Transform> {
   public:
     /// Intrinsic is an InternalAttribute that's used to decorate a stub function so that the HLSL
     /// transforms this into calls to
     /// `[RW]ByteAddressBuffer.Load[N]()` or `[RW]ByteAddressBuffer.Store[N]()`,
     /// with a possible cast.
-    class Intrinsic final : public Castable<Intrinsic, ast::InternalAttribute> {
+    class Intrinsic final : public utils::Castable<Intrinsic, ast::InternalAttribute> {
       public:
         /// Intrinsic op
         enum class Op {
diff --git a/src/tint/transform/decompose_strided_array.h b/src/tint/transform/decompose_strided_array.h
index 9555a9a..aee75b0 100644
--- a/src/tint/transform/decompose_strided_array.h
+++ b/src/tint/transform/decompose_strided_array.h
@@ -27,7 +27,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class DecomposeStridedArray final : public Castable<DecomposeStridedArray, Transform> {
+class DecomposeStridedArray final : public utils::Castable<DecomposeStridedArray, Transform> {
   public:
     /// Constructor
     DecomposeStridedArray();
diff --git a/src/tint/transform/decompose_strided_matrix.h b/src/tint/transform/decompose_strided_matrix.h
index 947dfc6..8fa0feb 100644
--- a/src/tint/transform/decompose_strided_matrix.h
+++ b/src/tint/transform/decompose_strided_matrix.h
@@ -27,7 +27,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class DecomposeStridedMatrix final : public Castable<DecomposeStridedMatrix, Transform> {
+class DecomposeStridedMatrix final : public utils::Castable<DecomposeStridedMatrix, Transform> {
   public:
     /// Constructor
     DecomposeStridedMatrix();
diff --git a/src/tint/transform/demote_to_helper.h b/src/tint/transform/demote_to_helper.h
index ba87feb..75909d2 100644
--- a/src/tint/transform/demote_to_helper.h
+++ b/src/tint/transform/demote_to_helper.h
@@ -29,7 +29,7 @@
 /// @note Depends on the following transforms to have been run first:
 /// * PromoteSideEffectsToDecl
 /// * ExpandCompoundAssignment
-class DemoteToHelper final : public Castable<DemoteToHelper, Transform> {
+class DemoteToHelper final : public utils::Castable<DemoteToHelper, Transform> {
   public:
     /// Constructor
     DemoteToHelper();
diff --git a/src/tint/transform/direct_variable_access.h b/src/tint/transform/direct_variable_access.h
index 34b6e91..74aafa7 100644
--- a/src/tint/transform/direct_variable_access.h
+++ b/src/tint/transform/direct_variable_access.h
@@ -32,7 +32,7 @@
 /// comments in src/tint/transform/direct_variable_access.cc.
 ///
 /// @note DirectVariableAccess requires the transform::Unshadow transform to have been run first.
-class DirectVariableAccess final : public Castable<DirectVariableAccess, Transform> {
+class DirectVariableAccess final : public utils::Castable<DirectVariableAccess, Transform> {
   public:
     /// Constructor
     DirectVariableAccess();
@@ -49,7 +49,7 @@
 
     /// Config is consumed by the DirectVariableAccess transform.
     /// Config specifies the behavior of the transform.
-    struct Config final : public Castable<Data, transform::Data> {
+    struct Config final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param options behavior of the transform
         explicit Config(const Options& options);
diff --git a/src/tint/transform/disable_uniformity_analysis.h b/src/tint/transform/disable_uniformity_analysis.h
index a9922af..a2827c5 100644
--- a/src/tint/transform/disable_uniformity_analysis.h
+++ b/src/tint/transform/disable_uniformity_analysis.h
@@ -20,7 +20,8 @@
 namespace tint::transform {
 
 /// Disable uniformity analysis for the program.
-class DisableUniformityAnalysis final : public Castable<DisableUniformityAnalysis, Transform> {
+class DisableUniformityAnalysis final
+    : public utils::Castable<DisableUniformityAnalysis, Transform> {
   public:
     /// Constructor
     DisableUniformityAnalysis();
diff --git a/src/tint/transform/expand_compound_assignment.h b/src/tint/transform/expand_compound_assignment.h
index 6b299c5..ce5a36c 100644
--- a/src/tint/transform/expand_compound_assignment.h
+++ b/src/tint/transform/expand_compound_assignment.h
@@ -38,7 +38,7 @@
 ///
 /// This transform also handles increment and decrement statements in the same
 /// manner, by replacing `i++` with `i = i + 1`.
-class ExpandCompoundAssignment final : public Castable<ExpandCompoundAssignment, Transform> {
+class ExpandCompoundAssignment final : public utils::Castable<ExpandCompoundAssignment, Transform> {
   public:
     /// Constructor
     ExpandCompoundAssignment();
diff --git a/src/tint/transform/first_index_offset.h b/src/tint/transform/first_index_offset.h
index f84d811..dbf47ae 100644
--- a/src/tint/transform/first_index_offset.h
+++ b/src/tint/transform/first_index_offset.h
@@ -57,12 +57,12 @@
 ///   }
 /// ```
 ///
-class FirstIndexOffset final : public Castable<FirstIndexOffset, Transform> {
+class FirstIndexOffset final : public utils::Castable<FirstIndexOffset, Transform> {
   public:
     /// BindingPoint is consumed by the FirstIndexOffset transform.
     /// BindingPoint specifies the binding point of the first index uniform
     /// buffer.
-    struct BindingPoint final : public Castable<BindingPoint, transform::Data> {
+    struct BindingPoint final : public utils::Castable<BindingPoint, transform::Data> {
         /// Constructor
         BindingPoint();
 
@@ -82,7 +82,7 @@
 
     /// Data is outputted by the FirstIndexOffset transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Data final : public Castable<Data, transform::Data> {
+    struct Data final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param has_vtx_or_inst_index True if the shader uses vertex_index or
         /// instance_index
diff --git a/src/tint/transform/for_loop_to_loop.h b/src/tint/transform/for_loop_to_loop.h
index fe3db97..8ed53c5 100644
--- a/src/tint/transform/for_loop_to_loop.h
+++ b/src/tint/transform/for_loop_to_loop.h
@@ -21,7 +21,7 @@
 
 /// ForLoopToLoop is a Transform that converts a for-loop statement into a loop
 /// statement. This is required by the SPIR-V writer.
-class ForLoopToLoop final : public Castable<ForLoopToLoop, Transform> {
+class ForLoopToLoop final : public utils::Castable<ForLoopToLoop, Transform> {
   public:
     /// Constructor
     ForLoopToLoop();
diff --git a/src/tint/transform/localize_struct_array_assignment.h b/src/tint/transform/localize_struct_array_assignment.h
index 169e33c..8173fa8 100644
--- a/src/tint/transform/localize_struct_array_assignment.h
+++ b/src/tint/transform/localize_struct_array_assignment.h
@@ -28,7 +28,7 @@
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
 class LocalizeStructArrayAssignment final
-    : public Castable<LocalizeStructArrayAssignment, Transform> {
+    : public utils::Castable<LocalizeStructArrayAssignment, Transform> {
   public:
     /// Constructor
     LocalizeStructArrayAssignment();
diff --git a/src/tint/transform/manager.h b/src/tint/transform/manager.h
index 64ca847..a364c1a 100644
--- a/src/tint/transform/manager.h
+++ b/src/tint/transform/manager.h
@@ -27,7 +27,7 @@
 /// The inner transforms will execute in the appended order.
 /// If any inner transform fails the manager will return immediately and
 /// the error can be retrieved with the Output's diagnostics.
-class Manager final : public Castable<Manager, Transform> {
+class Manager final : public utils::Castable<Manager, Transform> {
   public:
     /// Constructor
     Manager();
diff --git a/src/tint/transform/merge_return.h b/src/tint/transform/merge_return.h
index f6db5c2..fa9654b 100644
--- a/src/tint/transform/merge_return.h
+++ b/src/tint/transform/merge_return.h
@@ -20,7 +20,7 @@
 namespace tint::transform {
 
 /// Merge return statements into a single return at the end of the function.
-class MergeReturn final : public Castable<MergeReturn, Transform> {
+class MergeReturn final : public utils::Castable<MergeReturn, Transform> {
   public:
     /// Constructor
     MergeReturn();
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.h b/src/tint/transform/module_scope_var_to_entry_point_param.h
index 377151f..4f4f562 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param.h
+++ b/src/tint/transform/module_scope_var_to_entry_point_param.h
@@ -62,7 +62,7 @@
 /// }
 /// ```
 class ModuleScopeVarToEntryPointParam final
-    : public Castable<ModuleScopeVarToEntryPointParam, Transform> {
+    : public utils::Castable<ModuleScopeVarToEntryPointParam, Transform> {
   public:
     /// Constructor
     ModuleScopeVarToEntryPointParam();
diff --git a/src/tint/transform/multiplanar_external_texture.cc b/src/tint/transform/multiplanar_external_texture.cc
index dcf1e1c..2105781 100644
--- a/src/tint/transform/multiplanar_external_texture.cc
+++ b/src/tint/transform/multiplanar_external_texture.cc
@@ -115,7 +115,7 @@
             // The binding points for the newly introduced bindings must have been provided to this
             // transform. We fetch the new binding points by providing the original texture_external
             // binding points into the passed map.
-            sem::BindingPoint bp = sem_var->BindingPoint();
+            sem::BindingPoint bp = *sem_var->BindingPoint();
 
             BindingsMap::const_iterator it = new_binding_points->bindings_map.find(bp);
             if (it == new_binding_points->bindings_map.end()) {
diff --git a/src/tint/transform/multiplanar_external_texture.h b/src/tint/transform/multiplanar_external_texture.h
index 656d0ad..c47c345 100644
--- a/src/tint/transform/multiplanar_external_texture.h
+++ b/src/tint/transform/multiplanar_external_texture.h
@@ -37,7 +37,8 @@
 /// decoding, gamut conversion, and gamma encoding steps. Specifically
 // for BT.709 to SRGB conversion, it takes the fast path only doing the yuv->rgb
 // step and skipping all other steps.
-class MultiplanarExternalTexture final : public Castable<MultiplanarExternalTexture, Transform> {
+class MultiplanarExternalTexture final
+    : public utils::Castable<MultiplanarExternalTexture, Transform> {
   public:
     /// This struct identifies the binding groups and locations for new bindings to
     /// use when transforming a texture_external instance.
@@ -51,7 +52,7 @@
     /// NewBindingPoints is consumed by the MultiplanarExternalTexture transform.
     /// Data holds information about location of each texture_external binding and
     /// which binding slots it should expand into.
-    struct NewBindingPoints final : public Castable<Data, transform::Data> {
+    struct NewBindingPoints final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param bm a map to the new binding slots to use.
         explicit NewBindingPoints(BindingsMap bm);
diff --git a/src/tint/transform/num_workgroups_from_uniform.cc b/src/tint/transform/num_workgroups_from_uniform.cc
index e4bb05e..18889f4 100644
--- a/src/tint/transform/num_workgroups_from_uniform.cc
+++ b/src/tint/transform/num_workgroups_from_uniform.cc
@@ -148,11 +148,10 @@
                 group = 0;
 
                 for (auto* global : src->AST().GlobalVariables()) {
-                    if (global->HasBindingPoint()) {
-                        auto* global_sem = src->Sem().Get<sem::GlobalVariable>(global);
-                        auto binding_point = global_sem->BindingPoint();
-                        if (binding_point.group >= group) {
-                            group = binding_point.group + 1;
+                    auto* global_sem = src->Sem().Get<sem::GlobalVariable>(global);
+                    if (auto bp = global_sem->BindingPoint()) {
+                        if (bp->group >= group) {
+                            group = bp->group + 1;
                         }
                     }
                 }
diff --git a/src/tint/transform/num_workgroups_from_uniform.h b/src/tint/transform/num_workgroups_from_uniform.h
index 25308f2..049c1db 100644
--- a/src/tint/transform/num_workgroups_from_uniform.h
+++ b/src/tint/transform/num_workgroups_from_uniform.h
@@ -44,7 +44,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * CanonicalizeEntryPointIO
-class NumWorkgroupsFromUniform final : public Castable<NumWorkgroupsFromUniform, Transform> {
+class NumWorkgroupsFromUniform final : public utils::Castable<NumWorkgroupsFromUniform, Transform> {
   public:
     /// Constructor
     NumWorkgroupsFromUniform();
@@ -52,7 +52,7 @@
     ~NumWorkgroupsFromUniform() override;
 
     /// Configuration options for the NumWorkgroupsFromUniform transform.
-    struct Config final : public Castable<Data, transform::Data> {
+    struct Config final : public utils::Castable<Data, transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer. If ubo_bp
         /// contains no value, a free binding point will be used to ensure the generated program is
diff --git a/src/tint/transform/packed_vec3.h b/src/tint/transform/packed_vec3.h
index 89e8286..d13111b 100644
--- a/src/tint/transform/packed_vec3.h
+++ b/src/tint/transform/packed_vec3.h
@@ -38,7 +38,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * ExpandCompoundAssignment
-class PackedVec3 final : public Castable<PackedVec3, Transform> {
+class PackedVec3 final : public utils::Castable<PackedVec3, Transform> {
   public:
     /// Constructor
     PackedVec3();
diff --git a/src/tint/transform/pad_structs.h b/src/tint/transform/pad_structs.h
index 1add1d6..f5bf8cc 100644
--- a/src/tint/transform/pad_structs.h
+++ b/src/tint/transform/pad_structs.h
@@ -24,7 +24,7 @@
 /// the offset= decoration.
 ///
 /// @note This transform requires the CanonicalizeEntryPointIO transform to have been run first.
-class PadStructs final : public Castable<PadStructs, Transform> {
+class PadStructs final : public utils::Castable<PadStructs, Transform> {
   public:
     /// Constructor
     PadStructs();
diff --git a/src/tint/transform/preserve_padding.h b/src/tint/transform/preserve_padding.h
index 3bf0a35..e025c2e 100644
--- a/src/tint/transform/preserve_padding.h
+++ b/src/tint/transform/preserve_padding.h
@@ -26,7 +26,7 @@
 /// assignments into element-wise assignments via helper functions.
 ///
 /// @note Assumes that the DirectVariableTransform will be run afterwards for backends that need it.
-class PreservePadding final : public Castable<PreservePadding, Transform> {
+class PreservePadding final : public utils::Castable<PreservePadding, Transform> {
   public:
     /// Constructor
     PreservePadding();
diff --git a/src/tint/transform/promote_initializers_to_let.h b/src/tint/transform/promote_initializers_to_let.h
index b1bb291..a08ad36 100644
--- a/src/tint/transform/promote_initializers_to_let.h
+++ b/src/tint/transform/promote_initializers_to_let.h
@@ -25,7 +25,7 @@
 /// array or structure. For example, the following is not immediately expressable for HLSL:
 ///   `array<i32, 2>(1, 2)[0]`
 /// @see crbug.com/tint/406
-class PromoteInitializersToLet final : public Castable<PromoteInitializersToLet, Transform> {
+class PromoteInitializersToLet final : public utils::Castable<PromoteInitializersToLet, Transform> {
   public:
     /// Constructor
     PromoteInitializersToLet();
diff --git a/src/tint/transform/promote_side_effects_to_decl.cc b/src/tint/transform/promote_side_effects_to_decl.cc
index 9db7e38..fef15ee 100644
--- a/src/tint/transform/promote_side_effects_to_decl.cc
+++ b/src/tint/transform/promote_side_effects_to_decl.cc
@@ -52,7 +52,7 @@
 // This first transform converts side-effecting for-loops to loops and else-ifs
 // to else {if}s so that the next transform, DecomposeSideEffects, can insert
 // hoisted expressions above their current location.
-struct SimplifySideEffectStatements : Castable<PromoteSideEffectsToDecl, Transform> {
+struct SimplifySideEffectStatements : tint::utils::Castable<PromoteSideEffectsToDecl, Transform> {
     ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override;
 };
 
@@ -87,7 +87,7 @@
 // Decomposes side-effecting expressions to ensure order of evaluation. This
 // handles both breaking down logical binary expressions for short-circuit
 // evaluation, as well as hoisting expressions to ensure order of evaluation.
-struct DecomposeSideEffects : Castable<PromoteSideEffectsToDecl, Transform> {
+struct DecomposeSideEffects : tint::utils::Castable<PromoteSideEffectsToDecl, Transform> {
     class CollectHoistsState;
     class DecomposeState;
     ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override;
diff --git a/src/tint/transform/promote_side_effects_to_decl.h b/src/tint/transform/promote_side_effects_to_decl.h
index 99e80c6..36426b6 100644
--- a/src/tint/transform/promote_side_effects_to_decl.h
+++ b/src/tint/transform/promote_side_effects_to_decl.h
@@ -23,7 +23,7 @@
 /// declarations before the statement of usage with the goal of ensuring
 /// left-to-right order of evaluation, while respecting short-circuit
 /// evaluation.
-class PromoteSideEffectsToDecl final : public Castable<PromoteSideEffectsToDecl, Transform> {
+class PromoteSideEffectsToDecl final : public utils::Castable<PromoteSideEffectsToDecl, Transform> {
   public:
     /// Constructor
     PromoteSideEffectsToDecl();
diff --git a/src/tint/transform/remove_continue_in_switch.h b/src/tint/transform/remove_continue_in_switch.h
index 1070906..a888513 100644
--- a/src/tint/transform/remove_continue_in_switch.h
+++ b/src/tint/transform/remove_continue_in_switch.h
@@ -23,7 +23,7 @@
 /// bool variable, and checking if the variable is set after the switch to
 /// continue. It is necessary to work around FXC "error X3708: continue cannot
 /// be used in a switch". See crbug.com/tint/1080.
-class RemoveContinueInSwitch final : public Castable<RemoveContinueInSwitch, Transform> {
+class RemoveContinueInSwitch final : public utils::Castable<RemoveContinueInSwitch, Transform> {
   public:
     /// Constructor
     RemoveContinueInSwitch();
diff --git a/src/tint/transform/remove_phonies.h b/src/tint/transform/remove_phonies.h
index 99a049e..7b1f9fd 100644
--- a/src/tint/transform/remove_phonies.h
+++ b/src/tint/transform/remove_phonies.h
@@ -25,7 +25,7 @@
 /// RemovePhonies is a Transform that removes all phony-assignment statements,
 /// while preserving function call expressions in the RHS of the assignment that
 /// may have side-effects. It also removes calls to builtins that return a constant value.
-class RemovePhonies final : public Castable<RemovePhonies, Transform> {
+class RemovePhonies final : public utils::Castable<RemovePhonies, Transform> {
   public:
     /// Constructor
     RemovePhonies();
diff --git a/src/tint/transform/remove_unreachable_statements.h b/src/tint/transform/remove_unreachable_statements.h
index f5848f5..dd1054d 100644
--- a/src/tint/transform/remove_unreachable_statements.h
+++ b/src/tint/transform/remove_unreachable_statements.h
@@ -24,7 +24,8 @@
 
 /// RemoveUnreachableStatements is a Transform that removes all statements
 /// marked as unreachable.
-class RemoveUnreachableStatements final : public Castable<RemoveUnreachableStatements, Transform> {
+class RemoveUnreachableStatements final
+    : public utils::Castable<RemoveUnreachableStatements, Transform> {
   public:
     /// Constructor
     RemoveUnreachableStatements();
diff --git a/src/tint/transform/renamer.cc b/src/tint/transform/renamer.cc
index af26109..bd74cf6 100644
--- a/src/tint/transform/renamer.cc
+++ b/src/tint/transform/renamer.cc
@@ -25,7 +25,7 @@
 #include "src/tint/sem/value_constructor.h"
 #include "src/tint/sem/value_conversion.h"
 #include "src/tint/switch.h"
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::Renamer);
 TINT_INSTANTIATE_TYPEINFO(tint::transform::Renamer::Data);
@@ -1333,7 +1333,7 @@
             return true;
         }
         auto name = symbol.Name();
-        if (!text::utf8::IsASCII(name)) {
+        if (!utils::utf8::IsASCII(name)) {
             // name is non-ascii. All of the backend keywords are ascii, so rename if we're not
             // preserving unicode symbols.
             return !preserve_unicode;
diff --git a/src/tint/transform/renamer.h b/src/tint/transform/renamer.h
index 8a9f97e..015e386 100644
--- a/src/tint/transform/renamer.h
+++ b/src/tint/transform/renamer.h
@@ -23,11 +23,11 @@
 namespace tint::transform {
 
 /// Renamer is a Transform that renames all the symbols in a program.
-class Renamer final : public Castable<Renamer, Transform> {
+class Renamer final : public utils::Castable<Renamer, Transform> {
   public:
     /// Data is outputted by the Renamer transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Data final : public Castable<Data, transform::Data> {
+    struct Data final : public utils::Castable<Data, transform::Data> {
         /// Remappings is a map of old symbol name to new symbol name
         using Remappings = std::unordered_map<std::string, std::string>;
 
@@ -59,7 +59,7 @@
 
     /// Optional configuration options for the transform.
     /// If omitted, then the renamer will use Target::kAll.
-    struct Config final : public Castable<Config, transform::Data> {
+    struct Config final : public utils::Castable<Config, transform::Data> {
         /// Constructor
         /// @param tgt the targets to rename
         /// @param keep_unicode if false, symbols with non-ascii code-points are
diff --git a/src/tint/transform/robustness.h b/src/tint/transform/robustness.h
index 596db4d..e916b16 100644
--- a/src/tint/transform/robustness.h
+++ b/src/tint/transform/robustness.h
@@ -29,7 +29,7 @@
 ///       * BuiltinPolyfill as 'clamp' and binary operators may need to be polyfilled.
 ///       * CanonicalizeEntryPointIO as the transform does not support the 'in' and 'out' address
 ///         spaces.
-class Robustness final : public Castable<Robustness, Transform> {
+class Robustness final : public utils::Castable<Robustness, Transform> {
   public:
     /// Robustness action for out-of-bounds indexing.
     enum class Action {
@@ -45,7 +45,7 @@
     };
 
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/simplify_pointers.h b/src/tint/transform/simplify_pointers.h
index 6e040bb..8783d43 100644
--- a/src/tint/transform/simplify_pointers.h
+++ b/src/tint/transform/simplify_pointers.h
@@ -31,7 +31,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * Unshadow
-class SimplifyPointers final : public Castable<SimplifyPointers, Transform> {
+class SimplifyPointers final : public utils::Castable<SimplifyPointers, Transform> {
   public:
     /// Constructor
     SimplifyPointers();
diff --git a/src/tint/transform/single_entry_point.h b/src/tint/transform/single_entry_point.h
index 7aba5e8..b91856b 100644
--- a/src/tint/transform/single_entry_point.h
+++ b/src/tint/transform/single_entry_point.h
@@ -25,10 +25,10 @@
 ///
 /// All module-scope variables, types, and functions that are not used by the
 /// target entry point will also be removed.
-class SingleEntryPoint final : public Castable<SingleEntryPoint, Transform> {
+class SingleEntryPoint final : public utils::Castable<SingleEntryPoint, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         /// @param entry_point the name of the entry point to keep
         explicit Config(std::string entry_point = "");
diff --git a/src/tint/transform/spirv_atomic.h b/src/tint/transform/spirv_atomic.h
index b524200..907996b 100644
--- a/src/tint/transform/spirv_atomic.h
+++ b/src/tint/transform/spirv_atomic.h
@@ -32,7 +32,7 @@
 /// with calls to the WGSL atomic builtin. It also makes sure to replace variable declarations that
 /// are the target of the atomic operations with an atomic declaration of the same type. For
 /// structs, it creates a copy of the original struct with atomic members.
-class SpirvAtomic final : public Castable<SpirvAtomic, Transform> {
+class SpirvAtomic final : public utils::Castable<SpirvAtomic, Transform> {
   public:
     /// Constructor
     SpirvAtomic();
@@ -41,7 +41,7 @@
 
     /// Stub is an attribute applied to stub SPIR-V reader generated functions that need to be
     /// translated to an atomic builtin.
-    class Stub final : public Castable<Stub, ast::InternalAttribute> {
+    class Stub final : public utils::Castable<Stub, ast::InternalAttribute> {
       public:
         /// @param pid the identifier of the program that owns this node
         /// @param nid the unique node identifier
diff --git a/src/tint/transform/std140.h b/src/tint/transform/std140.h
index 769932f..ab1b31e 100644
--- a/src/tint/transform/std140.h
+++ b/src/tint/transform/std140.h
@@ -28,7 +28,7 @@
 /// sufficient to have any WGSL structure be std140-layout conformant.
 ///
 /// @note This transform requires the PromoteSideEffectsToDecl transform to have been run first.
-class Std140 final : public Castable<Std140, Transform> {
+class Std140 final : public utils::Castable<Std140, Transform> {
   public:
     /// Constructor
     Std140();
diff --git a/src/tint/transform/substitute_override.h b/src/tint/transform/substitute_override.h
index 853acc7..10200ca 100644
--- a/src/tint/transform/substitute_override.h
+++ b/src/tint/transform/substitute_override.h
@@ -43,10 +43,10 @@
 /// ```
 ///
 /// @see crbug.com/tint/1582
-class SubstituteOverride final : public Castable<SubstituteOverride, Transform> {
+class SubstituteOverride final : public utils::Castable<SubstituteOverride, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/texture_1d_to_2d.h b/src/tint/transform/texture_1d_to_2d.h
index 9999821..c742e2a 100644
--- a/src/tint/transform/texture_1d_to_2d.h
+++ b/src/tint/transform/texture_1d_to_2d.h
@@ -21,7 +21,7 @@
 
 /// This transform converts all 1D texture types and accesses to 2D.
 /// This is required for GLSL ES, which does not support 1D textures.
-class Texture1DTo2D final : public Castable<Texture1DTo2D, Transform> {
+class Texture1DTo2D final : public utils::Castable<Texture1DTo2D, Transform> {
   public:
     /// Constructor
     Texture1DTo2D();
diff --git a/src/tint/transform/transform.h b/src/tint/transform/transform.h
index c9c1eee..b498e82 100644
--- a/src/tint/transform/transform.h
+++ b/src/tint/transform/transform.h
@@ -19,14 +19,14 @@
 #include <unordered_map>
 #include <utility>
 
-#include "src/tint/castable.h"
 #include "src/tint/program.h"
+#include "src/tint/utils/castable.h"
 
 namespace tint::transform {
 
 /// Data is the base class for transforms that accept extra input or emit extra
 /// output information along with a Program.
-class Data : public Castable<Data> {
+class Data : public utils::Castable<Data> {
   public:
     /// Constructor
     Data();
@@ -72,7 +72,7 @@
     template <typename T>
     void Put(std::unique_ptr<T>&& data) {
         static_assert(std::is_base_of<Data, T>::value, "T does not derive from Data");
-        map_[&TypeInfo::Of<T>()] = std::move(data);
+        map_[&utils::TypeInfo::Of<T>()] = std::move(data);
     }
 
     /// Creates the data of type `T` with the provided arguments and adds it into
@@ -94,7 +94,7 @@
     /// Put()
     template <typename T>
     T* Get() {
-        auto it = map_.find(&TypeInfo::Of<T>());
+        auto it = map_.find(&utils::TypeInfo::Of<T>());
         if (it == map_.end()) {
             return nullptr;
         }
@@ -122,7 +122,7 @@
         PutAll(std::forward<Tn>(remainder)...);
     }
 
-    std::unordered_map<const TypeInfo*, std::unique_ptr<Data>> map_;
+    std::unordered_map<const utils::TypeInfo*, std::unique_ptr<Data>> map_;
 };
 
 /// The return type of Run()
@@ -151,7 +151,7 @@
 };
 
 /// Interface for Program transforms
-class Transform : public Castable<Transform> {
+class Transform : public utils::Castable<Transform> {
   public:
     /// Constructor
     Transform();
diff --git a/src/tint/transform/truncate_interstage_variables.cc b/src/tint/transform/truncate_interstage_variables.cc
index b4ae24e..33c5cf7 100644
--- a/src/tint/transform/truncate_interstage_variables.cc
+++ b/src/tint/transform/truncate_interstage_variables.cc
@@ -24,7 +24,7 @@
 #include "src/tint/sem/member_accessor_expression.h"
 #include "src/tint/sem/statement.h"
 #include "src/tint/sem/variable.h"
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::TruncateInterstageVariables);
 TINT_INSTANTIATE_TYPEINFO(tint::transform::TruncateInterstageVariables::Config);
@@ -57,7 +57,7 @@
         b.Diagnostics().add_error(
             diag::System::Transform,
             "missing transform data for " +
-                std::string(TypeInfo::Of<TruncateInterstageVariables>().name));
+                std::string(utils::TypeInfo::Of<TruncateInterstageVariables>().name));
         return Program(std::move(b));
     }
 
diff --git a/src/tint/transform/truncate_interstage_variables.h b/src/tint/transform/truncate_interstage_variables.h
index bed226c..f279eb3 100644
--- a/src/tint/transform/truncate_interstage_variables.h
+++ b/src/tint/transform/truncate_interstage_variables.h
@@ -88,10 +88,11 @@
 ///  }
 /// ```
 ///
-class TruncateInterstageVariables final : public Castable<TruncateInterstageVariables, Transform> {
+class TruncateInterstageVariables final
+    : public utils::Castable<TruncateInterstageVariables, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/unshadow.h b/src/tint/transform/unshadow.h
index 9030006..313e556 100644
--- a/src/tint/transform/unshadow.h
+++ b/src/tint/transform/unshadow.h
@@ -20,7 +20,7 @@
 namespace tint::transform {
 
 /// Unshadow is a Transform that renames any variables that shadow another variable.
-class Unshadow final : public Castable<Unshadow, Transform> {
+class Unshadow final : public utils::Castable<Unshadow, Transform> {
   public:
     /// Constructor
     Unshadow();
diff --git a/src/tint/transform/vectorize_matrix_conversions.h b/src/tint/transform/vectorize_matrix_conversions.h
index c86240c..2308eb1 100644
--- a/src/tint/transform/vectorize_matrix_conversions.h
+++ b/src/tint/transform/vectorize_matrix_conversions.h
@@ -20,7 +20,8 @@
 namespace tint::transform {
 
 /// A transform that converts matrix conversions (between f32 and f16 matrices) to the vector form.
-class VectorizeMatrixConversions final : public Castable<VectorizeMatrixConversions, Transform> {
+class VectorizeMatrixConversions final
+    : public utils::Castable<VectorizeMatrixConversions, Transform> {
   public:
     /// Constructor
     VectorizeMatrixConversions();
diff --git a/src/tint/transform/vectorize_scalar_matrix_initializers.h b/src/tint/transform/vectorize_scalar_matrix_initializers.h
index f9c0164..f0aba6e 100644
--- a/src/tint/transform/vectorize_scalar_matrix_initializers.h
+++ b/src/tint/transform/vectorize_scalar_matrix_initializers.h
@@ -21,7 +21,7 @@
 
 /// A transform that converts scalar matrix initializers to the vector form.
 class VectorizeScalarMatrixInitializers final
-    : public Castable<VectorizeScalarMatrixInitializers, Transform> {
+    : public utils::Castable<VectorizeScalarMatrixInitializers, Transform> {
   public:
     /// Constructor
     VectorizeScalarMatrixInitializers();
diff --git a/src/tint/transform/vertex_pulling.h b/src/tint/transform/vertex_pulling.h
index c0f88a5..d3849f7 100644
--- a/src/tint/transform/vertex_pulling.h
+++ b/src/tint/transform/vertex_pulling.h
@@ -137,10 +137,10 @@
 /// shader to use.
 ///
 /// The SingleEntryPoint transform must have run before VertexPulling.
-class VertexPulling final : public Castable<VertexPulling, Transform> {
+class VertexPulling final : public utils::Castable<VertexPulling, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public utils::Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/while_to_loop.h b/src/tint/transform/while_to_loop.h
index 187799a..91e5b0c 100644
--- a/src/tint/transform/while_to_loop.h
+++ b/src/tint/transform/while_to_loop.h
@@ -21,7 +21,7 @@
 
 /// WhileToLoop is a Transform that converts a while statement into a loop
 /// statement. This is required by the SPIR-V writer.
-class WhileToLoop final : public Castable<WhileToLoop, Transform> {
+class WhileToLoop final : public utils::Castable<WhileToLoop, Transform> {
   public:
     /// Constructor
     WhileToLoop();
diff --git a/src/tint/transform/zero_init_workgroup_memory.h b/src/tint/transform/zero_init_workgroup_memory.h
index 64f4da8..24ba226 100644
--- a/src/tint/transform/zero_init_workgroup_memory.h
+++ b/src/tint/transform/zero_init_workgroup_memory.h
@@ -22,7 +22,7 @@
 /// ZeroInitWorkgroupMemory is a transform that injects code at the top of entry
 /// points to zero-initialize workgroup memory used by that entry point (and all
 /// transitive functions called by that entry point)
-class ZeroInitWorkgroupMemory final : public Castable<ZeroInitWorkgroupMemory, Transform> {
+class ZeroInitWorkgroupMemory final : public utils::Castable<ZeroInitWorkgroupMemory, Transform> {
   public:
     /// Constructor
     ZeroInitWorkgroupMemory();
diff --git a/src/tint/type/abstract_float.cc b/src/tint/type/abstract_float.cc
index 74e5d34..41e0430 100644
--- a/src/tint/type/abstract_float.cc
+++ b/src/tint/type/abstract_float.cc
@@ -21,7 +21,9 @@
 
 namespace tint::type {
 
-AbstractFloat::AbstractFloat() : Base(utils::Hash(TypeInfo::Of<AbstractFloat>().full_hashcode)) {}
+AbstractFloat::AbstractFloat()
+    : Base(utils::Hash(utils::TypeInfo::Of<AbstractFloat>().full_hashcode)) {}
+
 AbstractFloat::~AbstractFloat() = default;
 
 bool AbstractFloat::Equals(const UniqueNode& other) const {
diff --git a/src/tint/type/abstract_float.h b/src/tint/type/abstract_float.h
index 2c6ac09..10d6f3a 100644
--- a/src/tint/type/abstract_float.h
+++ b/src/tint/type/abstract_float.h
@@ -23,7 +23,7 @@
 
 /// An abstract-float type.
 /// @see https://www.w3.org/TR/WGSL/#abstractFloat
-class AbstractFloat final : public Castable<AbstractFloat, AbstractNumeric> {
+class AbstractFloat final : public utils::Castable<AbstractFloat, AbstractNumeric> {
   public:
     /// Constructor
     AbstractFloat();
diff --git a/src/tint/type/abstract_int.cc b/src/tint/type/abstract_int.cc
index 7fc883d..6b97622 100644
--- a/src/tint/type/abstract_int.cc
+++ b/src/tint/type/abstract_int.cc
@@ -21,7 +21,7 @@
 
 namespace tint::type {
 
-AbstractInt::AbstractInt() : Base(utils::Hash(TypeInfo::Of<AbstractInt>().full_hashcode)) {}
+AbstractInt::AbstractInt() : Base(utils::Hash(utils::TypeInfo::Of<AbstractInt>().full_hashcode)) {}
 
 AbstractInt::~AbstractInt() = default;
 
diff --git a/src/tint/type/abstract_int.h b/src/tint/type/abstract_int.h
index f1f67d8..aaaaadd 100644
--- a/src/tint/type/abstract_int.h
+++ b/src/tint/type/abstract_int.h
@@ -23,7 +23,7 @@
 
 /// An abstract-int type.
 /// @see https://www.w3.org/TR/WGSL/#abstractint
-class AbstractInt final : public Castable<AbstractInt, AbstractNumeric> {
+class AbstractInt final : public utils::Castable<AbstractInt, AbstractNumeric> {
   public:
     /// Constructor
     AbstractInt();
diff --git a/src/tint/type/abstract_numeric.h b/src/tint/type/abstract_numeric.h
index 5b6b9f4..217b2a2 100644
--- a/src/tint/type/abstract_numeric.h
+++ b/src/tint/type/abstract_numeric.h
@@ -23,7 +23,7 @@
 
 /// The base class for abstract-int and abstract-float types.
 /// @see https://www.w3.org/TR/WGSL/#types-for-creation-time-constants
-class AbstractNumeric : public Castable<AbstractNumeric, Type> {
+class AbstractNumeric : public utils::Castable<AbstractNumeric, Type> {
   public:
     /// Constructor
     /// @param hash the unique hash of the node
diff --git a/src/tint/type/array.cc b/src/tint/type/array.cc
index 71ec6b3..31893f8 100644
--- a/src/tint/type/array.cc
+++ b/src/tint/type/array.cc
@@ -60,7 +60,7 @@
              uint32_t size,
              uint32_t stride,
              uint32_t implicit_stride)
-    : Base(utils::Hash(TypeInfo::Of<Array>().full_hashcode, count, align, size, stride),
+    : Base(utils::Hash(utils::TypeInfo::Of<Array>().full_hashcode, count, align, size, stride),
            FlagsFrom(element, count)),
       element_(element),
       count_(count),
diff --git a/src/tint/type/array.h b/src/tint/type/array.h
index 79b8e3c..c400475 100644
--- a/src/tint/type/array.h
+++ b/src/tint/type/array.h
@@ -28,7 +28,7 @@
 namespace tint::type {
 
 /// Array holds the type information for Array nodes.
-class Array final : public Castable<Array, Type> {
+class Array final : public utils::Castable<Array, Type> {
   public:
     /// An error message string stating that the array count was expected to be a constant
     /// expression. Used by multiple writers and transforms.
diff --git a/src/tint/type/array_count.cc b/src/tint/type/array_count.cc
index 8ddda4c..7b47300 100644
--- a/src/tint/type/array_count.cc
+++ b/src/tint/type/array_count.cc
@@ -26,7 +26,8 @@
 ArrayCount::~ArrayCount() = default;
 
 ConstantArrayCount::ConstantArrayCount(uint32_t val)
-    : Base(static_cast<size_t>(TypeInfo::Of<ConstantArrayCount>().full_hashcode)), value(val) {}
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<ConstantArrayCount>().full_hashcode)),
+      value(val) {}
 ConstantArrayCount::~ConstantArrayCount() = default;
 
 bool ConstantArrayCount::Equals(const UniqueNode& other) const {
@@ -45,7 +46,7 @@
 }
 
 RuntimeArrayCount::RuntimeArrayCount()
-    : Base(static_cast<size_t>(TypeInfo::Of<RuntimeArrayCount>().full_hashcode)) {}
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<RuntimeArrayCount>().full_hashcode)) {}
 RuntimeArrayCount::~RuntimeArrayCount() = default;
 
 bool RuntimeArrayCount::Equals(const UniqueNode& other) const {
diff --git a/src/tint/type/array_count.h b/src/tint/type/array_count.h
index 94dc135..172d2d3 100644
--- a/src/tint/type/array_count.h
+++ b/src/tint/type/array_count.h
@@ -25,7 +25,7 @@
 namespace tint::type {
 
 /// An array count
-class ArrayCount : public Castable<ArrayCount, UniqueNode> {
+class ArrayCount : public utils::Castable<ArrayCount, UniqueNode> {
   public:
     ~ArrayCount() override;
 
@@ -48,7 +48,7 @@
 /// const N = 123;
 /// type arr = array<i32, N>
 /// ```
-class ConstantArrayCount final : public Castable<ConstantArrayCount, ArrayCount> {
+class ConstantArrayCount final : public utils::Castable<ConstantArrayCount, ArrayCount> {
   public:
     /// Constructor
     /// @param val the constant-expression value
@@ -75,7 +75,7 @@
 /// ```
 /// type arr = array<i32>
 /// ```
-class RuntimeArrayCount final : public Castable<RuntimeArrayCount, ArrayCount> {
+class RuntimeArrayCount final : public utils::Castable<RuntimeArrayCount, ArrayCount> {
   public:
     /// Constructor
     RuntimeArrayCount();
diff --git a/src/tint/type/atomic.cc b/src/tint/type/atomic.cc
index a1d5da1..0189301 100644
--- a/src/tint/type/atomic.cc
+++ b/src/tint/type/atomic.cc
@@ -26,7 +26,7 @@
 namespace tint::type {
 
 Atomic::Atomic(const type::Type* subtype)
-    : Base(utils::Hash(TypeInfo::Of<Atomic>().full_hashcode, subtype),
+    : Base(utils::Hash(utils::TypeInfo::Of<Atomic>().full_hashcode, subtype),
            type::Flags{
                Flag::kCreationFixedFootprint,
                Flag::kFixedFootprint,
diff --git a/src/tint/type/atomic.h b/src/tint/type/atomic.h
index 4f304f0..a19239b 100644
--- a/src/tint/type/atomic.h
+++ b/src/tint/type/atomic.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A atomic type.
-class Atomic final : public Castable<Atomic, Type> {
+class Atomic final : public utils::Castable<Atomic, Type> {
   public:
     /// Constructor
     /// @param subtype the atomic type
diff --git a/src/tint/type/bool.cc b/src/tint/type/bool.cc
index 6c972c3..26741c5 100644
--- a/src/tint/type/bool.cc
+++ b/src/tint/type/bool.cc
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 Bool::Bool()
-    : Base(static_cast<size_t>(TypeInfo::Of<Bool>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<Bool>().full_hashcode),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/bool.h b/src/tint/type/bool.h
index 6173bbb..33906b8 100644
--- a/src/tint/type/bool.h
+++ b/src/tint/type/bool.h
@@ -28,7 +28,7 @@
 namespace tint::type {
 
 /// A boolean type
-class Bool final : public Castable<Bool, Type> {
+class Bool final : public utils::Castable<Bool, Type> {
   public:
     /// Constructor
     Bool();
diff --git a/src/tint/type/depth_multisampled_texture.cc b/src/tint/type/depth_multisampled_texture.cc
index 765fec3..7109920 100644
--- a/src/tint/type/depth_multisampled_texture.cc
+++ b/src/tint/type/depth_multisampled_texture.cc
@@ -33,7 +33,7 @@
 }  // namespace
 
 DepthMultisampledTexture::DepthMultisampledTexture(TextureDimension dim)
-    : Base(utils::Hash(TypeInfo::Of<DepthMultisampledTexture>().full_hashcode, dim), dim) {
+    : Base(utils::Hash(utils::TypeInfo::Of<DepthMultisampledTexture>().full_hashcode, dim), dim) {
     TINT_ASSERT(Type, IsValidDepthDimension(dim));
 }
 
diff --git a/src/tint/type/depth_multisampled_texture.h b/src/tint/type/depth_multisampled_texture.h
index 4a6d3c2..9a72d61 100644
--- a/src/tint/type/depth_multisampled_texture.h
+++ b/src/tint/type/depth_multisampled_texture.h
@@ -23,7 +23,7 @@
 namespace tint::type {
 
 /// A multisampled depth texture type.
-class DepthMultisampledTexture final : public Castable<DepthMultisampledTexture, Texture> {
+class DepthMultisampledTexture final : public utils::Castable<DepthMultisampledTexture, Texture> {
   public:
     /// Constructor
     /// @param dim the dimensionality of the texture
diff --git a/src/tint/type/depth_texture.cc b/src/tint/type/depth_texture.cc
index 1d6eab5..d4476fb 100644
--- a/src/tint/type/depth_texture.cc
+++ b/src/tint/type/depth_texture.cc
@@ -34,7 +34,7 @@
 }  // namespace
 
 DepthTexture::DepthTexture(TextureDimension dim)
-    : Base(utils::Hash(TypeInfo::Of<DepthTexture>().full_hashcode, dim), dim) {
+    : Base(utils::Hash(utils::TypeInfo::Of<DepthTexture>().full_hashcode, dim), dim) {
     TINT_ASSERT(Type, IsValidDepthDimension(dim));
 }
 
diff --git a/src/tint/type/depth_texture.h b/src/tint/type/depth_texture.h
index 079b5fb..8e8c47d 100644
--- a/src/tint/type/depth_texture.h
+++ b/src/tint/type/depth_texture.h
@@ -23,7 +23,7 @@
 namespace tint::type {
 
 /// A depth texture type.
-class DepthTexture final : public Castable<DepthTexture, Texture> {
+class DepthTexture final : public utils::Castable<DepthTexture, Texture> {
   public:
     /// Constructor
     /// @param dim the dimensionality of the texture
diff --git a/src/tint/type/external_texture.cc b/src/tint/type/external_texture.cc
index 85469a7..924d06a 100644
--- a/src/tint/type/external_texture.cc
+++ b/src/tint/type/external_texture.cc
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 ExternalTexture::ExternalTexture()
-    : Base(static_cast<size_t>(TypeInfo::Of<ExternalTexture>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<ExternalTexture>().full_hashcode),
            TextureDimension::k2d) {}
 
 ExternalTexture::~ExternalTexture() = default;
diff --git a/src/tint/type/external_texture.h b/src/tint/type/external_texture.h
index be46048..219e7b8 100644
--- a/src/tint/type/external_texture.h
+++ b/src/tint/type/external_texture.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// An external texture type
-class ExternalTexture final : public Castable<ExternalTexture, Texture> {
+class ExternalTexture final : public utils::Castable<ExternalTexture, Texture> {
   public:
     /// Constructor
     ExternalTexture();
diff --git a/src/tint/type/f16.cc b/src/tint/type/f16.cc
index 842d147..288f14a 100644
--- a/src/tint/type/f16.cc
+++ b/src/tint/type/f16.cc
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 F16::F16()
-    : Base(static_cast<size_t>(TypeInfo::Of<F16>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<F16>().full_hashcode),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/f16.h b/src/tint/type/f16.h
index 21100ff..c7fc3eb 100644
--- a/src/tint/type/f16.h
+++ b/src/tint/type/f16.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A float 16 type
-class F16 final : public Castable<F16, Type> {
+class F16 final : public utils::Castable<F16, Type> {
   public:
     /// Constructor
     F16();
diff --git a/src/tint/type/f32.cc b/src/tint/type/f32.cc
index b7c5135..e3afcc7 100644
--- a/src/tint/type/f32.cc
+++ b/src/tint/type/f32.cc
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 F32::F32()
-    : Base(static_cast<size_t>(TypeInfo::Of<F32>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<F32>().full_hashcode),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/f32.h b/src/tint/type/f32.h
index 397911d..68b3c77 100644
--- a/src/tint/type/f32.h
+++ b/src/tint/type/f32.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A float 32 type
-class F32 final : public Castable<F32, Type> {
+class F32 final : public utils::Castable<F32, Type> {
   public:
     /// Constructor
     F32();
diff --git a/src/tint/type/i32.cc b/src/tint/type/i32.cc
index 045706d..66da527 100644
--- a/src/tint/type/i32.cc
+++ b/src/tint/type/i32.cc
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 I32::I32()
-    : Base(static_cast<size_t>(TypeInfo::Of<I32>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<I32>().full_hashcode),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/i32.h b/src/tint/type/i32.h
index ddf59b4..563ee69 100644
--- a/src/tint/type/i32.h
+++ b/src/tint/type/i32.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A signed int 32 type.
-class I32 final : public Castable<I32, Type> {
+class I32 final : public utils::Castable<I32, Type> {
   public:
     /// Constructor
     I32();
diff --git a/src/tint/type/manager.h b/src/tint/type/manager.h
index 59a8cf5..0650f1b 100644
--- a/src/tint/type/manager.h
+++ b/src/tint/type/manager.h
@@ -65,9 +65,9 @@
     ///         constructed, then the same pointer is returned.
     template <typename NODE, typename... ARGS>
     NODE* Get(ARGS&&... args) {
-        if constexpr (traits::IsTypeOrDerived<NODE, Type>) {
+        if constexpr (utils::traits::IsTypeOrDerived<NODE, Type>) {
             return types_.Get<NODE>(std::forward<ARGS>(args)...);
-        } else if constexpr (traits::IsTypeOrDerived<NODE, UniqueNode>) {
+        } else if constexpr (utils::traits::IsTypeOrDerived<NODE, UniqueNode>) {
             return unique_nodes_.Get<NODE>(std::forward<ARGS>(args)...);
         } else {
             return nodes_.Create<NODE>(std::forward<ARGS>(args)...);
@@ -78,7 +78,7 @@
     /// @return a pointer to an instance of `T` with the provided arguments, or nullptr if the item
     ///         was not found.
     template <typename TYPE,
-              typename _ = std::enable_if<traits::IsTypeOrDerived<TYPE, Type>>,
+              typename _ = std::enable_if<utils::traits::IsTypeOrDerived<TYPE, Type>>,
               typename... ARGS>
     TYPE* Find(ARGS&&... args) const {
         return types_.Find<TYPE>(std::forward<ARGS>(args)...);
diff --git a/src/tint/type/matrix.cc b/src/tint/type/matrix.cc
index 59dfaa9..35664f3 100644
--- a/src/tint/type/matrix.cc
+++ b/src/tint/type/matrix.cc
@@ -26,7 +26,7 @@
 namespace tint::type {
 
 Matrix::Matrix(const Vector* column_type, uint32_t columns)
-    : Base(utils::Hash(TypeInfo::Of<Vector>().full_hashcode, columns, column_type),
+    : Base(utils::Hash(utils::TypeInfo::Of<Vector>().full_hashcode, columns, column_type),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/matrix.h b/src/tint/type/matrix.h
index c6707a2..1519d6a 100644
--- a/src/tint/type/matrix.h
+++ b/src/tint/type/matrix.h
@@ -27,7 +27,7 @@
 namespace tint::type {
 
 /// A matrix type
-class Matrix final : public Castable<Matrix, Type> {
+class Matrix final : public utils::Castable<Matrix, Type> {
   public:
     /// Constructor
     /// @param column_type the type of a column of the matrix
diff --git a/src/tint/type/multisampled_texture.cc b/src/tint/type/multisampled_texture.cc
index b63c681..850b11b 100644
--- a/src/tint/type/multisampled_texture.cc
+++ b/src/tint/type/multisampled_texture.cc
@@ -26,7 +26,7 @@
 namespace tint::type {
 
 MultisampledTexture::MultisampledTexture(TextureDimension dim, const Type* type)
-    : Base(utils::Hash(TypeInfo::Of<MultisampledTexture>().full_hashcode, dim, type), dim),
+    : Base(utils::Hash(utils::TypeInfo::Of<MultisampledTexture>().full_hashcode, dim, type), dim),
       type_(type) {
     TINT_ASSERT(Type, type_);
 }
diff --git a/src/tint/type/multisampled_texture.h b/src/tint/type/multisampled_texture.h
index d6c2e50..89575ee 100644
--- a/src/tint/type/multisampled_texture.h
+++ b/src/tint/type/multisampled_texture.h
@@ -23,7 +23,7 @@
 namespace tint::type {
 
 /// A multisampled texture type.
-class MultisampledTexture final : public Castable<MultisampledTexture, Texture> {
+class MultisampledTexture final : public utils::Castable<MultisampledTexture, Texture> {
   public:
     /// Constructor
     /// @param dim the dimensionality of the texture
diff --git a/src/tint/type/node.h b/src/tint/type/node.h
index eddcb27..237c3ec 100644
--- a/src/tint/type/node.h
+++ b/src/tint/type/node.h
@@ -15,12 +15,12 @@
 #ifndef SRC_TINT_TYPE_NODE_H_
 #define SRC_TINT_TYPE_NODE_H_
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 
 namespace tint::type {
 
 /// Node is the base class for all type nodes
-class Node : public Castable<Node> {
+class Node : public utils::Castable<Node> {
   public:
     /// Constructor
     Node();
diff --git a/src/tint/type/pointer.cc b/src/tint/type/pointer.cc
index 2922b0e..636f8e5 100644
--- a/src/tint/type/pointer.cc
+++ b/src/tint/type/pointer.cc
@@ -26,8 +26,9 @@
 namespace tint::type {
 
 Pointer::Pointer(const Type* subtype, builtin::AddressSpace address_space, builtin::Access access)
-    : Base(utils::Hash(TypeInfo::Of<Pointer>().full_hashcode, address_space, subtype, access),
-           type::Flags{}),
+    : Base(
+          utils::Hash(utils::TypeInfo::Of<Pointer>().full_hashcode, address_space, subtype, access),
+          type::Flags{}),
       subtype_(subtype),
       address_space_(address_space),
       access_(access) {
diff --git a/src/tint/type/pointer.h b/src/tint/type/pointer.h
index 0eed27c..80626e9 100644
--- a/src/tint/type/pointer.h
+++ b/src/tint/type/pointer.h
@@ -24,7 +24,7 @@
 namespace tint::type {
 
 /// A pointer type.
-class Pointer final : public Castable<Pointer, Type> {
+class Pointer final : public utils::Castable<Pointer, Type> {
   public:
     /// Constructor
     /// @param subtype the pointee type
diff --git a/src/tint/type/reference.cc b/src/tint/type/reference.cc
index bf2d22c..e0fd92a 100644
--- a/src/tint/type/reference.cc
+++ b/src/tint/type/reference.cc
@@ -27,7 +27,10 @@
 Reference::Reference(const Type* subtype,
                      builtin::AddressSpace address_space,
                      builtin::Access access)
-    : Base(utils::Hash(TypeInfo::Of<Reference>().full_hashcode, address_space, subtype, access),
+    : Base(utils::Hash(utils::TypeInfo::Of<Reference>().full_hashcode,
+                       address_space,
+                       subtype,
+                       access),
            type::Flags{}),
       subtype_(subtype),
       address_space_(address_space),
diff --git a/src/tint/type/reference.h b/src/tint/type/reference.h
index 794b48b..617b2ca 100644
--- a/src/tint/type/reference.h
+++ b/src/tint/type/reference.h
@@ -24,7 +24,7 @@
 namespace tint::type {
 
 /// A reference type.
-class Reference final : public Castable<Reference, Type> {
+class Reference final : public utils::Castable<Reference, Type> {
   public:
     /// Constructor
     /// @param subtype the pointee type
diff --git a/src/tint/type/sampled_texture.cc b/src/tint/type/sampled_texture.cc
index 8a4e440..2fb29f6 100644
--- a/src/tint/type/sampled_texture.cc
+++ b/src/tint/type/sampled_texture.cc
@@ -26,7 +26,8 @@
 namespace tint::type {
 
 SampledTexture::SampledTexture(TextureDimension dim, const Type* type)
-    : Base(utils::Hash(TypeInfo::Of<SampledTexture>().full_hashcode, dim, type), dim), type_(type) {
+    : Base(utils::Hash(utils::TypeInfo::Of<SampledTexture>().full_hashcode, dim, type), dim),
+      type_(type) {
     TINT_ASSERT(Type, type_);
 }
 
diff --git a/src/tint/type/sampled_texture.h b/src/tint/type/sampled_texture.h
index 35208b7..346741f 100644
--- a/src/tint/type/sampled_texture.h
+++ b/src/tint/type/sampled_texture.h
@@ -23,7 +23,7 @@
 namespace tint::type {
 
 /// A sampled texture type.
-class SampledTexture final : public Castable<SampledTexture, Texture> {
+class SampledTexture final : public utils::Castable<SampledTexture, Texture> {
   public:
     /// Constructor
     /// @param dim the dimensionality of the texture
diff --git a/src/tint/type/sampler.cc b/src/tint/type/sampler.cc
index 271b8be..2ad8f0c 100644
--- a/src/tint/type/sampler.cc
+++ b/src/tint/type/sampler.cc
@@ -22,7 +22,8 @@
 namespace tint::type {
 
 Sampler::Sampler(SamplerKind kind)
-    : Base(utils::Hash(TypeInfo::Of<Sampler>().full_hashcode, kind), type::Flags{}), kind_(kind) {}
+    : Base(utils::Hash(utils::TypeInfo::Of<Sampler>().full_hashcode, kind), type::Flags{}),
+      kind_(kind) {}
 
 Sampler::~Sampler() = default;
 
diff --git a/src/tint/type/sampler.h b/src/tint/type/sampler.h
index 9a3554f..d9f7286 100644
--- a/src/tint/type/sampler.h
+++ b/src/tint/type/sampler.h
@@ -23,7 +23,7 @@
 namespace tint::type {
 
 /// A sampler type.
-class Sampler final : public Castable<Sampler, Type> {
+class Sampler final : public utils::Castable<Sampler, Type> {
   public:
     /// Constructor
     /// @param kind the kind of sampler
diff --git a/src/tint/type/storage_texture.cc b/src/tint/type/storage_texture.cc
index 7aa84c1..cd41452 100644
--- a/src/tint/type/storage_texture.cc
+++ b/src/tint/type/storage_texture.cc
@@ -29,7 +29,8 @@
                                builtin::TexelFormat format,
                                builtin::Access access,
                                Type* subtype)
-    : Base(utils::Hash(TypeInfo::Of<StorageTexture>().full_hashcode, dim, format, access), dim),
+    : Base(utils::Hash(utils::TypeInfo::Of<StorageTexture>().full_hashcode, dim, format, access),
+           dim),
       texel_format_(format),
       access_(access),
       subtype_(subtype) {}
diff --git a/src/tint/type/storage_texture.h b/src/tint/type/storage_texture.h
index 206e403..abd580c 100644
--- a/src/tint/type/storage_texture.h
+++ b/src/tint/type/storage_texture.h
@@ -30,7 +30,7 @@
 namespace tint::type {
 
 /// A storage texture type.
-class StorageTexture final : public Castable<StorageTexture, Texture> {
+class StorageTexture final : public utils::Castable<StorageTexture, Texture> {
   public:
     /// Constructor
     /// @param dim the dimensionality of the texture
diff --git a/src/tint/type/struct.cc b/src/tint/type/struct.cc
index bffaa21..e5c8d4c 100644
--- a/src/tint/type/struct.cc
+++ b/src/tint/type/struct.cc
@@ -58,7 +58,7 @@
                uint32_t align,
                uint32_t size,
                uint32_t size_no_padding)
-    : Base(utils::Hash(TypeInfo::Of<Struct>().full_hashcode, name), FlagsFrom(members)),
+    : Base(utils::Hash(utils::TypeInfo::Of<Struct>().full_hashcode, name), FlagsFrom(members)),
       source_(source),
       name_(name),
       members_(std::move(members)),
diff --git a/src/tint/type/struct.h b/src/tint/type/struct.h
index 035ea7c..ea459f5 100644
--- a/src/tint/type/struct.h
+++ b/src/tint/type/struct.h
@@ -45,7 +45,7 @@
 };
 
 /// Struct holds the Type information for structures.
-class Struct : public Castable<Struct, Type> {
+class Struct : public utils::Castable<Struct, Type> {
   public:
     /// Constructor
     /// @param source the source of the structure
@@ -164,7 +164,7 @@
 };
 
 /// StructMember holds the type information for structure members.
-class StructMember : public Castable<StructMember, Node> {
+class StructMember : public utils::Castable<StructMember, Node> {
   public:
     /// Constructor
     /// @param source the source of the struct member
diff --git a/src/tint/type/texture.h b/src/tint/type/texture.h
index e7cbe81..db99ae5 100644
--- a/src/tint/type/texture.h
+++ b/src/tint/type/texture.h
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 /// A texture type.
-class Texture : public Castable<Texture, Type> {
+class Texture : public utils::Castable<Texture, Type> {
   public:
     /// Constructor
     /// @param hash the unique hash of the node
diff --git a/src/tint/type/type.h b/src/tint/type/type.h
index 083934e..b6f34a6 100644
--- a/src/tint/type/type.h
+++ b/src/tint/type/type.h
@@ -47,7 +47,7 @@
 using Flags = utils::EnumSet<Flag>;
 
 /// Base class for a type in the system
-class Type : public Castable<Type, UniqueNode> {
+class Type : public utils::Castable<Type, UniqueNode> {
   public:
     /// Destructor
     ~Type() override;
diff --git a/src/tint/type/u32.cc b/src/tint/type/u32.cc
index f1eaf66..4e40c73 100644
--- a/src/tint/type/u32.cc
+++ b/src/tint/type/u32.cc
@@ -21,7 +21,7 @@
 namespace tint::type {
 
 U32::U32()
-    : Base(static_cast<size_t>(TypeInfo::Of<U32>().full_hashcode),
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<U32>().full_hashcode),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/u32.h b/src/tint/type/u32.h
index 456436a..de9b550 100644
--- a/src/tint/type/u32.h
+++ b/src/tint/type/u32.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A unsigned int 32 type.
-class U32 final : public Castable<U32, Type> {
+class U32 final : public utils::Castable<U32, Type> {
   public:
     /// Constructor
     U32();
diff --git a/src/tint/type/unique_node.h b/src/tint/type/unique_node.h
index 1fd6011..0e642df 100644
--- a/src/tint/type/unique_node.h
+++ b/src/tint/type/unique_node.h
@@ -25,7 +25,7 @@
 /// Deduplication is achieved by comparing a temporary object to the set of existing objects, using
 /// Hash() and Equals(). If an existing object is found, then the pointer to that object is
 /// returned, otherwise a new object is constructed, added to the Manager's set and returned.
-class UniqueNode : public Castable<UniqueNode, Node> {
+class UniqueNode : public utils::Castable<UniqueNode, Node> {
   public:
     /// Constructor
     /// @param hash the immutable hash for the node
diff --git a/src/tint/type/vector.cc b/src/tint/type/vector.cc
index 3ee68e9..9c5bebd 100644
--- a/src/tint/type/vector.cc
+++ b/src/tint/type/vector.cc
@@ -25,7 +25,7 @@
 namespace tint::type {
 
 Vector::Vector(Type const* subtype, uint32_t width, bool packed /* = false */)
-    : Base(utils::Hash(TypeInfo::Of<Vector>().full_hashcode, width, subtype, packed),
+    : Base(utils::Hash(utils::TypeInfo::Of<Vector>().full_hashcode, width, subtype, packed),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
diff --git a/src/tint/type/vector.h b/src/tint/type/vector.h
index 04e349a..47b5c40 100644
--- a/src/tint/type/vector.h
+++ b/src/tint/type/vector.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A vector type.
-class Vector : public Castable<Vector, Type> {
+class Vector : public utils::Castable<Vector, Type> {
   public:
     /// Constructor
     /// @param subtype the vector element type
diff --git a/src/tint/type/void.cc b/src/tint/type/void.cc
index c562a52..2b8ddf7 100644
--- a/src/tint/type/void.cc
+++ b/src/tint/type/void.cc
@@ -20,7 +20,8 @@
 
 namespace tint::type {
 
-Void::Void() : Base(static_cast<size_t>(TypeInfo::Of<Void>().full_hashcode), type::Flags{}) {}
+Void::Void()
+    : Base(static_cast<size_t>(utils::TypeInfo::Of<Void>().full_hashcode), type::Flags{}) {}
 
 Void::~Void() = default;
 
diff --git a/src/tint/type/void.h b/src/tint/type/void.h
index b24ea9f..0215c08 100644
--- a/src/tint/type/void.h
+++ b/src/tint/type/void.h
@@ -22,7 +22,7 @@
 namespace tint::type {
 
 /// A void type
-class Void final : public Castable<Void, Type> {
+class Void final : public utils::Castable<Void, Type> {
   public:
     /// Constructor
     Void();
diff --git a/src/tint/castable.cc b/src/tint/utils/castable.cc
similarity index 81%
rename from src/tint/castable.cc
rename to src/tint/utils/castable.cc
index 40c32da..b7756b3 100644
--- a/src/tint/castable.cc
+++ b/src/tint/utils/castable.cc
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 
-namespace tint {
+namespace tint::utils {
 
 /// The unique TypeInfo for the CastableBase type
 /// @return doxygen-thinks-this-static-field-is-a-function :(
@@ -22,12 +22,12 @@
 const TypeInfo detail::TypeInfoOf<CastableBase>::info{
     nullptr,
     "CastableBase",
-    tint::TypeInfo::HashCodeOf<CastableBase>(),
-    tint::TypeInfo::FullHashCodeOf<CastableBase>(),
+    tint::utils::TypeInfo::HashCodeOf<CastableBase>(),
+    tint::utils::TypeInfo::FullHashCodeOf<CastableBase>(),
 };
 
 CastableBase::CastableBase(const CastableBase&) = default;
 
 CastableBase::~CastableBase() = default;
 
-}  // namespace tint
+}  // namespace tint::utils
diff --git a/src/tint/castable.h b/src/tint/utils/castable.h
similarity index 88%
rename from src/tint/castable.h
rename to src/tint/utils/castable.h
index 71e3ab8..abe7610 100644
--- a/src/tint/castable.h
+++ b/src/tint/utils/castable.h
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_CASTABLE_H_
-#define SRC_TINT_CASTABLE_H_
+#ifndef SRC_TINT_UTILS_CASTABLE_H_
+#define SRC_TINT_UTILS_CASTABLE_H_
 
 #include <stdint.h>
 #include <functional>
 #include <tuple>
 #include <utility>
 
-#include "src/tint/traits.h"
 #include "src/tint/utils/crc32.h"
+#include "src/tint/utils/traits.h"
 
 #if defined(__clang__)
 /// Temporarily disable certain warnings when using Castable API
@@ -42,37 +42,37 @@
 TINT_CASTABLE_PUSH_DISABLE_WARNINGS();
 
 // Forward declarations
-namespace tint {
+namespace tint::utils {
 class CastableBase;
 
 /// Ignore is used as a special type used for skipping over types for trait
 /// helper functions.
 class Ignore {};
-}  // namespace tint
+}  // namespace tint::utils
 
-namespace tint::detail {
+namespace tint::utils::detail {
 template <typename T>
 struct TypeInfoOf;
-}  // namespace tint::detail
+}  // namespace tint::utils::detail
 
-namespace tint {
+namespace tint::utils {
 
 /// True if all template types that are not Ignore derive from CastableBase
 template <typename... TYPES>
 static constexpr bool IsCastable =
-    ((traits::IsTypeOrDerived<TYPES, CastableBase> || std::is_same_v<TYPES, Ignore>)&&...) &&
+    ((utils::traits::IsTypeOrDerived<TYPES, CastableBase> || std::is_same_v<TYPES, Ignore>)&&...) &&
     !(std::is_same_v<TYPES, Ignore> && ...);
 
 /// Helper macro to instantiate the TypeInfo<T> template for `CLASS`.
-#define TINT_INSTANTIATE_TYPEINFO(CLASS)                        \
-    TINT_CASTABLE_PUSH_DISABLE_WARNINGS();                      \
-    template <>                                                 \
-    const tint::TypeInfo tint::detail::TypeInfoOf<CLASS>::info{ \
-        &tint::detail::TypeInfoOf<CLASS::TrueBase>::info,       \
-        #CLASS,                                                 \
-        tint::TypeInfo::HashCodeOf<CLASS>(),                    \
-        tint::TypeInfo::FullHashCodeOf<CLASS>(),                \
-    };                                                          \
+#define TINT_INSTANTIATE_TYPEINFO(CLASS)                                      \
+    TINT_CASTABLE_PUSH_DISABLE_WARNINGS();                                    \
+    template <>                                                               \
+    const tint::utils::TypeInfo tint::utils::detail::TypeInfoOf<CLASS>::info{ \
+        &tint::utils::detail::TypeInfoOf<CLASS::TrueBase>::info,              \
+        #CLASS,                                                               \
+        tint::utils::TypeInfo::HashCodeOf<CLASS>(),                           \
+        tint::utils::TypeInfo::FullHashCodeOf<CLASS>(),                       \
+    };                                                                        \
     TINT_CASTABLE_POP_DISABLE_WARNINGS()
 
 /// Bit flags that can be passed to the template parameter `FLAGS` of Is() and As().
@@ -127,7 +127,7 @@
     /// @param object the object type to test from, which must be, or derive from type `FROM`.
     /// @see CastFlags
     template <typename TO, typename FROM, int FLAGS = 0>
-    static inline bool Is(const tint::TypeInfo* object) {
+    static inline bool Is(const tint::utils::TypeInfo* object) {
         constexpr const bool downcast = std::is_base_of<FROM, TO>::value;
         constexpr const bool upcast = std::is_base_of<TO, FROM>::value;
         constexpr const bool nocast = std::is_same<FROM, TO>::value;
@@ -155,7 +155,7 @@
     /// @param type the test type info
     /// @returns true if the class with this TypeInfo is of, or derives from the
     /// class with the given TypeInfo.
-    inline bool Is(const tint::TypeInfo* type) const {
+    inline bool Is(const tint::utils::TypeInfo* type) const {
         if (!Maybe(type->hashcode, full_hashcode)) {
             return false;
         }
@@ -220,8 +220,8 @@
             return HashCodeOf<std::remove_cv_t<std::tuple_element_t<0, TUPLE>>>();
         } else {
             constexpr auto kMid = kCount / 2;
-            return CombinedHashCodeOfTuple<traits::SliceTuple<0, kMid, TUPLE>>() |
-                   CombinedHashCodeOfTuple<traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
+            return CombinedHashCodeOfTuple<utils::traits::SliceTuple<0, kMid, TUPLE>>() |
+                   CombinedHashCodeOfTuple<utils::traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
         }
     }
 
@@ -245,8 +245,8 @@
                 // Possibly one of the types in `TUPLE`.
                 // Split the search in two, and scan each block.
                 static constexpr auto kMid = kCount / 2;
-                return IsAnyOfTuple<traits::SliceTuple<0, kMid, TUPLE>>() ||
-                       IsAnyOfTuple<traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
+                return IsAnyOfTuple<utils::traits::SliceTuple<0, kMid, TUPLE>>() ||
+                       IsAnyOfTuple<utils::traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
             }
             return false;
         }
@@ -346,12 +346,12 @@
     CastableBase& operator=(const CastableBase& other) = default;
 
     /// @returns the TypeInfo of the object
-    inline const tint::TypeInfo& TypeInfo() const { return *type_info_; }
+    inline const tint::utils::TypeInfo& TypeInfo() const { return *type_info_; }
 
     /// @returns true if this object is of, or derives from the class `TO`
     template <typename TO>
     inline bool Is() const {
-        return tint::Is<TO>(this);
+        return tint::utils::Is<TO>(this);
     }
 
     /// @returns true if this object is of, or derives from the class `TO` and pred(const TO*)
@@ -360,13 +360,13 @@
     /// derives from the class `TO`.
     template <typename TO, int FLAGS = 0, typename Pred = detail::Infer>
     inline bool Is(Pred&& pred) const {
-        return tint::Is<TO, FLAGS>(this, std::forward<Pred>(pred));
+        return tint::utils::Is<TO, FLAGS>(this, std::forward<Pred>(pred));
     }
 
     /// @returns true if this object is of, or derives from any of the `TO` classes.
     template <typename... TO>
     inline bool IsAnyOf() const {
-        return tint::IsAnyOf<TO...>(this);
+        return tint::utils::IsAnyOf<TO...>(this);
     }
 
     /// @returns this object dynamically cast to the type `TO` or `nullptr` if this object does not
@@ -374,7 +374,7 @@
     /// @see CastFlags
     template <typename TO, int FLAGS = 0>
     inline TO* As() {
-        return tint::As<TO, FLAGS>(this);
+        return tint::utils::As<TO, FLAGS>(this);
     }
 
     /// @returns this object dynamically cast to the type `TO` or `nullptr` if this object does not
@@ -382,14 +382,14 @@
     /// @see CastFlags
     template <typename TO, int FLAGS = 0>
     inline const TO* As() const {
-        return tint::As<const TO, FLAGS>(this);
+        return tint::utils::As<const TO, FLAGS>(this);
     }
 
   protected:
     CastableBase() = default;
 
     /// The type information for the object
-    const tint::TypeInfo* type_info_ = nullptr;
+    const tint::utils::TypeInfo* type_info_ = nullptr;
 };
 
 /// Castable is a helper to derive `CLASS` from `BASE`, automatically implementing the Is() and As()
@@ -433,7 +433,7 @@
     /// @see CastFlags
     template <typename TO, int FLAGS = 0>
     inline bool Is() const {
-        return tint::Is<TO, FLAGS>(static_cast<const CLASS*>(this));
+        return tint::utils::Is<TO, FLAGS>(static_cast<const CLASS*>(this));
     }
 
     /// @returns true if this object is of, or derives from the class `TO` and
@@ -442,15 +442,16 @@
     /// object is of, or derives from the class `TO`.
     template <int FLAGS = 0, typename Pred = detail::Infer>
     inline bool Is(Pred&& pred) const {
-        using TO = typename std::remove_pointer<traits::ParameterType<Pred, 0>>::type;
-        return tint::Is<TO, FLAGS>(static_cast<const CLASS*>(this), std::forward<Pred>(pred));
+        using TO = typename std::remove_pointer<utils::traits::ParameterType<Pred, 0>>::type;
+        return tint::utils::Is<TO, FLAGS>(static_cast<const CLASS*>(this),
+                                          std::forward<Pred>(pred));
     }
 
     /// @returns true if this object is of, or derives from any of the `TO`
     /// classes.
     template <typename... TO>
     inline bool IsAnyOf() const {
-        return tint::IsAnyOf<TO...>(static_cast<const CLASS*>(this));
+        return tint::utils::IsAnyOf<TO...>(static_cast<const CLASS*>(this));
     }
 
     /// @returns this object dynamically cast to the type `TO` or `nullptr` if
@@ -458,7 +459,7 @@
     /// @see CastFlags
     template <typename TO, int FLAGS = 0>
     inline TO* As() {
-        return tint::As<TO, FLAGS>(this);
+        return tint::utils::As<TO, FLAGS>(this);
     }
 
     /// @returns this object dynamically cast to the type `TO` or `nullptr` if
@@ -466,7 +467,7 @@
     /// @see CastFlags
     template <typename TO, int FLAGS = 0>
     inline const TO* As() const {
-        return tint::As<const TO, FLAGS>(this);
+        return tint::utils::As<const TO, FLAGS>(this);
     }
 };
 
@@ -512,7 +513,7 @@
 template <typename A, typename B>
 struct CastableCommonBaseImpl<A, B> {
     /// The common base class for A, B and OTHERS
-    using type = std::conditional_t<traits::IsTypeOrDerived<A, B>,
+    using type = std::conditional_t<utils::traits::IsTypeOrDerived<A, B>,
                                     B,  // A derives from B
                                     CastableCommonBase<A, typename B::TrueBase>>;
 };
@@ -530,8 +531,15 @@
 template <typename... TYPES>
 using CastableCommonBase = detail::CastableCommonBase<TYPES...>;
 
+}  // namespace tint::utils
+
+namespace tint {
+
+using utils::As;
+using utils::Is;
+
 }  // namespace tint
 
 TINT_CASTABLE_POP_DISABLE_WARNINGS();
 
-#endif  // SRC_TINT_CASTABLE_H_
+#endif  // SRC_TINT_UTILS_CASTABLE_H_
diff --git a/src/tint/castable_test.cc b/src/tint/utils/castable_test.cc
similarity index 94%
rename from src/tint/castable_test.cc
rename to src/tint/utils/castable_test.cc
index 57bd904..419e506 100644
--- a/src/tint/castable_test.cc
+++ b/src/tint/utils/castable_test.cc
@@ -12,25 +12,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/castable.h"
+#include "src/tint/utils/castable.h"
 
 #include <memory>
 #include <string>
 
 #include "gtest/gtest.h"
 
-namespace tint {
+namespace tint::utils {
 namespace {
 
-struct Animal : public tint::Castable<Animal> {};
-struct Amphibian : public tint::Castable<Amphibian, Animal> {};
-struct Mammal : public tint::Castable<Mammal, Animal> {};
-struct Reptile : public tint::Castable<Reptile, Animal> {};
-struct Frog : public tint::Castable<Frog, Amphibian> {};
-struct Bear : public tint::Castable<Bear, Mammal> {};
-struct Lizard : public tint::Castable<Lizard, Reptile> {};
-struct Gecko : public tint::Castable<Gecko, Lizard> {};
-struct Iguana : public tint::Castable<Iguana, Lizard> {};
+struct Animal : public tint::utils::Castable<Animal> {};
+struct Amphibian : public tint::utils::Castable<Amphibian, Animal> {};
+struct Mammal : public tint::utils::Castable<Mammal, Animal> {};
+struct Reptile : public tint::utils::Castable<Reptile, Animal> {};
+struct Frog : public tint::utils::Castable<Frog, Amphibian> {};
+struct Bear : public tint::utils::Castable<Bear, Mammal> {};
+struct Lizard : public tint::utils::Castable<Lizard, Reptile> {};
+struct Gecko : public tint::utils::Castable<Gecko, Lizard> {};
+struct Iguana : public tint::utils::Castable<Iguana, Lizard> {};
 
 TEST(CastableBase, Is) {
     std::unique_ptr<CastableBase> frog = std::make_unique<Frog>();
@@ -291,4 +291,4 @@
 TINT_INSTANTIATE_TYPEINFO(Lizard);
 TINT_INSTANTIATE_TYPEINFO(Gecko);
 
-}  // namespace tint
+}  // namespace tint::utils
diff --git a/src/tint/utils/slice.h b/src/tint/utils/slice.h
index 325c470..ad4dbcf 100644
--- a/src/tint/utils/slice.h
+++ b/src/tint/utils/slice.h
@@ -18,9 +18,9 @@
 #include <cstdint>
 #include <iterator>
 
-#include "src/tint/castable.h"
-#include "src/tint/traits.h"
 #include "src/tint/utils/bitcast.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/traits.h"
 
 namespace tint::utils {
 
@@ -73,8 +73,8 @@
           // or
           //   derives from TO
           (std::is_same_v<std::remove_const_t<FROM_EL>, std::remove_const_t<TO_EL>> ||
-           (IsCastable<FROM_EL, TO_EL> &&
-            (MODE == ReinterpretMode::kUnsafe || traits::IsTypeOrDerived<FROM_EL, TO_EL>)))));
+           (IsCastable<FROM_EL, TO_EL> && (MODE == ReinterpretMode::kUnsafe ||
+                                           utils::traits::IsTypeOrDerived<FROM_EL, TO_EL>)))));
 };
 
 /// Specialization of 'CanReinterpretSlice' for when TO and FROM are equal types.
diff --git a/src/tint/utils/string_stream.cc b/src/tint/utils/string_stream.cc
index f0dbe0c..fb37782 100644
--- a/src/tint/utils/string_stream.cc
+++ b/src/tint/utils/string_stream.cc
@@ -24,10 +24,6 @@
 
 StringStream::~StringStream() = default;
 
-}  // namespace tint::utils
-
-namespace tint::text {
-
 utils::StringStream& operator<<(utils::StringStream& out, CodePoint code_point) {
     if (code_point < 0x7f) {
         // See https://en.cppreference.com/w/cpp/language/escape
@@ -52,4 +48,4 @@
     return out << "'U+" << std::hex << code_point.value << "'";
 }
 
-}  // namespace tint::text
+}  // namespace tint::utils
diff --git a/src/tint/utils/string_stream.h b/src/tint/utils/string_stream.h
index 77e8fe1..7cbce09 100644
--- a/src/tint/utils/string_stream.h
+++ b/src/tint/utils/string_stream.h
@@ -23,7 +23,7 @@
 #include <string>
 #include <utility>
 
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 namespace tint::utils {
 
@@ -183,16 +183,12 @@
     std::stringstream sstream_;
 };
 
-}  // namespace tint::utils
-
-namespace tint::text {
-
 /// Writes the CodePoint to the stream.
 /// @param out the stream to write to
 /// @param codepoint the CodePoint to write
 /// @returns out so calls can be chained
 utils::StringStream& operator<<(utils::StringStream& out, CodePoint codepoint);
 
-}  // namespace tint::text
+}  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_STRING_STREAM_H_
diff --git a/src/tint/traits.h b/src/tint/utils/traits.h
similarity index 97%
rename from src/tint/traits.h
rename to src/tint/utils/traits.h
index 8d74bf7..525ed9b 100644
--- a/src/tint/traits.h
+++ b/src/tint/utils/traits.h
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_TRAITS_H_
-#define SRC_TINT_TRAITS_H_
+#ifndef SRC_TINT_UTILS_TRAITS_H_
+#define SRC_TINT_UTILS_TRAITS_H_
 
 #include <string>
 #include <tuple>
 #include <type_traits>
 #include <utility>
 
-namespace tint::traits {
+namespace tint::utils::traits {
 
 /// Convience type definition for std::decay<T>::type
 template <typename T>
@@ -183,6 +183,6 @@
     std::is_same_v<Decay<T>, std::string> || std::is_same_v<Decay<T>, std::string_view> ||
     std::is_same_v<Decay<T>, const char*>;
 
-}  // namespace tint::traits
+}  // namespace tint::utils::traits
 
-#endif  // SRC_TINT_TRAITS_H_
+#endif  // SRC_TINT_UTILS_TRAITS_H_
diff --git a/src/tint/traits_test.cc b/src/tint/utils/traits_test.cc
similarity index 98%
rename from src/tint/traits_test.cc
rename to src/tint/utils/traits_test.cc
index e86e389..578ed44 100644
--- a/src/tint/traits_test.cc
+++ b/src/tint/utils/traits_test.cc
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/traits.h"
+#include "src/tint/utils/traits.h"
 
 #include "gtest/gtest.h"
 
-namespace tint::traits {
+namespace tint::utils::traits {
 
 namespace {
 
@@ -241,4 +241,4 @@
     static_assert(std::is_same_v<std::tuple_element_t<1, sliced>, float>);
 }
 
-}  // namespace tint::traits
+}  // namespace tint::utils::traits
diff --git a/src/tint/utils/transform.h b/src/tint/utils/transform.h
index d96a4bb..9615471 100644
--- a/src/tint/utils/transform.h
+++ b/src/tint/utils/transform.h
@@ -20,7 +20,7 @@
 #include <utility>
 #include <vector>
 
-#include "src/tint/traits.h"
+#include "src/tint/utils/traits.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::utils {
diff --git a/src/tint/text/unicode.cc b/src/tint/utils/unicode.cc
similarity index 99%
rename from src/tint/text/unicode.cc
rename to src/tint/utils/unicode.cc
index ee3092b..65a003a 100644
--- a/src/tint/text/unicode.cc
+++ b/src/tint/utils/unicode.cc
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 #include <algorithm>
 
-namespace tint::text {
+namespace tint::utils {
 namespace {
 
 struct CodePointRange {
@@ -418,4 +418,4 @@
 
 }  // namespace utf8
 
-}  // namespace tint::text
+}  // namespace tint::utils
diff --git a/src/tint/text/unicode.h b/src/tint/utils/unicode.h
similarity index 93%
rename from src/tint/text/unicode.h
rename to src/tint/utils/unicode.h
index 493cdf2..083b9a2 100644
--- a/src/tint/text/unicode.h
+++ b/src/tint/utils/unicode.h
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_TEXT_UNICODE_H_
-#define SRC_TINT_TEXT_UNICODE_H_
+#ifndef SRC_TINT_UTILS_UNICODE_H_
+#define SRC_TINT_UTILS_UNICODE_H_
 
 #include <cstddef>
 #include <cstdint>
 #include <string_view>
 #include <utility>
 
-namespace tint::text {
+namespace tint::utils {
 
 /// CodePoint is a unicode code point.
 struct CodePoint {
@@ -75,6 +75,6 @@
 
 }  // namespace utf8
 
-}  // namespace tint::text
+}  // namespace tint::utils
 
-#endif  // SRC_TINT_TEXT_UNICODE_H_
+#endif  // SRC_TINT_UTILS_UNICODE_H_
diff --git a/src/tint/text/unicode_test.cc b/src/tint/utils/unicode_test.cc
similarity index 98%
rename from src/tint/text/unicode_test.cc
rename to src/tint/utils/unicode_test.cc
index 67bbead..6e74e9a 100644
--- a/src/tint/text/unicode_test.cc
+++ b/src/tint/utils/unicode_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/text/unicode.h"
+#include "src/tint/utils/unicode.h"
 
 #include <string>
 #include <vector>
@@ -22,7 +22,7 @@
 /// Helper for constructing a CodePoint
 #define C(x) CodePoint(x)
 
-namespace tint::text {
+namespace tint::utils {
 
 ////////////////////////////////////////////////////////////////////////////////
 // CodePoint character set tests
@@ -335,16 +335,16 @@
                                  {C(0x0928), 3},  // न
                                  {C(0x092e), 3},  // म
                                  {C(0x0938), 3},  // स
-                                 {C(0x094d), 3},  // ्
+                                 {C(0x094d), 3},  // ् //
                                  {C(0x0924), 3},  // त
-                                 {C(0x0947), 3},  // े
+                                 {C(0x0947), 3},  // े //
                                  {C(' '), 1},
                                  {C(0x0926), 3},  // द
-                                 {C(0x0941), 3},  // ु
+                                 {C(0x0941), 3},  // ु //
                                  {C(0x0928), 3},  // न
-                                 {C(0x093f), 3},  // ि
+                                 {C(0x093f), 3},  // ि //
                                  {C(0x092f), 3},  // य
-                                 {C(0x093e), 3},  // ा
+                                 {C(0x093e), 3},  // ा //
                              },
                          }}));
 
@@ -487,4 +487,4 @@
 
 }  // namespace
 
-}  // namespace tint::text
+}  // namespace tint::utils
diff --git a/src/tint/writer/flatten_bindings_test.cc b/src/tint/writer/flatten_bindings_test.cc
index 7e53356..d1351d8 100644
--- a/src/tint/writer/flatten_bindings_test.cc
+++ b/src/tint/writer/flatten_bindings_test.cc
@@ -67,18 +67,18 @@
 
     auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[0]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint().group, 0u);
-    EXPECT_EQ(sem->BindingPoint().binding, 0u);
+    EXPECT_EQ(sem->BindingPoint()->group, 0u);
+    EXPECT_EQ(sem->BindingPoint()->binding, 0u);
 
     sem = flattened->Sem().Get<sem::GlobalVariable>(vars[1]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint().group, 0u);
-    EXPECT_EQ(sem->BindingPoint().binding, 1u);
+    EXPECT_EQ(sem->BindingPoint()->group, 0u);
+    EXPECT_EQ(sem->BindingPoint()->binding, 1u);
 
     sem = flattened->Sem().Get<sem::GlobalVariable>(vars[2]);
     ASSERT_NE(sem, nullptr);
-    EXPECT_EQ(sem->BindingPoint().group, 0u);
-    EXPECT_EQ(sem->BindingPoint().binding, 2u);
+    EXPECT_EQ(sem->BindingPoint()->group, 0u);
+    EXPECT_EQ(sem->BindingPoint()->binding, 2u);
 }
 
 TEST_F(FlattenBindingsTest, NotFlat_MultipleNamespaces) {
@@ -131,20 +131,20 @@
     for (size_t i = 0; i < num_buffers; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint().group, 0u);
-        EXPECT_EQ(sem->BindingPoint().binding, i);
+        EXPECT_EQ(sem->BindingPoint()->group, 0u);
+        EXPECT_EQ(sem->BindingPoint()->binding, i);
     }
     for (size_t i = 0; i < num_samplers; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i + num_buffers]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint().group, 0u);
-        EXPECT_EQ(sem->BindingPoint().binding, i);
+        EXPECT_EQ(sem->BindingPoint()->group, 0u);
+        EXPECT_EQ(sem->BindingPoint()->binding, i);
     }
     for (size_t i = 0; i < num_textures; ++i) {
         auto* sem = flattened->Sem().Get<sem::GlobalVariable>(vars[i + num_buffers + num_samplers]);
         ASSERT_NE(sem, nullptr);
-        EXPECT_EQ(sem->BindingPoint().group, 0u);
-        EXPECT_EQ(sem->BindingPoint().binding, i);
+        EXPECT_EQ(sem->BindingPoint()->group, 0u);
+        EXPECT_EQ(sem->BindingPoint()->binding, i);
     }
 }
 
diff --git a/src/tint/writer/glsl/generator.cc b/src/tint/writer/glsl/generator.cc
index 32f14aa..fa2e0e8 100644
--- a/src/tint/writer/glsl/generator.cc
+++ b/src/tint/writer/glsl/generator.cc
@@ -45,7 +45,8 @@
 
     // Generate the GLSL code.
     auto impl = std::make_unique<GeneratorImpl>(&sanitized_result.program, options.version);
-    result.success = impl->Generate();
+    impl->Generate();
+    result.success = impl->Diagnostics().empty();
     result.error = impl->Diagnostics().str();
     result.glsl = impl->result();
 
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 4f46e66..cd3834f 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -91,7 +91,7 @@
 const char kTempNamePrefix[] = "tint_tmp";
 
 bool last_is_break(const ast::BlockStatement* stmts) {
-    return IsAnyOf<ast::BreakStatement>(stmts->Last());
+    return utils::IsAnyOf<ast::BreakStatement>(stmts->Last());
 }
 
 bool IsRelational(tint::ast::BinaryOp op) {
@@ -257,7 +257,7 @@
 
 GeneratorImpl::~GeneratorImpl() = default;
 
-bool GeneratorImpl::Generate() {
+void GeneratorImpl::Generate() {
     {
         auto out = line();
         out << "#version " << version_.major_version << version_.minor_version << "0";
@@ -276,7 +276,7 @@
             continue;  // These are not emitted.
         }
 
-        bool ok = Switch(
+        Switch(
             decl,  //
             [&](const ast::Variable* global) { return EmitGlobalVariable(global); },
             [&](const ast::Struct* str) {
@@ -288,31 +288,24 @@
                 bool is_block = ast::HasAttribute<transform::AddBlockAttribute::BlockAttribute>(
                     str->attributes);
                 if (!has_rt_arr && !is_block) {
-                    if (!EmitStructType(current_buffer_, sem)) {
-                        return false;
-                    }
+                    EmitStructType(current_buffer_, sem);
                 }
-                return true;
             },
             [&](const ast::Function* func) {
                 if (func->IsEntryPoint()) {
-                    return EmitEntryPointFunction(func);
+                    EmitEntryPointFunction(func);
+                } else {
+                    EmitFunction(func);
                 }
-                return EmitFunction(func);
             },
             [&](const ast::Enable* enable) {
                 // Record the required extension for generating extension directive later
-                return RecordExtension(enable);
+                RecordExtension(enable);
             },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_)
                     << "unhandled module-scope declaration: " << decl->TypeInfo().name;
-                return false;
             });
-
-        if (TINT_UNLIKELY(!ok)) {
-            return false;
-        }
     }
 
     TextBuffer extensions;
@@ -341,43 +334,32 @@
         current_buffer_->Insert(helpers_, helpers_insertion_point, indent);
         helpers_insertion_point += helpers_.lines.size();
     }
-
-    return true;
 }
 
-bool GeneratorImpl::RecordExtension(const ast::Enable* enable) {
+void GeneratorImpl::RecordExtension(const ast::Enable* enable) {
     // Deal with extension node here, recording it within the generator for later emition.
 
     if (enable->HasExtension(builtin::Extension::kF16)) {
         requires_f16_extension_ = true;
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
+void GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
                                       const ast::IndexAccessorExpression* expr) {
-    if (!EmitExpression(out, expr->object)) {
-        return false;
-    }
+    EmitExpression(out, expr->object);
     out << "[";
-
-    if (!EmitExpression(out, expr->index)) {
-        return false;
-    }
+    EmitExpression(out, expr->index);
     out << "]";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
+void GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
     auto* src_type = TypeOf(expr->expr)->UnwrapRef();
     auto* dst_type = TypeOf(expr)->UnwrapRef();
 
     if (!dst_type->is_integer_scalar_or_vector() && !dst_type->is_float_scalar_or_vector()) {
         diagnostics_.add_error(diag::System::Writer,
                                "Unable to do bitcast to type " + dst_type->FriendlyName());
-        return false;
+        return;
     }
 
     if (src_type == dst_type) {
@@ -396,32 +378,21 @@
                dst_type->is_float_scalar_or_vector()) {
         out << "uintBitsToFloat";
     } else {
-        if (!EmitType(out, dst_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                      "")) {
-            return false;
-        }
+        EmitType(out, dst_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     }
     ScopedParen sp(out);
-    if (!EmitExpression(out, expr->expr)) {
-        return false;
-    }
-    return true;
+    EmitExpression(out, expr->expr);
 }
 
-bool GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
+void GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
     auto out = line();
-    if (!EmitExpression(out, stmt->lhs)) {
-        return false;
-    }
+    EmitExpression(out, stmt->lhs);
     out << " = ";
-    if (!EmitExpression(out, stmt->rhs)) {
-        return false;
-    }
+    EmitExpression(out, stmt->rhs);
     out << ";";
-    return true;
 }
 
-bool GeneratorImpl::EmitVectorRelational(utils::StringStream& out,
+void GeneratorImpl::EmitVectorRelational(utils::StringStream& out,
                                          const ast::BinaryExpression* expr) {
     switch (expr->op) {
         case ast::BinaryOp::kEqual:
@@ -446,37 +417,24 @@
             break;
     }
     ScopedParen sp(out);
-    if (!EmitExpression(out, expr->lhs)) {
-        return false;
-    }
+    EmitExpression(out, expr->lhs);
     out << ", ";
-    if (!EmitExpression(out, expr->rhs)) {
-        return false;
-    }
-    return true;
+    EmitExpression(out, expr->rhs);
 }
 
-bool GeneratorImpl::EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr) {
+void GeneratorImpl::EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr) {
     auto* bool_type = TypeOf(expr->lhs)->UnwrapRef();
     auto* uint_type = BoolTypeToUint(bool_type);
 
     // Cast result to bool scalar or vector type.
-    if (!EmitType(out, bool_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                  "")) {
-        return false;
-    }
+    EmitType(out, bool_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     ScopedParen outerCastParen(out);
     // Cast LHS to uint scalar or vector type.
-    if (!EmitType(out, uint_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                  "")) {
-        return false;
-    }
+    EmitType(out, uint_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     {
         ScopedParen innerCastParen(out);
         // Emit LHS.
-        if (!EmitExpression(out, expr->lhs)) {
-            return false;
-        }
+        EmitExpression(out, expr->lhs);
     }
     // Emit operator.
     if (expr->op == ast::BinaryOp::kAnd) {
@@ -485,98 +443,78 @@
         out << " | ";
     } else {
         TINT_ICE(Writer, diagnostics_) << "unexpected binary op: " << FriendlyName(expr->op);
-        return false;
+        return;
     }
+
     // Cast RHS to uint scalar or vector type.
-    if (!EmitType(out, uint_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                  "")) {
-        return false;
-    }
+    EmitType(out, uint_type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     {
         ScopedParen innerCastParen(out);
         // Emit RHS.
-        if (!EmitExpression(out, expr->rhs)) {
-            return false;
-        }
+        EmitExpression(out, expr->rhs);
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr) {
+void GeneratorImpl::EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr) {
     std::string fn;
     auto* ret_ty = TypeOf(expr)->UnwrapRef();
     auto* lhs_ty = TypeOf(expr->lhs)->UnwrapRef();
     auto* rhs_ty = TypeOf(expr->rhs)->UnwrapRef();
-    fn = utils::GetOrCreate(
-        float_modulo_funcs_, BinaryOperandType{{lhs_ty, rhs_ty}}, [&]() -> std::string {
-            TextBuffer b;
-            TINT_DEFER(helpers_.Append(b));
+    fn = utils::GetOrCreate(float_modulo_funcs_, BinaryOperandType{{lhs_ty, rhs_ty}},
+                            [&]() -> std::string {
+                                TextBuffer b;
+                                TINT_DEFER(helpers_.Append(b));
 
-            auto fn_name = UniqueIdentifier("tint_float_modulo");
-            std::vector<std::string> parameter_names;
-            {
-                auto decl = line(&b);
-                if (!EmitTypeAndName(decl, ret_ty, builtin::AddressSpace::kUndefined,
-                                     builtin::Access::kUndefined, fn_name)) {
-                    return "";
-                }
-                {
-                    ScopedParen sp(decl);
-                    const auto* ty = TypeOf(expr->lhs)->UnwrapRef();
-                    if (!EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
-                                         builtin::Access::kUndefined, "lhs")) {
-                        return "";
-                    }
-                    decl << ", ";
-                    ty = TypeOf(expr->rhs)->UnwrapRef();
-                    if (!EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
-                                         builtin::Access::kUndefined, "rhs")) {
-                        return "";
-                    }
-                }
-                decl << " {";
-            }
-            {
-                ScopedIndent si(&b);
-                line(&b) << "return (lhs - rhs * trunc(lhs / rhs));";
-            }
-            line(&b) << "}";
-            line(&b);
-            return fn_name;
-        });
-
-    if (fn.empty()) {
-        return false;
-    }
+                                auto fn_name = UniqueIdentifier("tint_float_modulo");
+                                std::vector<std::string> parameter_names;
+                                {
+                                    auto decl = line(&b);
+                                    EmitTypeAndName(decl, ret_ty, builtin::AddressSpace::kUndefined,
+                                                    builtin::Access::kUndefined, fn_name);
+                                    {
+                                        ScopedParen sp(decl);
+                                        const auto* ty = TypeOf(expr->lhs)->UnwrapRef();
+                                        EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
+                                                        builtin::Access::kUndefined, "lhs");
+                                        decl << ", ";
+                                        ty = TypeOf(expr->rhs)->UnwrapRef();
+                                        EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
+                                                        builtin::Access::kUndefined, "rhs");
+                                    }
+                                    decl << " {";
+                                }
+                                {
+                                    ScopedIndent si(&b);
+                                    line(&b) << "return (lhs - rhs * trunc(lhs / rhs));";
+                                }
+                                line(&b) << "}";
+                                line(&b);
+                                return fn_name;
+                            });
 
     // Call the helper
     out << fn;
     {
         ScopedParen sp(out);
-        if (!EmitExpression(out, expr->lhs)) {
-            return false;
-        }
+        EmitExpression(out, expr->lhs);
         out << ", ";
-        if (!EmitExpression(out, expr->rhs)) {
-            return false;
-        }
+        EmitExpression(out, expr->rhs);
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
+void GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
     if (IsRelational(expr->op) && !TypeOf(expr->lhs)->UnwrapRef()->is_scalar()) {
-        return EmitVectorRelational(out, expr);
+        EmitVectorRelational(out, expr);
+        return;
     }
+
     if (expr->op == ast::BinaryOp::kLogicalAnd || expr->op == ast::BinaryOp::kLogicalOr) {
         auto name = UniqueIdentifier(kTempNamePrefix);
 
         {
             auto pre = line();
             pre << "bool " << name << " = ";
-            if (!EmitExpression(pre, expr->lhs)) {
-                return false;
-            }
+            EmitExpression(pre, expr->lhs);
             pre << ";";
         }
 
@@ -590,32 +528,31 @@
             ScopedIndent si(this);
             auto pre = line();
             pre << name << " = ";
-            if (!EmitExpression(pre, expr->rhs)) {
-                return false;
-            }
+            EmitExpression(pre, expr->rhs);
             pre << ";";
         }
 
         line() << "}";
 
         out << "(" << name << ")";
-        return true;
+        return;
     }
+
     if ((expr->op == ast::BinaryOp::kAnd || expr->op == ast::BinaryOp::kOr) &&
         TypeOf(expr->lhs)->UnwrapRef()->is_bool_scalar_or_vector()) {
-        return EmitBitwiseBoolOp(out, expr);
+        EmitBitwiseBoolOp(out, expr);
+        return;
     }
 
     if (expr->op == ast::BinaryOp::kModulo &&
         (TypeOf(expr->lhs)->UnwrapRef()->is_float_scalar_or_vector() ||
          TypeOf(expr->rhs)->UnwrapRef()->is_float_scalar_or_vector())) {
-        return EmitFloatModulo(out, expr);
+        EmitFloatModulo(out, expr);
+        return;
     }
 
     ScopedParen sp(out);
-    if (!EmitExpression(out, expr->lhs)) {
-        return false;
-    }
+    EmitExpression(out, expr->lhs);
     out << " ";
 
     switch (expr->op) {
@@ -632,7 +569,7 @@
         case ast::BinaryOp::kLogicalOr: {
             // These are both handled above.
             TINT_UNREACHABLE(Writer, diagnostics_);
-            return false;
+            return;
         }
         case ast::BinaryOp::kEqual:
             out << "==";
@@ -656,10 +593,6 @@
             out << "<<";
             break;
         case ast::BinaryOp::kShiftRight:
-            // TODO(dsinclair): MSL is based on C++14, and >> in C++14 has
-            // implementation-defined behaviour for negative LHS.  We may have to
-            // generate extra code to implement WGSL-specified behaviour for negative
-            // LHS.
             out << R"(>>)";
             break;
 
@@ -680,71 +613,55 @@
             break;
         case ast::BinaryOp::kNone:
             diagnostics_.add_error(diag::System::Writer, "missing binary operation type");
-            return false;
+            return;
     }
     out << " ";
-
-    if (!EmitExpression(out, expr->rhs)) {
-        return false;
-    }
-
-    return true;
+    EmitExpression(out, expr->rhs);
 }
 
-bool GeneratorImpl::EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
+void GeneratorImpl::EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
     for (auto* s : stmts) {
-        if (!EmitStatement(s)) {
-            return false;
-        }
+        EmitStatement(s);
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitStatementsWithIndent(utils::VectorRef<const ast::Statement*> stmts) {
+void GeneratorImpl::EmitStatementsWithIndent(utils::VectorRef<const ast::Statement*> stmts) {
     ScopedIndent si(this);
-    return EmitStatements(stmts);
+    EmitStatements(stmts);
 }
 
-bool GeneratorImpl::EmitBlock(const ast::BlockStatement* stmt) {
+void GeneratorImpl::EmitBlock(const ast::BlockStatement* stmt) {
     line() << "{";
-    if (!EmitStatementsWithIndent(stmt->statements)) {
-        return false;
-    }
+    EmitStatementsWithIndent(stmt->statements);
     line() << "}";
-    return true;
 }
 
-bool GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
+void GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
     line() << "break;";
-    return true;
 }
 
-bool GeneratorImpl::EmitBreakIf(const ast::BreakIfStatement* b) {
+void GeneratorImpl::EmitBreakIf(const ast::BreakIfStatement* b) {
     auto out = line();
     out << "if (";
-    if (!EmitExpression(out, b->condition)) {
-        return false;
-    }
+    EmitExpression(out, b->condition);
     out << ") { break; }";
-    return true;
 }
 
-bool GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
+void GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* call = builder_.Sem().Get<sem::Call>(expr);
-    return Switch(
+    Switch(
         call->Target(),  //
-        [&](const sem::Function* fn) { return EmitFunctionCall(out, call, fn); },
-        [&](const sem::Builtin* builtin) { return EmitBuiltinCall(out, call, builtin); },
-        [&](const sem::ValueConversion* conv) { return EmitValueConversion(out, call, conv); },
-        [&](const sem::ValueConstructor* ctor) { return EmitValueConstructor(out, call, ctor); },
+        [&](const sem::Function* fn) { EmitFunctionCall(out, call, fn); },
+        [&](const sem::Builtin* builtin) { EmitBuiltinCall(out, call, builtin); },
+        [&](const sem::ValueConversion* conv) { EmitValueConversion(out, call, conv); },
+        [&](const sem::ValueConstructor* ctor) { EmitValueConstructor(out, call, ctor); },
         [&](Default) {
             TINT_ICE(Writer, diagnostics_)
                 << "unhandled call target: " << call->Target()->TypeInfo().name;
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitFunctionCall(utils::StringStream& out,
+void GeneratorImpl::EmitFunctionCall(utils::StringStream& out,
                                      const sem::Call* call,
                                      const sem::Function* fn) {
     const auto& args = call->Arguments();
@@ -760,114 +677,84 @@
         }
         first = false;
 
-        if (!EmitExpression(out, arg->Declaration())) {
-            return false;
-        }
+        EmitExpression(out, arg->Declaration());
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitBuiltinCall(utils::StringStream& out,
+void GeneratorImpl::EmitBuiltinCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     auto* expr = call->Declaration();
     if (builtin->IsTexture()) {
-        return EmitTextureCall(out, call, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kCountOneBits) {
-        return EmitCountOneBitsCall(out, expr);
-    }
-    if (builtin->Type() == builtin::Function::kSelect) {
-        return EmitSelectCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kDot) {
-        return EmitDotCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kModf) {
-        return EmitModfCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kFrexp) {
-        return EmitFrexpCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kDegrees) {
-        return EmitDegreesCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kRadians) {
-        return EmitRadiansCall(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kQuantizeToF16) {
-        return EmitQuantizeToF16Call(out, expr, builtin);
-    }
-    if (builtin->Type() == builtin::Function::kArrayLength) {
-        return EmitArrayLength(out, expr);
-    }
-    if (builtin->Type() == builtin::Function::kExtractBits) {
-        return EmitExtractBits(out, expr);
-    }
-    if (builtin->Type() == builtin::Function::kInsertBits) {
-        return EmitInsertBits(out, expr);
-    }
-    if (builtin->Type() == builtin::Function::kFma && version_.IsES()) {
-        return EmitEmulatedFMA(out, expr);
-    }
-    if (builtin->Type() == builtin::Function::kAbs &&
-        TypeOf(expr->args[0])->UnwrapRef()->is_unsigned_integer_scalar_or_vector()) {
+        EmitTextureCall(out, call, builtin);
+    } else if (builtin->Type() == builtin::Function::kCountOneBits) {
+        EmitCountOneBitsCall(out, expr);
+    } else if (builtin->Type() == builtin::Function::kSelect) {
+        EmitSelectCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kDot) {
+        EmitDotCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kModf) {
+        EmitModfCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kFrexp) {
+        EmitFrexpCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kDegrees) {
+        EmitDegreesCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kRadians) {
+        EmitRadiansCall(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kQuantizeToF16) {
+        EmitQuantizeToF16Call(out, expr, builtin);
+    } else if (builtin->Type() == builtin::Function::kArrayLength) {
+        EmitArrayLength(out, expr);
+    } else if (builtin->Type() == builtin::Function::kExtractBits) {
+        EmitExtractBits(out, expr);
+    } else if (builtin->Type() == builtin::Function::kInsertBits) {
+        EmitInsertBits(out, expr);
+    } else if (builtin->Type() == builtin::Function::kFma && version_.IsES()) {
+        EmitEmulatedFMA(out, expr);
+    } else if (builtin->Type() == builtin::Function::kAbs &&
+               TypeOf(expr->args[0])->UnwrapRef()->is_unsigned_integer_scalar_or_vector()) {
         // GLSL does not support abs() on unsigned arguments. However, it's a no-op.
-        return EmitExpression(out, expr->args[0]);
-    }
-    if ((builtin->Type() == builtin::Function::kAny ||
-         builtin->Type() == builtin::Function::kAll) &&
-        TypeOf(expr->args[0])->UnwrapRef()->is_scalar()) {
+        EmitExpression(out, expr->args[0]);
+    } else if ((builtin->Type() == builtin::Function::kAny ||
+                builtin->Type() == builtin::Function::kAll) &&
+               TypeOf(expr->args[0])->UnwrapRef()->is_scalar()) {
         // GLSL does not support any() or all() on scalar arguments. It's a no-op.
-        return EmitExpression(out, expr->args[0]);
-    }
-    if (builtin->IsBarrier()) {
-        return EmitBarrierCall(out, builtin);
-    }
-    if (builtin->IsAtomic()) {
-        return EmitWorkgroupAtomicCall(out, expr, builtin);
-    }
-    auto name = generate_builtin_name(builtin);
-    if (name.empty()) {
-        return false;
-    }
-
-    out << name;
-    ScopedParen sp(out);
-
-    bool first = true;
-    for (auto* arg : call->Arguments()) {
-        if (!first) {
-            out << ", ";
+        EmitExpression(out, expr->args[0]);
+    } else if (builtin->IsBarrier()) {
+        EmitBarrierCall(out, builtin);
+    } else if (builtin->IsAtomic()) {
+        EmitWorkgroupAtomicCall(out, expr, builtin);
+    } else {
+        auto name = generate_builtin_name(builtin);
+        if (name.empty()) {
+            return;
         }
-        first = false;
 
-        if (!EmitExpression(out, arg->Declaration())) {
-            return false;
+        out << name;
+        ScopedParen sp(out);
+
+        bool first = true;
+        for (auto* arg : call->Arguments()) {
+            if (!first) {
+                out << ", ";
+            }
+            first = false;
+
+            EmitExpression(out, arg->Declaration());
         }
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitValueConversion(utils::StringStream& out,
+void GeneratorImpl::EmitValueConversion(utils::StringStream& out,
                                         const sem::Call* call,
                                         const sem::ValueConversion* conv) {
-    if (!EmitType(out, conv->Target(), builtin::AddressSpace::kUndefined,
-                  builtin::Access::kReadWrite, "")) {
-        return false;
-    }
+    EmitType(out, conv->Target(), builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
+             "");
     ScopedParen sp(out);
-
-    if (!EmitExpression(out, call->Arguments()[0]->Declaration())) {
-        return false;
-    }
-
-    return true;
+    EmitExpression(out, call->Arguments()[0]->Declaration());
 }
 
-bool GeneratorImpl::EmitValueConstructor(utils::StringStream& out,
+void GeneratorImpl::EmitValueConstructor(utils::StringStream& out,
                                          const sem::Call* call,
                                          const sem::ValueConstructor* ctor) {
     auto* type = ctor->ReturnType();
@@ -875,12 +762,11 @@
     // If the value constructor is empty then we need to construct with the zero value for all
     // components.
     if (call->Arguments().IsEmpty()) {
-        return EmitZeroValue(out, type);
+        EmitZeroValue(out, type);
+        return;
     }
 
-    if (!EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "")) {
-        return false;
-    }
+    EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
     ScopedParen sp(out);
 
     bool first = true;
@@ -890,15 +776,11 @@
         }
         first = false;
 
-        if (!EmitExpression(out, arg->Declaration())) {
-            return false;
-        }
+        EmitExpression(out, arg->Declaration());
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitWorkgroupAtomicCall(utils::StringStream& out,
+void GeneratorImpl::EmitWorkgroupAtomicCall(utils::StringStream& out,
                                             const ast::CallExpression* expr,
                                             const sem::Builtin* builtin) {
     auto call = [&](const char* name) {
@@ -910,12 +792,10 @@
                 if (i > 0) {
                     out << ", ";
                 }
-                if (!EmitExpression(out, arg)) {
-                    return false;
-                }
+                EmitExpression(out, arg);
             }
         }
-        return true;
+        return;
     };
 
     switch (builtin->Type()) {
@@ -925,20 +805,16 @@
             out << "atomicOr";
             {
                 ScopedParen sp(out);
-                if (!EmitExpression(out, expr->args[0])) {
-                    return false;
-                }
+                EmitExpression(out, expr->args[0]);
                 out << ", 0";
                 if (builtin->ReturnType()->Is<type::U32>()) {
                     out << "u";
                 }
             }
-            return true;
+            return;
         }
         case builtin::Function::kAtomicCompareExchangeWeak: {
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
-                return false;
-            }
+            EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
 
             auto* dest = expr->args[0];
             auto* compare_value = expr->args[1];
@@ -948,10 +824,8 @@
 
             {
                 auto pre = line();
-                if (!EmitTypeAndName(pre, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
-                                     builtin::Access::kUndefined, result)) {
-                    return false;
-                }
+                EmitTypeAndName(pre, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
+                                builtin::Access::kUndefined, result);
                 pre << ";";
             }
             {
@@ -959,174 +833,138 @@
                 pre << result << ".old_value = atomicCompSwap";
                 {
                     ScopedParen sp(pre);
-                    if (!EmitExpression(pre, dest)) {
-                        return false;
-                    }
+                    EmitExpression(pre, dest);
                     pre << ", ";
-                    if (!EmitExpression(pre, compare_value)) {
-                        return false;
-                    }
+                    EmitExpression(pre, compare_value);
                     pre << ", ";
-                    if (!EmitExpression(pre, value)) {
-                        return false;
-                    }
+                    EmitExpression(pre, value);
                 }
                 pre << ";";
             }
             {
                 auto pre = line();
                 pre << result << ".exchanged = " << result << ".old_value == ";
-                if (!EmitExpression(pre, compare_value)) {
-                    return false;
-                }
+                EmitExpression(pre, compare_value);
                 pre << ";";
             }
 
             out << result;
-            return true;
+            return;
         }
 
         case builtin::Function::kAtomicAdd:
         case builtin::Function::kAtomicSub:
-            return call("atomicAdd");
+            call("atomicAdd");
+            return;
 
         case builtin::Function::kAtomicMax:
-            return call("atomicMax");
+            call("atomicMax");
+            return;
 
         case builtin::Function::kAtomicMin:
-            return call("atomicMin");
+            call("atomicMin");
+            return;
 
         case builtin::Function::kAtomicAnd:
-            return call("atomicAnd");
+            call("atomicAnd");
+            return;
 
         case builtin::Function::kAtomicOr:
-            return call("atomicOr");
+            call("atomicOr");
+            return;
 
         case builtin::Function::kAtomicXor:
-            return call("atomicXor");
+            call("atomicXor");
+            return;
 
         case builtin::Function::kAtomicExchange:
         case builtin::Function::kAtomicStore:
             // GLSL does not have an atomicStore, so we emulate it with
             // atomicExchange.
-            return call("atomicExchange");
+            call("atomicExchange");
+            return;
 
         default:
             break;
     }
 
     TINT_UNREACHABLE(Writer, diagnostics_) << "unsupported atomic builtin: " << builtin->Type();
-    return false;
 }
 
-bool GeneratorImpl::EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr) {
+void GeneratorImpl::EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "uint(";
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << ".length())";
-    return true;
 }
 
-bool GeneratorImpl::EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr) {
+void GeneratorImpl::EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "bitfieldExtract(";
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << ", int(";
-    if (!EmitExpression(out, expr->args[1])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[1]);
     out << "), int(";
-    if (!EmitExpression(out, expr->args[2])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[2]);
     out << "))";
-    return true;
 }
 
-bool GeneratorImpl::EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr) {
+void GeneratorImpl::EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "bitfieldInsert(";
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << ", ";
-    if (!EmitExpression(out, expr->args[1])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[1]);
     out << ", int(";
-    if (!EmitExpression(out, expr->args[2])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[2]);
     out << "), int(";
-    if (!EmitExpression(out, expr->args[3])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[3]);
     out << "))";
-    return true;
 }
 
-bool GeneratorImpl::EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr) {
+void GeneratorImpl::EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "((";
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << ") * (";
-    if (!EmitExpression(out, expr->args[1])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[1]);
     out << ") + (";
-    if (!EmitExpression(out, expr->args[2])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[2]);
     out << "))";
-    return true;
 }
 
-bool GeneratorImpl::EmitCountOneBitsCall(utils::StringStream& out,
+void GeneratorImpl::EmitCountOneBitsCall(utils::StringStream& out,
                                          const ast::CallExpression* expr) {
     // GLSL's bitCount returns an integer type, so cast it to the appropriate
     // unsigned type.
-    if (!EmitType(out, TypeOf(expr)->UnwrapRef(), builtin::AddressSpace::kUndefined,
-                  builtin::Access::kReadWrite, "")) {
-        return false;
-    }
+    EmitType(out, TypeOf(expr)->UnwrapRef(), builtin::AddressSpace::kUndefined,
+             builtin::Access::kReadWrite, "");
     out << "(bitCount(";
-
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << "))";
-    return true;
 }
 
-bool GeneratorImpl::EmitSelectCall(utils::StringStream& out,
+void GeneratorImpl::EmitSelectCall(utils::StringStream& out,
                                    const ast::CallExpression* expr,
                                    const sem::Builtin* builtin) {
     // GLSL does not support ternary expressions with a bool vector conditional,
     // so polyfill with a helper.
     if (auto* vec = builtin->Parameters()[2]->Type()->As<type::Vector>()) {
-        return CallBuiltinHelper(
-            out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
-                auto l = line(b);
-                l << "  return ";
-                if (!EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
-                              builtin::Access::kUndefined, "")) {
-                    return false;
-                }
-                {
-                    ScopedParen sp(l);
-                    for (uint32_t i = 0; i < vec->Width(); i++) {
-                        if (i > 0) {
-                            l << ", ";
-                        }
-                        l << params[2] << "[" << i << "] ? " << params[1] << "[" << i
-                          << "] : " << params[0] << "[" << i << "]";
-                    }
-                }
-                l << ";";
-                return true;
-            });
+        CallBuiltinHelper(out, expr, builtin,
+                          [&](TextBuffer* b, const std::vector<std::string>& params) {
+                              auto l = line(b);
+                              l << "  return ";
+                              EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
+                                       builtin::Access::kUndefined, "");
+                              {
+                                  ScopedParen sp(l);
+                                  for (uint32_t i = 0; i < vec->Width(); i++) {
+                                      if (i > 0) {
+                                          l << ", ";
+                                      }
+                                      l << params[2] << "[" << i << "] ? " << params[1] << "[" << i
+                                        << "] : " << params[0] << "[" << i << "]";
+                                  }
+                              }
+                              l << ";";
+                          });
+        return;
     }
 
     auto* expr_false = expr->args[0];
@@ -1134,26 +972,15 @@
     auto* expr_cond = expr->args[2];
 
     ScopedParen paren(out);
-    if (!EmitExpression(out, expr_cond)) {
-        return false;
-    }
+    EmitExpression(out, expr_cond);
 
     out << " ? ";
-
-    if (!EmitExpression(out, expr_true)) {
-        return false;
-    }
-
+    EmitExpression(out, expr_true);
     out << " : ";
-
-    if (!EmitExpression(out, expr_false)) {
-        return false;
-    }
-
-    return true;
+    EmitExpression(out, expr_false);
 }
 
-bool GeneratorImpl::EmitDotCall(utils::StringStream& out,
+void GeneratorImpl::EmitDotCall(utils::StringStream& out,
                                 const ast::CallExpression* expr,
                                 const sem::Builtin* builtin) {
     auto* vec_ty = builtin->Parameters()[0]->Type()->As<type::Vector>();
@@ -1170,28 +997,18 @@
             std::string v;
             {
                 utils::StringStream s;
-                if (!EmitType(s, vec_ty->type(), builtin::AddressSpace::kUndefined,
-                              builtin::Access::kRead, "")) {
-                    return "";
-                }
+                EmitType(s, vec_ty->type(), builtin::AddressSpace::kUndefined,
+                         builtin::Access::kRead, "");
                 v = s.str();
             }
             {  // (u)int tint_int_dot([i|u]vecN a, [i|u]vecN b) {
                 auto l = line(&b);
-                if (!EmitType(l, vec_ty->type(), builtin::AddressSpace::kUndefined,
-                              builtin::Access::kRead, "")) {
-                    return "";
-                }
+                EmitType(l, vec_ty->type(), builtin::AddressSpace::kUndefined,
+                         builtin::Access::kRead, "");
                 l << " " << fn_name << "(";
-                if (!EmitType(l, vec_ty, builtin::AddressSpace::kUndefined, builtin::Access::kRead,
-                              "")) {
-                    return "";
-                }
+                EmitType(l, vec_ty, builtin::AddressSpace::kUndefined, builtin::Access::kRead, "");
                 l << " a, ";
-                if (!EmitType(l, vec_ty, builtin::AddressSpace::kUndefined, builtin::Access::kRead,
-                              "")) {
-                    return "";
-                }
+                EmitType(l, vec_ty, builtin::AddressSpace::kUndefined, builtin::Access::kRead, "");
                 l << " b) {";
             }
             {
@@ -1208,135 +1025,114 @@
             line(&b) << "}";
             return fn_name;
         });
-        if (fn.empty()) {
-            return false;
-        }
     }
 
     out << fn;
     ScopedParen sp(out);
 
-    if (!EmitExpression(out, expr->args[0])) {
-        return false;
-    }
+    EmitExpression(out, expr->args[0]);
     out << ", ";
-    if (!EmitExpression(out, expr->args[1])) {
-        return false;
-    }
-    return true;
+    EmitExpression(out, expr->args[1]);
 }
 
-bool GeneratorImpl::EmitModfCall(utils::StringStream& out,
+void GeneratorImpl::EmitModfCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin) {
     TINT_ASSERT(Writer, expr->args.Length() == 1);
-    return CallBuiltinHelper(
-        out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
-            // Emit the builtin return type unique to this overload. This does not
-            // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
-                return false;
-            }
+    CallBuiltinHelper(out, expr, builtin,
+                      [&](TextBuffer* b, const std::vector<std::string>& params) {
+                          // Emit the builtin return type unique to this overload. This does not
+                          // exist in the AST, so it will not be generated in Generate().
+                          EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
 
-            {
-                auto l = line(b);
-                if (!EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
-                              builtin::Access::kUndefined, "")) {
-                    return false;
-                }
-                l << " result;";
-            }
-            line(b) << "result.fract = modf(" << params[0] << ", result.whole);";
-            line(b) << "return result;";
-            return true;
-        });
+                          {
+                              auto l = line(b);
+                              EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
+                                       builtin::Access::kUndefined, "");
+                              l << " result;";
+                          }
+                          line(b) << "result.fract = modf(" << params[0] << ", result.whole);";
+                          line(b) << "return result;";
+                      });
 }
 
-bool GeneratorImpl::EmitFrexpCall(utils::StringStream& out,
+void GeneratorImpl::EmitFrexpCall(utils::StringStream& out,
                                   const ast::CallExpression* expr,
                                   const sem::Builtin* builtin) {
     TINT_ASSERT(Writer, expr->args.Length() == 1);
-    return CallBuiltinHelper(
-        out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
-            // Emit the builtin return type unique to this overload. This does not
-            // exist in the AST, so it will not be generated in Generate().
-            if (!EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>())) {
-                return false;
-            }
+    CallBuiltinHelper(out, expr, builtin,
+                      [&](TextBuffer* b, const std::vector<std::string>& params) {
+                          // Emit the builtin return type unique to this overload. This does not
+                          // exist in the AST, so it will not be generated in Generate().
+                          EmitStructType(&helpers_, builtin->ReturnType()->As<sem::Struct>());
 
-            {
-                auto l = line(b);
-                if (!EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
-                              builtin::Access::kUndefined, "")) {
-                    return false;
-                }
-                l << " result;";
-            }
-            line(b) << "result.fract = frexp(" << params[0] << ", result.exp);";
-            line(b) << "return result;";
-            return true;
-        });
+                          {
+                              auto l = line(b);
+                              EmitType(l, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
+                                       builtin::Access::kUndefined, "");
+                              l << " result;";
+                          }
+                          line(b) << "result.fract = frexp(" << params[0] << ", result.exp);";
+                          line(b) << "return result;";
+                      });
 }
 
-bool GeneratorImpl::EmitDegreesCall(utils::StringStream& out,
+void GeneratorImpl::EmitDegreesCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     auto* return_elem_type = type::Type::DeepestElementOf(builtin->ReturnType());
     const std::string suffix = Is<type::F16>(return_elem_type) ? "hf" : "f";
-    return CallBuiltinHelper(out, expr, builtin,
-                             [&](TextBuffer* b, const std::vector<std::string>& params) {
-                                 line(b) << "return " << params[0] << " * " << std::setprecision(20)
-                                         << sem::kRadToDeg << suffix << ";";
-                                 return true;
-                             });
+    CallBuiltinHelper(out, expr, builtin,
+                      [&](TextBuffer* b, const std::vector<std::string>& params) {
+                          line(b) << "return " << params[0] << " * " << std::setprecision(20)
+                                  << sem::kRadToDeg << suffix << ";";
+                      });
 }
 
-bool GeneratorImpl::EmitRadiansCall(utils::StringStream& out,
+void GeneratorImpl::EmitRadiansCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     auto* return_elem_type = type::Type::DeepestElementOf(builtin->ReturnType());
     const std::string suffix = Is<type::F16>(return_elem_type) ? "hf" : "f";
-    return CallBuiltinHelper(out, expr, builtin,
-                             [&](TextBuffer* b, const std::vector<std::string>& params) {
-                                 line(b) << "return " << params[0] << " * " << std::setprecision(20)
-                                         << sem::kDegToRad << suffix << ";";
-                                 return true;
-                             });
+    CallBuiltinHelper(out, expr, builtin,
+                      [&](TextBuffer* b, const std::vector<std::string>& params) {
+                          line(b) << "return " << params[0] << " * " << std::setprecision(20)
+                                  << sem::kDegToRad << suffix << ";";
+                      });
 }
 
-bool GeneratorImpl::EmitQuantizeToF16Call(utils::StringStream& out,
+void GeneratorImpl::EmitQuantizeToF16Call(utils::StringStream& out,
                                           const ast::CallExpression* expr,
                                           const sem::Builtin* builtin) {
     // Emulate by casting to f16 and back again.
-    return CallBuiltinHelper(
+    CallBuiltinHelper(
         out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
             const auto v = params[0];
             if (auto* vec = builtin->ReturnType()->As<type::Vector>()) {
                 switch (vec->Width()) {
                     case 2: {
                         line(b) << "return unpackHalf2x16(packHalf2x16(" << v << "));";
-                        return true;
+                        return;
                     }
                     case 3: {
                         line(b) << "return vec3(";
                         line(b) << "  unpackHalf2x16(packHalf2x16(" << v << ".xy)),";
                         line(b) << "  unpackHalf2x16(packHalf2x16(" << v << ".zz)).x);";
-                        return true;
+                        return;
                     }
                     default: {
                         line(b) << "return vec4(";
                         line(b) << "  unpackHalf2x16(packHalf2x16(" << v << ".xy)),";
                         line(b) << "  unpackHalf2x16(packHalf2x16(" << v << ".zw)));";
-                        return true;
+                        return;
                     }
                 }
             }
             line(b) << "return unpackHalf2x16(packHalf2x16(vec2(" << v << "))).x;";
-            return true;
         });
 }
 
-bool GeneratorImpl::EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin) {
+void GeneratorImpl::EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin) {
     // TODO(crbug.com/tint/661): Combine sequential barriers to a single
     // instruction.
     if (builtin->Type() == builtin::Function::kWorkgroupBarrier) {
@@ -1346,9 +1142,7 @@
     } else {
         TINT_UNREACHABLE(Writer, diagnostics_)
             << "unexpected barrier builtin type " << builtin::str(builtin->Type());
-        return false;
     }
-    return true;
 }
 
 const ast::Expression* GeneratorImpl::CreateF32Zero(const sem::Statement* stmt) {
@@ -1361,7 +1155,7 @@
     return zero;
 }
 
-bool GeneratorImpl::EmitTextureCall(utils::StringStream& out,
+void GeneratorImpl::EmitTextureCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     using Usage = sem::ParameterUsage;
@@ -1379,7 +1173,7 @@
     auto* texture = arg(Usage::kTexture);
     if (TINT_UNLIKELY(!texture)) {
         TINT_ICE(Writer, diagnostics_) << "missing texture argument";
-        return false;
+        return;
     }
 
     auto* texture_type = TypeOf(texture)->UnwrapRef()->As<type::Texture>();
@@ -1407,11 +1201,13 @@
     auto emit_expr_as_signed = [&](const ast::Expression* e) {
         auto* ty = TypeOf(e)->UnwrapRef();
         if (!ty->is_unsigned_integer_scalar_or_vector()) {
-            return EmitExpression(out, e);
+            EmitExpression(out, e);
+            return;
         }
         emit_signed_int_type(ty);
         ScopedParen sp(out);
-        return EmitExpression(out, e);
+        EmitExpression(out, e);
+        return;
     };
 
     switch (builtin->Type()) {
@@ -1427,9 +1223,7 @@
             } else {
                 out << "textureSize(";
             }
-            if (!EmitExpression(out, texture)) {
-                return false;
-            }
+            EmitExpression(out, texture);
 
             // The LOD parameter is mandatory on textureSize() for non-multisampled
             // textures.
@@ -1438,9 +1232,7 @@
                 !texture_type->Is<type::DepthMultisampledTexture>()) {
                 out << ", ";
                 if (auto* level_arg = arg(Usage::kLevel)) {
-                    if (!emit_expr_as_signed(level_arg)) {
-                        return false;
-                    }
+                    emit_expr_as_signed(level_arg);
                 } else {
                     out << "0";
                 }
@@ -1452,7 +1244,7 @@
                 texture_type->dim() == type::TextureDimension::kCubeArray) {
                 out << ".xy";
             }
-            return true;
+            return;
         }
         case builtin::Function::kTextureNumLayers: {
             // textureNumLayers() returns an unsigned scalar in WGSL.
@@ -1468,9 +1260,8 @@
             }
             // textureSize() on sampler2dArray returns the array size in the
             // final component, so return it
-            if (!EmitExpression(out, texture)) {
-                return false;
-            }
+            EmitExpression(out, texture);
+
             // The LOD parameter is mandatory on textureSize() for non-multisampled
             // textures.
             if (!texture_type->Is<type::StorageTexture>() &&
@@ -1478,15 +1269,13 @@
                 !texture_type->Is<type::DepthMultisampledTexture>()) {
                 out << ", ";
                 if (auto* level_arg = arg(Usage::kLevel)) {
-                    if (!emit_expr_as_signed(level_arg)) {
-                        return false;
-                    }
+                    emit_expr_as_signed(level_arg);
                 } else {
                     out << "0";
                 }
             }
             out << ").z";
-            return true;
+            return;
         }
         case builtin::Function::kTextureNumLevels: {
             // textureNumLevels() returns an unsigned scalar in WGSL.
@@ -1496,11 +1285,9 @@
             ScopedParen sp(out);
 
             out << "textureQueryLevels(";
-            if (!EmitExpression(out, texture)) {
-                return false;
-            }
+            EmitExpression(out, texture);
             out << ")";
-            return true;
+            return;
         }
         case builtin::Function::kTextureNumSamples: {
             // textureNumSamples() returns an unsigned scalar in WGSL.
@@ -1510,11 +1297,9 @@
             ScopedParen sp(out);
 
             out << "textureSamples(";
-            if (!EmitExpression(out, texture)) {
-                return false;
-            }
+            EmitExpression(out, texture);
             out << ")";
-            return true;
+            return;
         }
         default:
             break;
@@ -1561,7 +1346,7 @@
             diagnostics_.add_error(diag::System::Writer,
                                    "Internal compiler error: Unhandled texture builtin '" +
                                        std::string(builtin->str()) + "'");
-            return false;
+            return;
     }
 
     if (builtin->Signature().IndexOf(sem::ParameterUsage::kOffset) >= 0) {
@@ -1569,17 +1354,13 @@
     }
 
     out << "(";
-
-    if (!EmitExpression(out, texture)) {
-        return false;
-    }
-
+    EmitExpression(out, texture);
     out << ", ";
 
     auto* param_coords = arg(Usage::kCoords);
     if (TINT_UNLIKELY(!param_coords)) {
         TINT_ICE(Writer, diagnostics_) << "missing coords argument";
-        return false;
+        return;
     }
 
     if (auto* array_index = arg(Usage::kArrayIndex)) {
@@ -1604,9 +1385,7 @@
         param_coords = AppendVector(&builder_, param_coords, depth_ref)->Declaration();
     }
 
-    if (!emit_expr_as_signed(param_coords)) {
-        return false;
-    }
+    emit_expr_as_signed(param_coords);
 
     for (auto usage : {Usage::kLevel, Usage::kDdx, Usage::kDdy, Usage::kSampleIndex}) {
         if (auto* e = arg(usage)) {
@@ -1615,21 +1394,17 @@
                 // WGSL's textureSampleLevel() "level" param is i32 for depth textures,
                 // whereas GLSL's textureLod() "lod" param is always float, so cast it.
                 out << "float(";
-                if (!EmitExpression(out, e)) {
-                    return false;
-                }
+                EmitExpression(out, e);
                 out << ")";
-            } else if (!emit_expr_as_signed(e)) {
-                return false;
+            } else {
+                emit_expr_as_signed(e);
             }
         }
     }
 
     if (auto* e = arg(Usage::kValue)) {
         out << ", ";
-        if (!EmitExpression(out, e)) {
-            return false;
-        }
+        EmitExpression(out, e);
     }
 
     // GLSL's textureGather always requires a refZ parameter.
@@ -1641,9 +1416,7 @@
     if (is_depth && !append_depth_ref_to_coords) {
         if (auto* e = arg(Usage::kDepthRef)) {
             out << ", ";
-            if (!EmitExpression(out, e)) {
-                return false;
-            }
+            EmitExpression(out, e);
         } else if (builtin->Type() == builtin::Function::kTextureSample) {
             out << ", 0.0f";
         }
@@ -1652,16 +1425,14 @@
     for (auto usage : {Usage::kOffset, Usage::kComponent, Usage::kBias}) {
         if (auto* e = arg(usage)) {
             out << ", ";
-            if (!emit_expr_as_signed(e)) {
-                return false;
-            }
+            emit_expr_as_signed(e);
         }
     }
 
     out << ")";
 
     if (builtin->ReturnType()->Is<type::Void>()) {
-        return true;
+        return;
     }
     // If the builtin return type does not match the number of elements of the
     // GLSL builtin, we need to swizzle the expression to generate the correct
@@ -1680,10 +1451,8 @@
         TINT_ICE(Writer, diagnostics_)
             << "WGSL return width (" << wgsl_ret_width << ") is wider than GLSL return width ("
             << glsl_ret_width << ") for " << builtin->Type();
-        return false;
+        return;
     }
-
-    return true;
 }
 
 std::string GeneratorImpl::generate_builtin_name(const sem::Builtin* builtin) {
@@ -1805,7 +1574,7 @@
     return "";
 }
 
-bool GeneratorImpl::EmitCase(const ast::CaseStatement* stmt) {
+void GeneratorImpl::EmitCase(const ast::CaseStatement* stmt) {
     auto* sem = builder_.Sem().Get<sem::CaseStatement>(stmt);
     for (auto* selector : sem->Selectors()) {
         auto out = line();
@@ -1814,9 +1583,7 @@
             out << "default";
         } else {
             out << "case ";
-            if (!EmitConstant(out, selector->Value())) {
-                return false;
-            }
+            EmitConstant(out, selector->Value());
         }
         out << ":";
         if (selector == sem->Selectors().back()) {
@@ -1826,114 +1593,92 @@
 
     {
         ScopedIndent si(this);
-        if (!EmitStatements(stmt->body->statements)) {
-            return false;
-        }
+        EmitStatements(stmt->body->statements);
         if (!last_is_break(stmt->body)) {
             line() << "break;";
         }
     }
 
     line() << "}";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
-    if (!emit_continuing_ || !emit_continuing_()) {
-        return false;
+void GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
+    if (emit_continuing_) {
+        emit_continuing_();
     }
     line() << "continue;";
-    return true;
 }
 
-bool GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
+void GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
     // TODO(dsinclair): Verify this is correct when the discard semantics are
     // defined for WGSL (https://github.com/gpuweb/gpuweb/issues/361)
     line() << "discard;";
-    return true;
 }
 
-bool GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
+void GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
     if (auto* sem = builder_.Sem().GetVal(expr)) {
         if (auto* constant = sem->ConstantValue()) {
-            return EmitConstant(out, constant);
+            EmitConstant(out, constant);
+            return;
         }
     }
-    return Switch(
+    Switch(
         expr,  //
-        [&](const ast::IndexAccessorExpression* a) { return EmitIndexAccessor(out, a); },
-        [&](const ast::BinaryExpression* b) { return EmitBinary(out, b); },
-        [&](const ast::BitcastExpression* b) { return EmitBitcast(out, b); },
-        [&](const ast::CallExpression* c) { return EmitCall(out, c); },
-        [&](const ast::IdentifierExpression* i) { return EmitIdentifier(out, i); },
-        [&](const ast::LiteralExpression* l) { return EmitLiteral(out, l); },
-        [&](const ast::MemberAccessorExpression* m) { return EmitMemberAccessor(out, m); },
-        [&](const ast::UnaryOpExpression* u) { return EmitUnaryOp(out, u); },
+        [&](const ast::IndexAccessorExpression* a) { EmitIndexAccessor(out, a); },
+        [&](const ast::BinaryExpression* b) { EmitBinary(out, b); },
+        [&](const ast::BitcastExpression* b) { EmitBitcast(out, b); },
+        [&](const ast::CallExpression* c) { EmitCall(out, c); },
+        [&](const ast::IdentifierExpression* i) { EmitIdentifier(out, i); },
+        [&](const ast::LiteralExpression* l) { EmitLiteral(out, l); },
+        [&](const ast::MemberAccessorExpression* m) { EmitMemberAccessor(out, m); },
+        [&](const ast::UnaryOpExpression* u) { EmitUnaryOp(out, u); },
         [&](Default) {  //
             diagnostics_.add_error(diag::System::Writer, "unknown expression type: " +
                                                              std::string(expr->TypeInfo().name));
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitIdentifier(utils::StringStream& out,
+void GeneratorImpl::EmitIdentifier(utils::StringStream& out,
                                    const ast::IdentifierExpression* expr) {
     out << expr->identifier->symbol.Name();
-    return true;
 }
 
-bool GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
+void GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
     {
         auto out = line();
         out << "if (";
-        if (!EmitExpression(out, stmt->condition)) {
-            return false;
-        }
+        EmitExpression(out, stmt->condition);
         out << ") {";
     }
-
-    if (!EmitStatementsWithIndent(stmt->body->statements)) {
-        return false;
-    }
+    EmitStatementsWithIndent(stmt->body->statements);
 
     if (stmt->else_statement) {
         line() << "} else {";
         if (auto* block = stmt->else_statement->As<ast::BlockStatement>()) {
-            if (!EmitStatementsWithIndent(block->statements)) {
-                return false;
-            }
+            EmitStatementsWithIndent(block->statements);
         } else {
-            if (!EmitStatementsWithIndent(utils::Vector{stmt->else_statement})) {
-                return false;
-            }
+            EmitStatementsWithIndent(utils::Vector{stmt->else_statement});
         }
     }
     line() << "}";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitFunction(const ast::Function* func) {
+void GeneratorImpl::EmitFunction(const ast::Function* func) {
     auto* sem = builder_.Sem().Get(func);
 
     if (ast::HasAttribute<ast::InternalAttribute>(func->attributes)) {
         // An internal function. Do not emit.
-        return true;
+        return;
     }
 
     {
         auto out = line();
         auto name = func->name->symbol.Name();
-        if (!EmitType(out, sem->ReturnType(), builtin::AddressSpace::kUndefined,
-                      builtin::Access::kReadWrite, "")) {
-            return false;
-        }
-
+        EmitType(out, sem->ReturnType(), builtin::AddressSpace::kUndefined,
+                 builtin::Access::kReadWrite, "");
         out << " " << name << "(";
 
         bool first = true;
-
         for (auto* v : sem->Parameters()) {
             if (!first) {
                 out << ", ";
@@ -1957,81 +1702,79 @@
             // AddressSpace::kStorage or AddressSpace::kUniform. This is required to
             // correctly translate the parameter to a [RW]ByteAddressBuffer for
             // storage buffers and a uint4[N] for uniform buffers.
-            if (!EmitTypeAndName(out, type, v->AddressSpace(), v->Access(),
-                                 v->Declaration()->name->symbol.Name())) {
-                return false;
-            }
+            EmitTypeAndName(out, type, v->AddressSpace(), v->Access(),
+                            v->Declaration()->name->symbol.Name());
         }
         out << ") {";
     }
 
-    if (!EmitStatementsWithIndent(func->body->statements)) {
-        return false;
-    }
+    EmitStatementsWithIndent(func->body->statements);
 
     line() << "}";
     line();
-
-    return true;
 }
 
-bool GeneratorImpl::EmitGlobalVariable(const ast::Variable* global) {
-    return Switch(
+void GeneratorImpl::EmitGlobalVariable(const ast::Variable* global) {
+    Switch(
         global,  //
         [&](const ast::Var* var) {
             auto* sem = builder_.Sem().Get<sem::GlobalVariable>(global);
             switch (sem->AddressSpace()) {
                 case builtin::AddressSpace::kUniform:
-                    return EmitUniformVariable(var, sem);
+                    EmitUniformVariable(var, sem);
+                    return;
                 case builtin::AddressSpace::kStorage:
-                    return EmitStorageVariable(var, sem);
+                    EmitStorageVariable(var, sem);
+                    return;
                 case builtin::AddressSpace::kHandle:
-                    return EmitHandleVariable(var, sem);
+                    EmitHandleVariable(var, sem);
+                    return;
                 case builtin::AddressSpace::kPrivate:
-                    return EmitPrivateVariable(sem);
+                    EmitPrivateVariable(sem);
+                    return;
                 case builtin::AddressSpace::kWorkgroup:
-                    return EmitWorkgroupVariable(sem);
+                    EmitWorkgroupVariable(sem);
+                    return;
                 case builtin::AddressSpace::kIn:
                 case builtin::AddressSpace::kOut:
-                    return EmitIOVariable(sem);
+                    EmitIOVariable(sem);
+                    return;
                 case builtin::AddressSpace::kPushConstant:
                     diagnostics_.add_error(
                         diag::System::Writer,
                         "unhandled address space " + utils::ToString(sem->AddressSpace()));
-                    return false;
+                    return;
                 default: {
                     TINT_ICE(Writer, diagnostics_)
                         << "unhandled address space " << sem->AddressSpace();
-                    return false;
+                    break;
                 }
             }
         },
-        [&](const ast::Let* let) { return EmitProgramConstVariable(let); },
+        [&](const ast::Let* let) { EmitProgramConstVariable(let); },
         [&](const ast::Override*) {
             // Override is removed with SubstituteOverride
             diagnostics_.add_error(diag::System::Writer,
                                    "override-expressions should have been removed with the "
                                    "SubstituteOverride transform");
-            return false;
         },
         [&](const ast::Const*) {
-            return true;  // Constants are embedded at their use
+            // Constants are embedded at their use
         },
         [&](Default) {
             TINT_ICE(Writer, diagnostics_)
                 << "unhandled global variable type " << global->TypeInfo().name;
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitUniformVariable(const ast::Var* var, const sem::Variable* sem) {
+void GeneratorImpl::EmitUniformVariable(const ast::Var* var, const sem::Variable* sem) {
     auto* type = sem->Type()->UnwrapRef();
     auto* str = type->As<sem::Struct>();
     if (TINT_UNLIKELY(!str)) {
         TINT_ICE(Writer, builder_.Diagnostics()) << "storage variable must be of struct type";
-        return false;
+        return;
     }
-    auto bp = sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto bp = *sem->As<sem::GlobalVariable>()->BindingPoint();
     {
         auto out = line();
         out << "layout(binding = " << bp.binding << ", std140";
@@ -2041,37 +1784,34 @@
     auto name = var->name->symbol.Name();
     line() << "} " << name << ";";
     line();
-
-    return true;
 }
 
-bool GeneratorImpl::EmitStorageVariable(const ast::Var* var, const sem::Variable* sem) {
+void GeneratorImpl::EmitStorageVariable(const ast::Var* var, const sem::Variable* sem) {
     auto* type = sem->Type()->UnwrapRef();
     auto* str = type->As<sem::Struct>();
     if (TINT_UNLIKELY(!str)) {
         TINT_ICE(Writer, builder_.Diagnostics()) << "storage variable must be of struct type";
-        return false;
+        return;
     }
-    auto bp = sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto bp = *sem->As<sem::GlobalVariable>()->BindingPoint();
     line() << "layout(binding = " << bp.binding << ", std430) buffer "
            << UniqueIdentifier(StructName(str) + "_ssbo") << " {";
     EmitStructMembers(current_buffer_, str);
     auto name = var->name->symbol.Name();
     line() << "} " << name << ";";
     line();
-
-    return true;
 }
 
-bool GeneratorImpl::EmitHandleVariable(const ast::Var* var, const sem::Variable* sem) {
+void GeneratorImpl::EmitHandleVariable(const ast::Var* var, const sem::Variable* sem) {
     auto out = line();
 
     auto name = var->name->symbol.Name();
     auto* type = sem->Type()->UnwrapRef();
     if (type->Is<type::Sampler>()) {
         // GLSL ignores Sampler variables.
-        return true;
+        return;
     }
+
     if (auto* storage = type->As<type::StorageTexture>()) {
         out << "layout(";
         switch (storage->texel_format()) {
@@ -2129,44 +1869,32 @@
                 break;
             case builtin::TexelFormat::kUndefined:
                 TINT_ICE(Writer, diagnostics_) << "invalid texel format";
-                return false;
+                return;
         }
         out << ") ";
     }
-    if (!EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), name)) {
-        return false;
-    }
-
+    EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), name);
     out << ";";
-    return true;
 }
 
-bool GeneratorImpl::EmitPrivateVariable(const sem::Variable* var) {
+void GeneratorImpl::EmitPrivateVariable(const sem::Variable* var) {
     auto* decl = var->Declaration();
     auto out = line();
 
     auto name = decl->name->symbol.Name();
     auto* type = var->Type()->UnwrapRef();
-    if (!EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name)) {
-        return false;
-    }
+    EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name);
 
     out << " = ";
     if (auto* initializer = decl->initializer) {
-        if (!EmitExpression(out, initializer)) {
-            return false;
-        }
+        EmitExpression(out, initializer);
     } else {
-        if (!EmitZeroValue(out, var->Type()->UnwrapRef())) {
-            return false;
-        }
+        EmitZeroValue(out, var->Type()->UnwrapRef());
     }
-
     out << ";";
-    return true;
 }
 
-bool GeneratorImpl::EmitWorkgroupVariable(const sem::Variable* var) {
+void GeneratorImpl::EmitWorkgroupVariable(const sem::Variable* var) {
     auto* decl = var->Declaration();
     auto out = line();
 
@@ -2174,22 +1902,17 @@
 
     auto name = decl->name->symbol.Name();
     auto* type = var->Type()->UnwrapRef();
-    if (!EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name)) {
-        return false;
-    }
+    EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name);
 
     if (auto* initializer = decl->initializer) {
         out << " = ";
-        if (!EmitExpression(out, initializer)) {
-            return false;
-        }
+        EmitExpression(out, initializer);
     }
 
     out << ";";
-    return true;
 }
 
-bool GeneratorImpl::EmitIOVariable(const sem::GlobalVariable* var) {
+void GeneratorImpl::EmitIOVariable(const sem::GlobalVariable* var) {
     auto* decl = var->Declaration();
 
     if (auto* attr = ast::GetAttribute<ast::BuiltinAttribute>(decl->attributes)) {
@@ -2199,7 +1922,7 @@
             requires_oes_sample_variables_ = true;
         }
         // Do not emit builtin (gl_) variables.
-        return true;
+        return;
     }
 
     auto out = line();
@@ -2208,19 +1931,13 @@
 
     auto name = decl->name->symbol.Name();
     auto* type = var->Type()->UnwrapRef();
-    if (!EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name)) {
-        return false;
-    }
+    EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name);
 
     if (auto* initializer = decl->initializer) {
         out << " = ";
-        if (!EmitExpression(out, initializer)) {
-            return false;
-        }
+        EmitExpression(out, initializer);
     }
-
     out << ";";
-    return true;
 }
 
 void GeneratorImpl::EmitInterpolationQualifiers(
@@ -2260,12 +1977,13 @@
     }
 }
 
-bool GeneratorImpl::EmitAttributes(utils::StringStream& out,
+void GeneratorImpl::EmitAttributes(utils::StringStream& out,
                                    const sem::GlobalVariable* var,
                                    utils::VectorRef<const ast::Attribute*> attributes) {
     if (attributes.IsEmpty()) {
-        return true;
+        return;
     }
+
     bool first = true;
     for (auto* attr : attributes) {
         if (attr->As<ast::LocationAttribute>()) {
@@ -2277,10 +1995,9 @@
     if (!first) {
         out << ") ";
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitEntryPointFunction(const ast::Function* func) {
+void GeneratorImpl::EmitEntryPointFunction(const ast::Function* func) {
     auto* func_sem = builder_.Sem().Get(func);
 
     if (func->PipelineStage() == ast::PipelineStage::kFragment) {
@@ -2303,7 +2020,7 @@
                     diag::System::Writer,
                     "override-expressions should have been removed with the SubstituteOverride "
                     "transform");
-                return false;
+                return;
             }
             out << std::to_string(wgsize[i].value());
         }
@@ -2313,10 +2030,8 @@
     // Emit original entry point signature
     {
         auto out = line();
-        if (!EmitTypeAndName(out, func_sem->ReturnType(), builtin::AddressSpace::kUndefined,
-                             builtin::Access::kUndefined, func->name->symbol.Name())) {
-            return false;
-        }
+        EmitTypeAndName(out, func_sem->ReturnType(), builtin::AddressSpace::kUndefined,
+                        builtin::Access::kUndefined, func->name->symbol.Name());
         out << "(";
 
         bool first = true;
@@ -2336,10 +2051,8 @@
             }
             first = false;
 
-            if (!EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(),
-                                 var->name->symbol.Name())) {
-                return false;
-            }
+            EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(),
+                            var->name->symbol.Name());
         }
 
         out << ") {";
@@ -2352,73 +2065,44 @@
             line() << "gl_PointSize = 1.0;";
         }
 
-        if (!EmitStatements(func->body->statements)) {
-            return false;
-        }
+        EmitStatements(func->body->statements);
 
         if (!Is<ast::ReturnStatement>(func->body->Last())) {
             ast::ReturnStatement ret(ProgramID{}, ast::NodeID{}, Source{});
-            if (!EmitStatement(&ret)) {
-                return false;
-            }
+            EmitStatement(&ret);
         }
     }
 
     line() << "}";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitConstant(utils::StringStream& out, const constant::Value* constant) {
-    return Switch(
+void GeneratorImpl::EmitConstant(utils::StringStream& out, const constant::Value* constant) {
+    Switch(
         constant->Type(),  //
-        [&](const type::Bool*) {
-            out << (constant->ValueAs<AInt>() ? "true" : "false");
-            return true;
-        },
-        [&](const type::F32*) {
-            PrintF32(out, constant->ValueAs<f32>());
-            return true;
-        },
-        [&](const type::F16*) {
-            PrintF16(out, constant->ValueAs<f16>());
-            return true;
-        },
-        [&](const type::I32*) {
-            PrintI32(out, constant->ValueAs<i32>());
-            return true;
-        },
-        [&](const type::U32*) {
-            out << constant->ValueAs<AInt>() << "u";
-            return true;
-        },
+        [&](const type::Bool*) { out << (constant->ValueAs<AInt>() ? "true" : "false"); },
+        [&](const type::F32*) { PrintF32(out, constant->ValueAs<f32>()); },
+        [&](const type::F16*) { PrintF16(out, constant->ValueAs<f16>()); },
+        [&](const type::I32*) { PrintI32(out, constant->ValueAs<i32>()); },
+        [&](const type::U32*) { out << constant->ValueAs<AInt>() << "u"; },
         [&](const type::Vector* v) {
-            if (!EmitType(out, v, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                          "")) {
-                return false;
-            }
+            EmitType(out, v, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
 
             ScopedParen sp(out);
 
             if (auto* splat = constant->As<constant::Splat>()) {
-                return EmitConstant(out, splat->el);
+                EmitConstant(out, splat->el);
+                return;
             }
 
             for (size_t i = 0; i < v->Width(); i++) {
                 if (i > 0) {
                     out << ", ";
                 }
-                if (!EmitConstant(out, constant->Index(i))) {
-                    return false;
-                }
+                EmitConstant(out, constant->Index(i));
             }
-            return true;
         },
         [&](const type::Matrix* m) {
-            if (!EmitType(out, m, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                          "")) {
-                return false;
-            }
+            EmitType(out, m, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
 
             ScopedParen sp(out);
 
@@ -2426,17 +2110,11 @@
                 if (column_idx > 0) {
                     out << ", ";
                 }
-                if (!EmitConstant(out, constant->Index(column_idx))) {
-                    return false;
-                }
+                EmitConstant(out, constant->Index(column_idx));
             }
-            return true;
         },
         [&](const type::Array* a) {
-            if (!EmitType(out, a, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                          "")) {
-                return false;
-            }
+            EmitType(out, a, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
 
             ScopedParen sp(out);
 
@@ -2444,24 +2122,18 @@
             if (!count) {
                 diagnostics_.add_error(diag::System::Writer,
                                        type::Array::kErrExpectedConstantCount);
-                return false;
+                return;
             }
 
             for (size_t i = 0; i < count; i++) {
                 if (i > 0) {
                     out << ", ";
                 }
-                if (!EmitConstant(out, constant->Index(i))) {
-                    return false;
-                }
+                EmitConstant(out, constant->Index(i));
             }
-
-            return true;
         },
         [&](const sem::Struct* s) {
-            if (!EmitStructType(&helpers_, s)) {
-                return false;
-            }
+            EmitStructType(&helpers_, s);
 
             out << StructName(s);
 
@@ -2471,58 +2143,45 @@
                 if (i > 0) {
                     out << ", ";
                 }
-                if (!EmitConstant(out, constant->Index(i))) {
-                    return false;
-                }
+                EmitConstant(out, constant->Index(i));
             }
-
-            return true;
         },
         [&](Default) {
             diagnostics_.add_error(
                 diag::System::Writer,
                 "unhandled constant type: " + builder_.FriendlyName(constant->Type()));
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
-    return Switch(
-        lit,
-        [&](const ast::BoolLiteralExpression* l) {
-            out << (l->value ? "true" : "false");
-            return true;
-        },
+void GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
+    Switch(
+        lit,  //
+        [&](const ast::BoolLiteralExpression* l) { out << (l->value ? "true" : "false"); },
         [&](const ast::FloatLiteralExpression* l) {
             if (l->suffix == ast::FloatLiteralExpression::Suffix::kH) {
                 PrintF16(out, static_cast<float>(l->value));
             } else {
                 PrintF32(out, static_cast<float>(l->value));
             }
-            return true;
         },
         [&](const ast::IntLiteralExpression* i) {
             switch (i->suffix) {
                 case ast::IntLiteralExpression::Suffix::kNone:
                 case ast::IntLiteralExpression::Suffix::kI: {
                     PrintI32(out, static_cast<int32_t>(i->value));
-                    return true;
+                    return;
                 }
                 case ast::IntLiteralExpression::Suffix::kU: {
                     out << i->value << "u";
-                    return true;
+                    return;
                 }
             }
             diagnostics_.add_error(diag::System::Writer, "unknown integer literal suffix type");
-            return false;
         },
-        [&](Default) {
-            diagnostics_.add_error(diag::System::Writer, "unknown literal type");
-            return false;
-        });
+        [&](Default) { diagnostics_.add_error(diag::System::Writer, "unknown literal type"); });
 }
 
-bool GeneratorImpl::EmitZeroValue(utils::StringStream& out, const type::Type* type) {
+void GeneratorImpl::EmitZeroValue(utils::StringStream& out, const type::Type* type) {
     if (type->Is<type::Bool>()) {
         out << "false";
     } else if (type->Is<type::F32>()) {
@@ -2534,38 +2193,25 @@
     } else if (type->Is<type::U32>()) {
         out << "0u";
     } else if (auto* vec = type->As<type::Vector>()) {
-        if (!EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                      "")) {
-            return false;
-        }
+        EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
         ScopedParen sp(out);
         for (uint32_t i = 0; i < vec->Width(); i++) {
             if (i != 0) {
                 out << ", ";
             }
-            if (!EmitZeroValue(out, vec->type())) {
-                return false;
-            }
+            EmitZeroValue(out, vec->type());
         }
     } else if (auto* mat = type->As<type::Matrix>()) {
-        if (!EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
-                      "")) {
-            return false;
-        }
+        EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
         ScopedParen sp(out);
         for (uint32_t i = 0; i < (mat->rows() * mat->columns()); i++) {
             if (i != 0) {
                 out << ", ";
             }
-            if (!EmitZeroValue(out, mat->type())) {
-                return false;
-            }
+            EmitZeroValue(out, mat->type());
         }
     } else if (auto* str = type->As<sem::Struct>()) {
-        if (!EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                      "")) {
-            return false;
-        }
+        EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
         bool first = true;
         ScopedParen sp(out);
         for (auto* member : str->Members()) {
@@ -2577,16 +2223,13 @@
             EmitZeroValue(out, member->Type());
         }
     } else if (auto* arr = type->As<type::Array>()) {
-        if (!EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                      "")) {
-            return false;
-        }
+        EmitType(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined, "");
         ScopedParen sp(out);
 
         auto count = arr->ConstantCount();
         if (!count) {
             diagnostics_.add_error(diag::System::Writer, type::Array::kErrExpectedConstantCount);
-            return false;
+            return;
         }
 
         for (uint32_t i = 0; i < count; i++) {
@@ -2598,38 +2241,27 @@
     } else {
         diagnostics_.add_error(diag::System::Writer,
                                "Invalid type for zero emission: " + type->FriendlyName());
-        return false;
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
+void GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
     auto emit_continuing = [this, stmt]() {
         if (stmt->continuing && !stmt->continuing->Empty()) {
-            if (!EmitBlock(stmt->continuing)) {
-                return false;
-            }
+            EmitBlock(stmt->continuing);
         }
-        return true;
     };
 
     TINT_SCOPED_ASSIGNMENT(emit_continuing_, emit_continuing);
     line() << "while (true) {";
     {
         ScopedIndent si(this);
-        if (!EmitStatements(stmt->body->statements)) {
-            return false;
-        }
-        if (!emit_continuing_()) {
-            return false;
-        }
+        EmitStatements(stmt->body->statements);
+        emit_continuing_();
     }
     line() << "}";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
+void GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
     // Nest a for loop with a new block. In HLSL the initializer scope is not
     // nested by the for-loop, so we may get variable redefinitions.
     line() << "{";
@@ -2642,26 +2274,20 @@
     TextBuffer init_buf;
     if (auto* init = stmt->initializer) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &init_buf);
-        if (!EmitStatement(init)) {
-            return false;
-        }
+        EmitStatement(init);
     }
 
     TextBuffer cond_pre;
     utils::StringStream cond_buf;
     if (auto* cond = stmt->condition) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
-        if (!EmitExpression(cond_buf, cond)) {
-            return false;
-        }
+        EmitExpression(cond_buf, cond);
     }
 
     TextBuffer cont_buf;
     if (auto* cont = stmt->continuing) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cont_buf);
-        if (!EmitStatement(cont)) {
-            return false;
-        }
+        EmitStatement(cont);
     }
 
     // If the for-loop has a multi-statement conditional and / or continuing, then
@@ -2678,10 +2304,7 @@
     }
 
     if (emit_as_loop) {
-        auto emit_continuing = [&]() {
-            current_buffer_->Append(cont_buf);
-            return true;
-        };
+        auto emit_continuing = [&]() { current_buffer_->Append(cont_buf); };
 
         TINT_SCOPED_ASSIGNMENT(emit_continuing_, emit_continuing);
         line() << "while (true) {";
@@ -2696,13 +2319,8 @@
             line() << "if (!(" << cond_buf.str() << ")) { break; }";
         }
 
-        if (!EmitStatements(stmt->body->statements)) {
-            return false;
-        }
-
-        if (!emit_continuing_()) {
-            return false;
-        }
+        EmitStatements(stmt->body->statements);
+        emit_continuing_();
     } else {
         // For-loop can be generated.
         {
@@ -2728,28 +2346,22 @@
         {
             auto emit_continuing = [] { return true; };
             TINT_SCOPED_ASSIGNMENT(emit_continuing_, emit_continuing);
-            if (!EmitStatementsWithIndent(stmt->body->statements)) {
-                return false;
-            }
+            EmitStatementsWithIndent(stmt->body->statements);
         }
         line() << "}";
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitWhile(const ast::WhileStatement* stmt) {
+void GeneratorImpl::EmitWhile(const ast::WhileStatement* stmt) {
     TextBuffer cond_pre;
     utils::StringStream cond_buf;
     {
         auto* cond = stmt->condition;
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
-        if (!EmitExpression(cond_buf, cond)) {
-            return false;
-        }
+        EmitExpression(cond_buf, cond);
     }
 
-    auto emit_continuing = [&]() { return true; };
+    auto emit_continuing = [&]() {};
     TINT_SCOPED_ASSIGNMENT(emit_continuing_, emit_continuing);
 
     // If the whilehas a multi-statement conditional, then we cannot emit this
@@ -2766,9 +2378,7 @@
         current_buffer_->Append(cond_pre);
         line() << "if (!(" << cond_buf.str() << ")) { break; }";
 
-        if (!EmitStatements(stmt->body->statements)) {
-            return false;
-        }
+        EmitStatements(stmt->body->statements);
     } else {
         // While can be generated.
         {
@@ -2780,128 +2390,105 @@
             }
             out << " {";
         }
-        if (!EmitStatementsWithIndent(stmt->body->statements)) {
-            return false;
-        }
+        EmitStatementsWithIndent(stmt->body->statements);
         line() << "}";
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
+void GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
                                        const ast::MemberAccessorExpression* expr) {
-    if (!EmitExpression(out, expr->object)) {
-        return false;
-    }
+    EmitExpression(out, expr->object);
     out << ".";
 
     auto* sem = builder_.Sem().Get(expr)->UnwrapLoad();
 
-    return Switch(
+    Switch(
         sem,
         [&](const sem::Swizzle*) {
             // Swizzles output the name directly
             out << expr->member->symbol.Name();
-            return true;
         },
         [&](const sem::StructMemberAccess* member_access) {
             out << member_access->Member()->Name().Name();
-            return true;
         },
         [&](Default) {
             TINT_ICE(Writer, diagnostics_)
                 << "unknown member access type: " << sem->TypeInfo().name;
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
+void GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
     if (stmt->value) {
         auto out = line();
         out << "return ";
-        if (!EmitExpression(out, stmt->value)) {
-            return false;
-        }
+        EmitExpression(out, stmt->value);
         out << ";";
     } else {
         line() << "return;";
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
-    return Switch(
+void GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
+    Switch(
         stmt,  //
-        [&](const ast::AssignmentStatement* a) { return EmitAssign(a); },
-        [&](const ast::BlockStatement* b) { return EmitBlock(b); },
-        [&](const ast::BreakStatement* b) { return EmitBreak(b); },
-        [&](const ast::BreakIfStatement* b) { return EmitBreakIf(b); },
+        [&](const ast::AssignmentStatement* a) { EmitAssign(a); },
+        [&](const ast::BlockStatement* b) { EmitBlock(b); },
+        [&](const ast::BreakStatement* b) { EmitBreak(b); },
+        [&](const ast::BreakIfStatement* b) { EmitBreakIf(b); },
         [&](const ast::CallStatement* c) {
             auto out = line();
-            if (!EmitCall(out, c->expr)) {
-                return false;
-            }
+            EmitCall(out, c->expr);
             out << ";";
-            return true;
         },
-        [&](const ast::ContinueStatement* c) { return EmitContinue(c); },
-        [&](const ast::DiscardStatement* d) { return EmitDiscard(d); },
-        [&](const ast::IfStatement* i) { return EmitIf(i); },
-        [&](const ast::LoopStatement* l) { return EmitLoop(l); },
-        [&](const ast::ForLoopStatement* l) { return EmitForLoop(l); },
-        [&](const ast::WhileStatement* l) { return EmitWhile(l); },
-        [&](const ast::ReturnStatement* r) { return EmitReturn(r); },
-        [&](const ast::SwitchStatement* s) { return EmitSwitch(s); },
+        [&](const ast::ContinueStatement* c) { EmitContinue(c); },
+        [&](const ast::DiscardStatement* d) { EmitDiscard(d); },
+        [&](const ast::IfStatement* i) { EmitIf(i); },
+        [&](const ast::LoopStatement* l) { EmitLoop(l); },
+        [&](const ast::ForLoopStatement* l) { EmitForLoop(l); },
+        [&](const ast::WhileStatement* l) { EmitWhile(l); },
+        [&](const ast::ReturnStatement* r) { EmitReturn(r); },
+        [&](const ast::SwitchStatement* s) { EmitSwitch(s); },
         [&](const ast::VariableDeclStatement* v) {
-            return Switch(
+            Switch(
                 v->variable,  //
-                [&](const ast::Var* var) { return EmitVar(var); },
-                [&](const ast::Let* let) { return EmitLet(let); },
+                [&](const ast::Var* var) { EmitVar(var); },
+                [&](const ast::Let* let) { EmitLet(let); },
                 [&](const ast::Const*) {
-                    return true;  // Constants are embedded at their use
+                    // Constants are embedded at their use
                 },
                 [&](Default) {  //
                     TINT_ICE(Writer, diagnostics_)
                         << "unknown variable type: " << v->variable->TypeInfo().name;
-                    return false;
                 });
         },
         [&](const ast::ConstAssert*) {
-            return true;  // Not emitted
+            // Not emitted
         },
         [&](Default) {
             diagnostics_.add_error(diag::System::Writer,
                                    "unknown statement type: " + std::string(stmt->TypeInfo().name));
-            return false;
         });
 }
 
-bool GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
+void GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
     {  // switch(expr) {
         auto out = line();
         out << "switch(";
-        if (!EmitExpression(out, stmt->condition)) {
-            return false;
-        }
+        EmitExpression(out, stmt->condition);
         out << ") {";
     }
 
     {
         ScopedIndent si(this);
         for (auto* s : stmt->body) {
-            if (!EmitCase(s)) {
-                return false;
-            }
+            EmitCase(s);
         }
     }
 
     line() << "}";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitType(utils::StringStream& out,
+void GeneratorImpl::EmitType(utils::StringStream& out,
                              const type::Type* type,
                              builtin::AddressSpace address_space,
                              builtin::Access access,
@@ -2939,16 +2526,14 @@
                 if (!count) {
                     diagnostics_.add_error(diag::System::Writer,
                                            type::Array::kErrExpectedConstantCount);
-                    return false;
+                    return;
                 }
                 sizes.push_back(count.value());
             }
 
             base_type = arr->ElemType();
         }
-        if (!EmitType(out, base_type, address_space, access, "")) {
-            return false;
-        }
+        EmitType(out, base_type, address_space, access, "");
         if (!name.empty()) {
             out << " " << name;
             if (name_printed) {
@@ -2982,15 +2567,13 @@
     } else if (TINT_UNLIKELY(type->Is<type::Pointer>())) {
         TINT_ICE(Writer, diagnostics_) << "Attempting to emit pointer type. These should have been "
                                           "removed with the SimplifyPointers transform";
-        return false;
     } else if (type->Is<type::Sampler>()) {
-        return false;
     } else if (auto* str = type->As<sem::Struct>()) {
         out << StructName(str);
     } else if (auto* tex = type->As<type::Texture>()) {
         if (TINT_UNLIKELY(tex->Is<type::ExternalTexture>())) {
             TINT_ICE(Writer, diagnostics_) << "Multiplanar external texture transform was not run.";
-            return false;
+            return;
         }
 
         auto* storage = tex->As<type::StorageTexture>();
@@ -3014,7 +2597,7 @@
             out << "u";
         } else {
             TINT_ICE(Writer, diagnostics_) << "Unsupported texture type";
-            return false;
+            return;
         }
 
         out << (storage ? "image" : "sampler");
@@ -3041,7 +2624,7 @@
             default:
                 TINT_UNREACHABLE(Writer, diagnostics_)
                     << "unexpected TextureDimension " << tex->dim();
-                return false;
+                return;
         }
         if (tex->Is<type::DepthTexture>()) {
             out << "Shadow";
@@ -3062,44 +2645,34 @@
             out << "bvec" << width;
         } else {
             out << "vector<";
-            if (!EmitType(out, vec->type(), address_space, access, "")) {
-                return false;
-            }
+            EmitType(out, vec->type(), address_space, access, "");
             out << ", " << width << ">";
         }
     } else if (auto* atomic = type->As<type::Atomic>()) {
-        if (!EmitType(out, atomic->Type(), address_space, access, name)) {
-            return false;
-        }
+        EmitType(out, atomic->Type(), address_space, access, name);
     } else if (type->Is<type::Void>()) {
         out << "void";
     } else {
         diagnostics_.add_error(diag::System::Writer, "unknown type in EmitType");
-        return false;
     }
-
-    return true;
 }
 
-bool GeneratorImpl::EmitTypeAndName(utils::StringStream& out,
+void GeneratorImpl::EmitTypeAndName(utils::StringStream& out,
                                     const type::Type* type,
                                     builtin::AddressSpace address_space,
                                     builtin::Access access,
                                     const std::string& name) {
     bool printed_name = false;
-    if (!EmitType(out, type, address_space, access, name, &printed_name)) {
-        return false;
-    }
+    EmitType(out, type, address_space, access, name, &printed_name);
     if (!name.empty() && !printed_name) {
         out << " " << name;
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitStructType(TextBuffer* b, const sem::Struct* str) {
+void GeneratorImpl::EmitStructType(TextBuffer* b, const sem::Struct* str) {
     auto it = emitted_structs_.emplace(str);
     if (!it.second) {
-        return true;
+        return;
     }
 
     auto address_space_uses = str->AddressSpaceUsage();
@@ -3107,33 +2680,27 @@
     EmitStructMembers(b, str);
     line(b) << "};";
     line(b);
-
-    return true;
 }
 
-bool GeneratorImpl::EmitStructMembers(TextBuffer* b, const sem::Struct* str) {
+void GeneratorImpl::EmitStructMembers(TextBuffer* b, const sem::Struct* str) {
     ScopedIndent si(b);
     for (auto* mem : str->Members()) {
         auto name = mem->Name().Name();
-
         auto* ty = mem->Type();
 
         auto out = line(b);
-
-        if (!EmitTypeAndName(out, ty, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, name)) {
-            return false;
-        }
+        EmitTypeAndName(out, ty, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
+                        name);
         out << ";";
     }
-    return true;
 }
 
-bool GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
+void GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
     switch (expr->op) {
         case ast::UnaryOp::kIndirection:
         case ast::UnaryOp::kAddressOf:
-            return EmitExpression(out, expr->expr);
+            EmitExpression(out, expr->expr);
+            return;
         case ast::UnaryOp::kComplement:
             out << "~";
             break;
@@ -3150,81 +2717,55 @@
     }
 
     ScopedParen sp(out);
-    if (!EmitExpression(out, expr->expr)) {
-        return false;
-    }
-
-    return true;
+    EmitExpression(out, expr->expr);
 }
 
-bool GeneratorImpl::EmitVar(const ast::Var* var) {
+void GeneratorImpl::EmitVar(const ast::Var* var) {
     auto* sem = builder_.Sem().Get(var);
     auto* type = sem->Type()->UnwrapRef();
 
     auto out = line();
-    if (!EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), var->name->symbol.Name())) {
-        return false;
-    }
+    EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), var->name->symbol.Name());
 
     out << " = ";
 
     if (var->initializer) {
-        if (!EmitExpression(out, var->initializer)) {
-            return false;
-        }
+        EmitExpression(out, var->initializer);
     } else {
-        if (!EmitZeroValue(out, type)) {
-            return false;
-        }
+        EmitZeroValue(out, type);
     }
     out << ";";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitLet(const ast::Let* let) {
+void GeneratorImpl::EmitLet(const ast::Let* let) {
     auto* sem = builder_.Sem().Get(let);
     auto* type = sem->Type()->UnwrapRef();
 
     auto out = line();
     // TODO(senorblanco): handle const
-    if (!EmitTypeAndName(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                         let->name->symbol.Name())) {
-        return false;
-    }
+    EmitTypeAndName(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
+                    let->name->symbol.Name());
 
     out << " = ";
-
-    if (!EmitExpression(out, let->initializer)) {
-        return false;
-    }
-
+    EmitExpression(out, let->initializer);
     out << ";";
-
-    return true;
 }
 
-bool GeneratorImpl::EmitProgramConstVariable(const ast::Variable* var) {
+void GeneratorImpl::EmitProgramConstVariable(const ast::Variable* var) {
     auto* sem = builder_.Sem().Get(var);
     auto* type = sem->Type();
 
     auto out = line();
     out << "const ";
-    if (!EmitTypeAndName(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
-                         var->name->symbol.Name())) {
-        return false;
-    }
+    EmitTypeAndName(out, type, builtin::AddressSpace::kUndefined, builtin::Access::kUndefined,
+                    var->name->symbol.Name());
     out << " = ";
-    if (!EmitExpression(out, var->initializer)) {
-        return false;
-    }
+    EmitExpression(out, var->initializer);
     out << ";";
-
-    return true;
 }
 
 template <typename F>
-bool GeneratorImpl::CallBuiltinHelper(utils::StringStream& out,
+void GeneratorImpl::CallBuiltinHelper(utils::StringStream& out,
                                       const ast::CallExpression* call,
                                       const sem::Builtin* builtin,
                                       F&& build) {
@@ -3237,10 +2778,8 @@
         std::vector<std::string> parameter_names;
         {
             auto decl = line(&b);
-            if (!EmitTypeAndName(decl, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
-                                 builtin::Access::kUndefined, fn_name)) {
-                return "";
-            }
+            EmitTypeAndName(decl, builtin->ReturnType(), builtin::AddressSpace::kUndefined,
+                            builtin::Access::kUndefined, fn_name);
             {
                 ScopedParen sp(decl);
                 for (auto* param : builtin->Parameters()) {
@@ -3253,10 +2792,8 @@
                         decl << "inout ";
                         ty = ptr->StoreType();
                     }
-                    if (!EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
-                                         builtin::Access::kUndefined, param_name)) {
-                        return "";
-                    }
+                    EmitTypeAndName(decl, ty, builtin::AddressSpace::kUndefined,
+                                    builtin::Access::kUndefined, param_name);
                     parameter_names.emplace_back(std::move(param_name));
                 }
             }
@@ -3264,19 +2801,13 @@
         }
         {
             ScopedIndent si(&b);
-            if (!build(&b, parameter_names)) {
-                return "";
-            }
+            build(&b, parameter_names);
         }
         line(&b) << "}";
         line(&b);
         return fn_name;
     });
 
-    if (fn.empty()) {
-        return false;
-    }
-
     // Call the helper
     out << fn;
     {
@@ -3287,12 +2818,9 @@
                 out << ", ";
             }
             first = false;
-            if (!EmitExpression(out, arg)) {
-                return false;
-            }
+            EmitExpression(out, arg);
         }
     }
-    return true;
 }
 
 type::Type* GeneratorImpl::BoolTypeToUint(const type::Type* type) {
diff --git a/src/tint/writer/glsl/generator_impl.h b/src/tint/writer/glsl/generator_impl.h
index 96042c1..0698d4f 100644
--- a/src/tint/writer/glsl/generator_impl.h
+++ b/src/tint/writer/glsl/generator_impl.h
@@ -83,135 +83,111 @@
     GeneratorImpl(const Program* program, const Version& version);
     ~GeneratorImpl();
 
-    /// @returns true on successful generation; false otherwise
-    bool Generate();
+    /// Generates the GLSL shader
+    void Generate();
 
     /// Record an extension directive within the generator
     /// @param ext the extension to record
-    /// @returns true if the extension directive was recorded successfully
-    bool RecordExtension(const ast::Enable* ext);
+    void RecordExtension(const ast::Enable* ext);
     /// Handles an index accessor expression
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
-    /// @returns true if the index accessor was emitted
-    bool EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
+    void EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
     /// Handles an assignment statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted successfully
-    bool EmitAssign(const ast::AssignmentStatement* stmt);
+    void EmitAssign(const ast::AssignmentStatement* stmt);
     /// Handles emission of bitwise operators (&|) on bool scalars and vectors
     /// @param out the output of the expression stream
     /// @param expr the binary expression
-    /// @returns true if the expression was emitted, false otherwise
-    bool EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr);
+    void EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a binary expression
     /// @param out the output of the expression stream
     /// @param expr the binary expression
-    /// @returns true if the expression was emitted, false otherwise
-    bool EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr);
+    void EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating the modulo operator on float vector operands
     /// @param out the output of the expression stream
     /// @param expr the binary expression
-    /// @returns true if the expression was emitted, false otherwise
-    bool EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
+    void EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a bitcast expression
     /// @param out the output of the expression stream
     /// @param expr the expression
-    /// @returns true if the binary expression was emitted
-    bool EmitVectorRelational(utils::StringStream& out, const ast::BinaryExpression* expr);
+    void EmitVectorRelational(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a vector relational expression
     /// @param out the output of the expression stream
     /// @param expr the expression
-    /// @returns true if the vector relational expression was emitted
-    bool EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
+    void EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
     /// Emits a list of statements
     /// @param stmts the statement list
-    /// @returns true if the statements were emitted successfully
-    bool EmitStatements(utils::VectorRef<const ast::Statement*> stmts);
+    void EmitStatements(utils::VectorRef<const ast::Statement*> stmts);
     /// Emits a list of statements with an indentation
     /// @param stmts the statement list
-    /// @returns true if the statements were emitted successfully
-    bool EmitStatementsWithIndent(utils::VectorRef<const ast::Statement*> stmts);
+    void EmitStatementsWithIndent(utils::VectorRef<const ast::Statement*> stmts);
     /// Handles a block statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted successfully
-    bool EmitBlock(const ast::BlockStatement* stmt);
+    void EmitBlock(const ast::BlockStatement* stmt);
     /// Handles a break statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted successfully
-    bool EmitBreak(const ast::BreakStatement* stmt);
+    void EmitBreak(const ast::BreakStatement* stmt);
     /// Handles a break-if statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted successfully
-    bool EmitBreakIf(const ast::BreakIfStatement* stmt);
+    void EmitBreakIf(const ast::BreakIfStatement* stmt);
     /// Handles generating a call expression
     /// @param out the output of the expression stream
     /// @param expr the call expression
-    /// @returns true if the call expression is emitted
-    bool EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a function call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param fn the function being called
-    /// @returns true if the expression is emitted
-    bool EmitFunctionCall(utils::StringStream& out, const sem::Call* call, const sem::Function* fn);
+    void EmitFunctionCall(utils::StringStream& out, const sem::Call* call, const sem::Function* fn);
     /// Handles generating a builtin call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param builtin the builtin being called
-    /// @returns true if the expression is emitted
-    bool EmitBuiltinCall(utils::StringStream& out,
+    void EmitBuiltinCall(utils::StringStream& out,
                          const sem::Call* call,
                          const sem::Builtin* builtin);
     /// Handles generating a value conversion expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param conv the value conversion
-    /// @returns true if the expression is emitted
-    bool EmitValueConversion(utils::StringStream& out,
+    void EmitValueConversion(utils::StringStream& out,
                              const sem::Call* call,
                              const sem::ValueConversion* conv);
     /// Handles generating a value constructor expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param ctor the value constructor
-    /// @returns true if the expression is emitted
-    bool EmitValueConstructor(utils::StringStream& out,
+    void EmitValueConstructor(utils::StringStream& out,
                               const sem::Call* call,
                               const sem::ValueConstructor* ctor);
     /// Handles generating a barrier builtin call
     /// @param out the output of the expression stream
     /// @param builtin the semantic information for the barrier builtin
-    /// @returns true if the call expression is emitted
-    bool EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin);
+    void EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin);
     /// Handles generating an atomic builtin call for a workgroup variable
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the atomic builtin
-    /// @returns true if the call expression is emitted
-    bool EmitWorkgroupAtomicCall(utils::StringStream& out,
+    void EmitWorkgroupAtomicCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin);
     /// Handles generating an array.length() call
     /// @param out the output of the expression stream
     /// @param expr the call expression
-    /// @returns true if the array length expression is emitted
-    bool EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to `bitfieldExtract`
     /// @param out the output of the expression stream
     /// @param expr the call expression
-    /// @returns true if the expression is emitted
-    bool EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to `bitfieldInsert`
     /// @param out the output of the expression stream
     /// @param expr the call expression
-    /// @returns true if the expression is emitted
-    bool EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr);
     /// Emulates 'fma' on GLSL ES, where it is unsupported.
     /// @param out the output of the expression stream
     /// @param expr the fma() expression
-    /// @returns true if the expression is emitted
-    bool EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr);
     /// Create a float literal zero AST node, and associated semantic nodes.
     /// @param stmt the statement which will own the semantic expression node
     /// @returns an AST expression representing 0.0f
@@ -222,130 +198,109 @@
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param builtin the semantic information for the texture builtin
-    /// @returns true if the call expression is emitted
-    bool EmitTextureCall(utils::StringStream& out,
+    void EmitTextureCall(utils::StringStream& out,
                          const sem::Call* call,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `select()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
-    /// @returns true if the call expression is emitted
-    bool EmitCountOneBitsCall(utils::StringStream& out, const ast::CallExpression* expr);
+    void EmitCountOneBitsCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to the `countOneBits()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitSelectCall(utils::StringStream& out,
+    void EmitSelectCall(utils::StringStream& out,
                         const ast::CallExpression* expr,
                         const sem::Builtin* builtin);
     /// Handles generating a call to the `dot()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitDotCall(utils::StringStream& out,
+    void EmitDotCall(utils::StringStream& out,
                      const ast::CallExpression* expr,
                      const sem::Builtin* builtin);
     /// Handles generating a call to the `modf()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitModfCall(utils::StringStream& out,
+    void EmitModfCall(utils::StringStream& out,
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to the `frexp()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitFrexpCall(utils::StringStream& out,
+    void EmitFrexpCall(utils::StringStream& out,
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to the `degrees()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitDegreesCall(utils::StringStream& out,
+    void EmitDegreesCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `radians()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitRadiansCall(utils::StringStream& out,
+    void EmitRadiansCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `quantizeToF16()` intrinsic
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
-    /// @returns true if the call expression is emitted
-    bool EmitQuantizeToF16Call(utils::StringStream& out,
+    void EmitQuantizeToF16Call(utils::StringStream& out,
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles a case statement
     /// @param stmt the statement
-    /// @returns true if the statement was emitted successfully
-    bool EmitCase(const ast::CaseStatement* stmt);
+    void EmitCase(const ast::CaseStatement* stmt);
     /// Handles generating a discard statement
     /// @param stmt the discard statement
-    /// @returns true if the statement was successfully emitted
-    bool EmitDiscard(const ast::DiscardStatement* stmt);
+    void EmitDiscard(const ast::DiscardStatement* stmt);
     /// Handles a continue statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted successfully
-    bool EmitContinue(const ast::ContinueStatement* stmt);
+    void EmitContinue(const ast::ContinueStatement* stmt);
     /// Handles generate an Expression
     /// @param out the output of the expression stream
     /// @param expr the expression
-    /// @returns true if the expression was emitted
-    bool EmitExpression(utils::StringStream& out, const ast::Expression* expr);
+    void EmitExpression(utils::StringStream& out, const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
-    /// @returns true if the function was emitted
-    bool EmitFunction(const ast::Function* func);
+    void EmitFunction(const ast::Function* func);
 
     /// Handles emitting a global variable
     /// @param global the global variable
-    /// @returns true on success
-    bool EmitGlobalVariable(const ast::Variable* global);
+    void EmitGlobalVariable(const ast::Variable* global);
 
     /// Handles emitting a global variable with the uniform address space
     /// @param var the AST node for the 'var'
     /// @param sem the semantic node for the 'var'
-    /// @returns true on success
-    bool EmitUniformVariable(const ast::Var* var, const sem::Variable* sem);
+    void EmitUniformVariable(const ast::Var* var, const sem::Variable* sem);
 
     /// Handles emitting a global variable with the storage address space
     /// @param var the AST node for the 'var'
     /// @param sem the semantic node for the 'var'
-    /// @returns true on success
-    bool EmitStorageVariable(const ast::Var* var, const sem::Variable* sem);
+    void EmitStorageVariable(const ast::Var* var, const sem::Variable* sem);
 
     /// Handles emitting a global variable with the handle address space
     /// @param var the AST node for the 'var'
     /// @param sem the semantic node for the 'var'
-    /// @returns true on success
-    bool EmitHandleVariable(const ast::Var* var, const sem::Variable* sem);
+    void EmitHandleVariable(const ast::Var* var, const sem::Variable* sem);
 
     /// Handles emitting a global variable with the private address space
     /// @param var the global variable
-    /// @returns true on success
-    bool EmitPrivateVariable(const sem::Variable* var);
+    void EmitPrivateVariable(const sem::Variable* var);
 
     /// Handles emitting a global variable with the workgroup address space
     /// @param var the global variable
-    /// @returns true on success
-    bool EmitWorkgroupVariable(const sem::Variable* var);
+    void EmitWorkgroupVariable(const sem::Variable* var);
 
     /// Handles emitting a global variable with the input or output address space
     /// @param var the global variable
-    /// @returns true on success
-    bool EmitIOVariable(const sem::GlobalVariable* var);
+    void EmitIOVariable(const sem::GlobalVariable* var);
 
     /// Handles emitting interpolation qualifiers
     /// @param out the output of the expression stream
@@ -356,62 +311,49 @@
     /// @param out the output of the expression stream
     /// @param var the global variable semantics
     /// @param attrs the attributes
-    /// @returns true if the attributes were emitted
-    bool EmitAttributes(utils::StringStream& out,
+    void EmitAttributes(utils::StringStream& out,
                         const sem::GlobalVariable* var,
                         utils::VectorRef<const ast::Attribute*> attrs);
     /// Handles emitting the entry point function
     /// @param func the entry point
-    /// @returns true if the entry point function was emitted
-    bool EmitEntryPointFunction(const ast::Function* func);
+    void EmitEntryPointFunction(const ast::Function* func);
     /// Handles an if statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was successfully emitted
-    bool EmitIf(const ast::IfStatement* stmt);
+    void EmitIf(const ast::IfStatement* stmt);
     /// Handles a constant value
     /// @param out the output stream
     /// @param constant the constant value to emit
-    /// @returns true if the constant value was successfully emitted
-    bool EmitConstant(utils::StringStream& out, const constant::Value* constant);
+    void EmitConstant(utils::StringStream& out, const constant::Value* constant);
     /// Handles a literal
     /// @param out the output stream
     /// @param lit the literal to emit
-    /// @returns true if the literal was successfully emitted
-    bool EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit);
+    void EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit);
     /// Handles a loop statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted
-    bool EmitLoop(const ast::LoopStatement* stmt);
+    void EmitLoop(const ast::LoopStatement* stmt);
     /// Handles a for loop statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted
-    bool EmitForLoop(const ast::ForLoopStatement* stmt);
+    void EmitForLoop(const ast::ForLoopStatement* stmt);
     /// Handles a while statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted
-    bool EmitWhile(const ast::WhileStatement* stmt);
+    void EmitWhile(const ast::WhileStatement* stmt);
     /// Handles generating an identifier expression
     /// @param out the output of the expression stream
     /// @param expr the identifier expression
-    /// @returns true if the identifier was emitted
-    bool EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
+    void EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles a member accessor expression
     /// @param out the output of the expression stream
     /// @param expr the member accessor expression
-    /// @returns true if the member accessor was emitted
-    bool EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
+    void EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
     /// Handles return statements
     /// @param stmt the statement to emit
-    /// @returns true if the statement was successfully emitted
-    bool EmitReturn(const ast::ReturnStatement* stmt);
+    void EmitReturn(const ast::ReturnStatement* stmt);
     /// Handles statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted
-    bool EmitStatement(const ast::Statement* stmt);
+    void EmitStatement(const ast::Statement* stmt);
     /// Handles generating a switch statement
     /// @param stmt the statement to emit
-    /// @returns true if the statement was emitted
-    bool EmitSwitch(const ast::SwitchStatement* stmt);
+    void EmitSwitch(const ast::SwitchStatement* stmt);
     /// Handles generating type
     /// @param out the output stream
     /// @param type the type to generate
@@ -420,8 +362,7 @@
     /// @param name the name of the variable, used for array emission.
     /// @param name_printed (optional) if not nullptr and an array was printed
     /// then the boolean is set to true.
-    /// @returns true if the type is emitted
-    bool EmitType(utils::StringStream& out,
+    void EmitType(utils::StringStream& out,
                   const type::Type* type,
                   builtin::AddressSpace address_space,
                   builtin::Access access,
@@ -433,8 +374,7 @@
     /// @param address_space the address space of the variable
     /// @param access the access control type of the variable
     /// @param name the name to emit
-    /// @returns true if the type is emitted
-    bool EmitTypeAndName(utils::StringStream& out,
+    void EmitTypeAndName(utils::StringStream& out,
                          const type::Type* type,
                          builtin::AddressSpace address_space,
                          builtin::Access access,
@@ -443,35 +383,28 @@
     /// this function will simply return `true` without emitting anything.
     /// @param buffer the text buffer that the type declaration will be written to
     /// @param ty the struct to generate
-    /// @returns true if the struct is emitted
-    bool EmitStructType(TextBuffer* buffer, const sem::Struct* ty);
+    void EmitStructType(TextBuffer* buffer, const sem::Struct* ty);
     /// Handles generating the members of a structure
     /// @param buffer the text buffer that the struct members will be written to
     /// @param ty the struct to generate
-    /// @returns true if the struct members are emitted
-    bool EmitStructMembers(TextBuffer* buffer, const sem::Struct* ty);
+    void EmitStructMembers(TextBuffer* buffer, const sem::Struct* ty);
     /// Handles a unary op expression
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
-    /// @returns true if the expression was emitted
-    bool EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
+    void EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
     /// Emits the zero value for the given type
     /// @param out the output stream
     /// @param type the type to emit the value for
-    /// @returns true if the zero value was successfully emitted.
-    bool EmitZeroValue(utils::StringStream& out, const type::Type* type);
+    void EmitZeroValue(utils::StringStream& out, const type::Type* type);
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
-    /// @returns true if the variable was emitted
-    bool EmitVar(const ast::Var* var);
+    void EmitVar(const ast::Var* var);
     /// Handles generating a function-scope 'let' declaration
     /// @param let the variable to generate
-    /// @returns true if the variable was emitted
-    bool EmitLet(const ast::Let* let);
+    void EmitLet(const ast::Let* let);
     /// Handles generating a module-scope 'let' declaration
     /// @param let the 'let' to emit
-    /// @returns true if the variable was emitted
-    bool EmitProgramConstVariable(const ast::Variable* let);
+    void EmitProgramConstVariable(const ast::Variable* let);
     /// Handles generating a builtin method name
     /// @param builtin the semantic info for the builtin
     /// @returns the name or "" if not valid
@@ -510,9 +443,8 @@
     ///        Where:
     ///          `buffer` is the body of the generated function
     ///          `params` is the name of all the generated function parameters
-    /// @returns true if the call expression is emitted
     template <typename F>
-    bool CallBuiltinHelper(utils::StringStream& out,
+    void CallBuiltinHelper(utils::StringStream& out,
                            const ast::CallExpression* call,
                            const sem::Builtin* builtin,
                            F&& build);
@@ -523,7 +455,7 @@
     type::Type* BoolTypeToUint(const type::Type* type);
 
     TextBuffer helpers_;  // Helper functions emitted at the top of the output
-    std::function<bool()> emit_continuing_;
+    std::function<void()> emit_continuing_;
     std::unordered_map<const sem::Builtin*, std::string> builtins_;
     std::unordered_map<const type::Vector*, std::string> dynamic_vector_write_;
     std::unordered_map<const type::Vector*, std::string> int_dot_funcs_;
diff --git a/src/tint/writer/glsl/generator_impl_array_accessor_test.cc b/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
index 793902b..c194d64 100644
--- a/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
@@ -16,6 +16,8 @@
 
 #include "src/tint/utils/string_stream.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -31,7 +33,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "ary[5]");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_assign_test.cc b/src/tint/writer/glsl/generator_impl_assign_test.cc
index 63b8ce8..0adcac2 100644
--- a/src/tint/writer/glsl/generator_impl_assign_test.cc
+++ b/src/tint/writer/glsl/generator_impl_assign_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -29,7 +31,8 @@
 
     gen.increment_indent();
 
-    ASSERT_TRUE(gen.EmitStatement(assign)) << gen.Diagnostics();
+    gen.EmitStatement(assign);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  lhs = rhs;\n");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_binary_test.cc b/src/tint/writer/glsl/generator_impl_binary_test.cc
index 29e7136..9be3fa4 100644
--- a/src/tint/writer/glsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/glsl/generator_impl_binary_test.cc
@@ -17,6 +17,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -59,7 +61,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), params.result);
 }
 TEST_P(GlslBinaryTest, Emit_f16) {
@@ -87,7 +90,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), params.result);
 }
 TEST_P(GlslBinaryTest, Emit_u32) {
@@ -106,7 +110,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), params.result);
 }
 TEST_P(GlslBinaryTest, Emit_i32) {
@@ -130,7 +135,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), params.result);
 }
 INSTANTIATE_TEST_SUITE_P(
@@ -165,7 +171,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(a * 1.0f)");
 }
 
@@ -183,7 +190,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(a * 1.0hf)");
 }
 
@@ -199,7 +207,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(1.0f * a)");
 }
 
@@ -217,7 +226,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(1.0hf * a)");
 }
 
@@ -232,7 +242,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(mat * 1.0f)");
 }
 
@@ -249,7 +260,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(mat * 1.0hf)");
 }
 
@@ -264,7 +276,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(1.0f * mat)");
 }
 
@@ -281,7 +294,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(1.0hf * mat)");
 }
 
@@ -296,7 +310,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(mat * vec3(1.0f))");
 }
 
@@ -313,7 +328,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(mat * f16vec3(1.0hf))");
 }
 
@@ -328,7 +344,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(vec3(1.0f) * mat)");
 }
 
@@ -345,7 +362,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(f16vec3(1.0hf) * mat)");
 }
 
@@ -359,7 +377,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(lhs * rhs)");
 }
 
@@ -375,7 +394,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(lhs * rhs)");
 }
 
@@ -389,7 +409,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -405,7 +426,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -419,7 +441,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -435,7 +458,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -449,7 +473,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -465,7 +490,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -479,7 +505,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -495,7 +522,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
 
@@ -513,7 +541,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_float_modulo(vec3 lhs, vec3 rhs) {
@@ -557,7 +586,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -596,7 +626,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
 if (tint_tmp) {
@@ -621,7 +652,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = a;
 if (tint_tmp_1) {
@@ -648,7 +680,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.Diagnostics();
+    gen.EmitExpression(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
 if (!tint_tmp) {
@@ -679,7 +712,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitStatement(expr)) << gen.Diagnostics();
+    gen.EmitStatement(expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
 if (tint_tmp) {
   tint_tmp = b;
@@ -715,7 +749,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitStatement(expr)) << gen.Diagnostics();
+    gen.EmitStatement(expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = a;
 if (tint_tmp_1) {
   tint_tmp_1 = b;
@@ -746,7 +781,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitStatement(expr)) << gen.Diagnostics();
+    gen.EmitStatement(expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = b;
 if (!tint_tmp_1) {
   tint_tmp_1 = c;
@@ -778,7 +814,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitStatement(decl)) << gen.Diagnostics();
+    gen.EmitStatement(decl);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = b;
 if (tint_tmp_1) {
   tint_tmp_1 = c;
@@ -820,7 +857,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitStatement(expr)) << gen.Diagnostics();
+    gen.EmitStatement(expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
 if (tint_tmp) {
   tint_tmp = b;
diff --git a/src/tint/writer/glsl/generator_impl_bitcast_test.cc b/src/tint/writer/glsl/generator_impl_bitcast_test.cc
index fc75caf..f3a7af0 100644
--- a/src/tint/writer/glsl/generator_impl_bitcast_test.cc
+++ b/src/tint/writer/glsl/generator_impl_bitcast_test.cc
@@ -16,6 +16,8 @@
 
 #include "src/tint/utils/string_stream.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -31,7 +33,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.Diagnostics();
+    gen.EmitExpression(out, bitcast);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "intBitsToFloat(a)");
 }
 
@@ -43,7 +46,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.Diagnostics();
+    gen.EmitExpression(out, bitcast);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "int(a)");
 }
 
@@ -55,7 +59,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.Diagnostics();
+    gen.EmitExpression(out, bitcast);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "uint(a)");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_block_test.cc b/src/tint/writer/glsl/generator_impl_block_test.cc
index d9aec9f..1d76883 100644
--- a/src/tint/writer/glsl/generator_impl_block_test.cc
+++ b/src/tint/writer/glsl/generator_impl_block_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -26,8 +28,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(b)) << gen.Diagnostics();
+    gen.EmitStatement(b);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     return;
   }
diff --git a/src/tint/writer/glsl/generator_impl_break_test.cc b/src/tint/writer/glsl/generator_impl_break_test.cc
index f3143eb..4912e0b 100644
--- a/src/tint/writer/glsl/generator_impl_break_test.cc
+++ b/src/tint/writer/glsl/generator_impl_break_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -26,8 +28,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(b)) << gen.Diagnostics();
+    gen.EmitStatement(b);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  break;\n");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_builtin_test.cc b/src/tint/writer/glsl/generator_impl_builtin_test.cc
index 835db7c..e7d1a61 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_test.cc
@@ -351,7 +351,8 @@
 
     gen.increment_indent();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "dot(param1, param2)");
 }
 
@@ -364,7 +365,8 @@
 
     gen.increment_indent();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(true ? b : a)");
 }
 
@@ -377,7 +379,8 @@
 
     gen.increment_indent();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "tint_select(a, b, bvec2(true, false))");
 }
 
@@ -394,7 +397,8 @@
 
     gen.increment_indent();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "((a) * (b) + (c))");
 }
 
@@ -412,7 +416,8 @@
 
     gen.increment_indent();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "((a) * (b) + (c))");
 }
 
@@ -422,7 +427,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct modf_result_f32 {
@@ -458,7 +464,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -493,7 +500,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct modf_result_vec3_f32 {
@@ -529,7 +537,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -563,7 +572,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct modf_result_f32 {
@@ -591,7 +601,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -618,7 +629,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct modf_result_vec3_f32 {
@@ -646,7 +658,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -674,7 +687,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct frexp_result_f32 {
@@ -710,7 +724,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -745,7 +760,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct frexp_result_vec3_f32 {
@@ -781,7 +797,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -815,7 +832,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct frexp_result_f32 {
@@ -843,7 +861,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -870,7 +889,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct frexp_result_vec3_f32 {
@@ -898,7 +918,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -927,7 +948,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_degrees(float param_0) {
@@ -955,7 +977,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_degrees(vec3 param_0) {
@@ -985,7 +1008,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -1016,7 +1040,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -1045,7 +1070,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_radians(float param_0) {
@@ -1073,7 +1099,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_radians(vec3 param_0) {
@@ -1103,7 +1130,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -1134,7 +1162,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -1165,7 +1194,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uvec3 tint_extract_bits(uvec3 v, uint offset, uint count) {
@@ -1199,7 +1229,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uvec3 tint_insert_bits(uvec3 v, uvec3 n, uint offset, uint count) {
@@ -1230,7 +1261,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec4 p1 = vec4(0.0f, 0.0f, 0.0f, 0.0f);
@@ -1248,7 +1280,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec4 p1 = vec4(0.0f, 0.0f, 0.0f, 0.0f);
@@ -1266,7 +1299,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec2 p1 = vec2(0.0f, 0.0f);
@@ -1284,7 +1318,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec2 p1 = vec2(0.0f, 0.0f);
@@ -1302,7 +1337,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec2 p1 = vec2(0.0f, 0.0f);
@@ -1320,7 +1356,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint p1 = 0u;
@@ -1338,7 +1375,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint p1 = 0u;
@@ -1356,7 +1394,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint p1 = 0u;
@@ -1374,7 +1413,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint p1 = 0u;
@@ -1392,7 +1432,8 @@
     WrapInFunction(Decl(Var("r", call)));
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint p1 = 0u;
@@ -1416,7 +1457,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
@@ -1439,7 +1481,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
@@ -1456,7 +1499,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 int tint_int_dot(ivec3 a, ivec3 b) {
@@ -1482,7 +1526,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 uint tint_int_dot(uvec3 a, uvec3 b) {
@@ -1508,7 +1553,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_quantizeToF16(float param_0) {
@@ -1535,7 +1581,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec2 tint_quantizeToF16(vec2 param_0) {
@@ -1562,7 +1609,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_quantizeToF16(vec3 param_0) {
@@ -1591,7 +1639,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec4 tint_quantizeToF16(vec4 param_0) {
diff --git a/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc b/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
index acd2faa..ca8abe4 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
@@ -289,7 +289,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto expected = expected_texture_overload(param.overload);
 
diff --git a/src/tint/writer/glsl/generator_impl_call_test.cc b/src/tint/writer/glsl/generator_impl_call_test.cc
index a1da7ac..b14cc9a 100644
--- a/src/tint/writer/glsl/generator_impl_call_test.cc
+++ b/src/tint/writer/glsl/generator_impl_call_test.cc
@@ -16,6 +16,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -32,7 +34,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "my_func()");
 }
 
@@ -52,7 +55,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.Diagnostics();
+    gen.EmitExpression(out, call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "my_func(param1, param2)");
 }
 
@@ -72,7 +76,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    ASSERT_TRUE(gen.EmitStatement(call)) << gen.Diagnostics();
+    gen.EmitStatement(call);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  my_func(param1, param2);\n");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_case_test.cc b/src/tint/writer/glsl/generator_impl_case_test.cc
index 39d5081..3707f6b 100644
--- a/src/tint/writer/glsl/generator_impl_case_test.cc
+++ b/src/tint/writer/glsl/generator_impl_case_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -29,8 +31,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitCase(s->body[0])) << gen.Diagnostics();
+    gen.EmitCase(s->body[0]);
     EXPECT_EQ(gen.result(), R"(  case 5: {
     break;
   }
@@ -44,8 +45,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitCase(s->body[0])) << gen.Diagnostics();
+    gen.EmitCase(s->body[0]);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  case 5: {
     break;
   }
@@ -66,8 +67,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitCase(s->body[0])) << gen.Diagnostics();
+    gen.EmitCase(s->body[0]);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  case 5:
   case 6: {
     break;
@@ -82,8 +83,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitCase(s->body[0])) << gen.Diagnostics();
+    gen.EmitCase(s->body[0]);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  default: {
     break;
   }
diff --git a/src/tint/writer/glsl/generator_impl_cast_test.cc b/src/tint/writer/glsl/generator_impl_cast_test.cc
index 9b57e4b..4773a4b 100644
--- a/src/tint/writer/glsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/glsl/generator_impl_cast_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -29,7 +31,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.Diagnostics();
+    gen.EmitExpression(out, cast);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "1.0f");
 }
 
@@ -40,7 +43,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.Diagnostics();
+    gen.EmitExpression(out, cast);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "vec3(1.0f, 2.0f, 3.0f)");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_constructor_test.cc b/src/tint/writer/glsl/generator_impl_constructor_test.cc
index 35bd2fb..2983382 100644
--- a/src/tint/writer/glsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_constructor_test.cc
@@ -28,8 +28,8 @@
     WrapInFunction(Expr(false));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("false"));
 }
 
@@ -37,8 +37,8 @@
     WrapInFunction(Expr(-12345_i));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("-12345"));
 }
 
@@ -46,8 +46,8 @@
     WrapInFunction(Expr(56779_u));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("56779u"));
 }
 
@@ -56,8 +56,8 @@
     WrapInFunction(Expr(f32((1 << 30) - 4)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("1073741824.0f"));
 }
 
@@ -68,8 +68,8 @@
     WrapInFunction(Expr(f16((1 << 15) - 8)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("32752.0hf"));
 }
 
@@ -77,8 +77,8 @@
     WrapInFunction(Call<f32>(-1.2e-5_f));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("-0.00001200000042445026f"));
 }
 
@@ -88,8 +88,8 @@
     WrapInFunction(Call<f16>(-1.2e-3_h));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("-0.0011997222900390625hf"));
 }
 
@@ -97,8 +97,8 @@
     WrapInFunction(Call<bool>(true));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("true"));
 }
 
@@ -106,8 +106,8 @@
     WrapInFunction(Call<i32>(-12345_i));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("-12345"));
 }
 
@@ -115,8 +115,8 @@
     WrapInFunction(Call<u32>(12345_u));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("12345u"));
 }
 
@@ -124,8 +124,8 @@
     WrapInFunction(vec3<f32>(1_f, 2_f, 3_f));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("vec3(1.0f, 2.0f, 3.0f)"));
 }
 
@@ -135,8 +135,8 @@
     WrapInFunction(vec3<f16>(1_h, 2_h, 3_h));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("f16vec3(1.0hf, 2.0hf, 3.0hf)"));
 }
 
@@ -144,8 +144,8 @@
     WrapInFunction(vec3<f32>());
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("vec3(0.0f)"));
 }
 
@@ -155,8 +155,8 @@
     WrapInFunction(vec3<f16>());
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("f16vec3(0.0hf)"));
 }
 
@@ -164,8 +164,8 @@
     WrapInFunction(vec3<f32>(2_f));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("vec3(2.0f)"));
 }
 
@@ -175,8 +175,8 @@
     WrapInFunction(vec3<f16>(2_h));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("f16vec3(2.0hf)"));
 }
 
@@ -186,8 +186,8 @@
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(R"(float v = 2.0f;
   vec3 tint_symbol = vec3(v);)"));
 }
@@ -200,8 +200,8 @@
     WrapInFunction(var, cast);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(R"(float16_t v = 2.0hf;
   f16vec3 tint_symbol = f16vec3(v);)"));
 }
@@ -210,8 +210,8 @@
     WrapInFunction(vec3<bool>(true));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("bvec3(true)"));
 }
 
@@ -219,8 +219,8 @@
     WrapInFunction(vec3<i32>(2_i));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("ivec3(2)"));
 }
 
@@ -228,8 +228,8 @@
     WrapInFunction(vec3<u32>(2_u));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("uvec3(2u)"));
 }
 
@@ -237,9 +237,8 @@
     WrapInFunction(mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(3_f, 4_f, 5_f)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("mat2x3(vec3(1.0f, 2.0f, 3.0f), vec3(3.0f, 4.0f, 5.0f))"));
 }
 
@@ -249,9 +248,8 @@
     WrapInFunction(mat2x3<f16>(vec3<f16>(1_h, 2_h, 3_h), vec3<f16>(3_h, 4_h, 5_h)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(),
                 HasSubstr("f16mat2x3(f16vec3(1.0hf, 2.0hf, 3.0hf), f16vec3(3.0hf, 4.0hf, 5.0hf))"));
 }
@@ -276,9 +274,8 @@
     WrapInFunction(ctor);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("mat4(vec4(2.0f, 3.0f, 4.0f, 8.0f), vec4(0.0f), "
                                         "vec4(7.0f), vec4(42.0f, 21.0f, 6.0f, -5.0f))"));
 }
@@ -305,9 +302,8 @@
     WrapInFunction(ctor);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(),
                 HasSubstr("f16mat4(f16vec4(2.0hf, 3.0hf, 4.0hf, 8.0hf), f16vec4(0.0hf), "
                           "f16vec4(7.0hf), f16vec4(42.0hf, 21.0hf, 6.0hf, -5.0hf))"));
@@ -317,9 +313,8 @@
     WrapInFunction(mat2x3<f32>());
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("mat2x3 tint_symbol = mat2x3(vec3(0.0f), vec3(0.0f))"));
 }
 
@@ -329,9 +324,8 @@
     WrapInFunction(mat2x3<f16>());
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(),
                 HasSubstr("f16mat2x3 tint_symbol = f16mat2x3(f16vec3(0.0hf), f16vec3(0.0hf))"));
 }
@@ -348,9 +342,8 @@
     WrapInFunction(m_1, m_2);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("mat4 m_2 = mat4(m_1);"));
 }
 
@@ -368,9 +361,8 @@
     WrapInFunction(m_1, m_2);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("f16mat4 m_2 = f16mat4(m_1);"));
 }
 
@@ -379,8 +371,8 @@
                         vec3<f32>(4_f, 5_f, 6_f), vec3<f32>(7_f, 8_f, 9_f)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("vec3[3](vec3(1.0f, 2.0f, 3.0f), "
                                         "vec3(4.0f, 5.0f, 6.0f), "
                                         "vec3(7.0f, 8.0f, 9.0f))"));
@@ -390,8 +382,8 @@
     WrapInFunction(Call(ty.array(ty.vec3<f32>(), 3_u)));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("vec3[3](vec3(0.0f), vec3(0.0f), vec3(0.0f))"));
 }
 
@@ -405,8 +397,8 @@
     WrapInFunction(Call(ty.Of(str), 1_i, 2_f, vec3<i32>(3_i, 4_i, 5_i)));
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("S(1, 2.0f, ivec3(3, 4, 5))"));
 }
 
@@ -420,8 +412,8 @@
     WrapInFunction(Call(ty.Of(str)));
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("S(0"));
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_continue_test.cc b/src/tint/writer/glsl/generator_impl_continue_test.cc
index 3ea8db6..2c4cdfb 100644
--- a/src/tint/writer/glsl/generator_impl_continue_test.cc
+++ b/src/tint/writer/glsl/generator_impl_continue_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -27,7 +29,8 @@
 
     gen.increment_indent();
 
-    ASSERT_TRUE(gen.EmitStatement(loop)) << gen.Diagnostics();
+    gen.EmitStatement(loop);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     if (false) {
       break;
diff --git a/src/tint/writer/glsl/generator_impl_discard_test.cc b/src/tint/writer/glsl/generator_impl_discard_test.cc
index a66ac1c..cb92414 100644
--- a/src/tint/writer/glsl/generator_impl_discard_test.cc
+++ b/src/tint/writer/glsl/generator_impl_discard_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -28,8 +30,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  discard;\n");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_function_test.cc b/src/tint/writer/glsl/generator_impl_function_test.cc
index ec72ef7..a0bfef9 100644
--- a/src/tint/writer/glsl/generator_impl_function_test.cc
+++ b/src/tint/writer/glsl/generator_impl_function_test.cc
@@ -36,8 +36,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  #version 310 es
 
   void my_func() {
@@ -56,8 +56,8 @@
     GeneratorImpl& gen = SanitizeAndBuild();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(R"(  void tint_symbol() {
     return;
   })"));
@@ -77,8 +77,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  #version 310 es
 
   void my_func(float a, int b) {
@@ -95,8 +95,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -114,8 +114,8 @@
          ty.f32(), utils::Vector{Return(Deref("foo"))});
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(R"(float f(inout float foo) {
   return foo;
 }
@@ -142,8 +142,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -183,8 +183,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -236,8 +236,8 @@
          utils::Vector{Stage(ast::PipelineStage::kFragment)});
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -375,8 +375,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -416,8 +416,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -457,8 +457,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -504,8 +504,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(),
               R"(#version 310 es
 precision highp float;
@@ -550,8 +550,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -595,8 +595,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -642,8 +642,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -688,8 +688,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(),
               R"(#version 310 es
 precision highp float;
@@ -725,8 +725,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 precision highp float;
 
@@ -751,8 +751,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
@@ -770,8 +770,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 layout(local_size_x = 2, local_size_y = 4, local_size_z = 6) in;
@@ -792,8 +792,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 layout(local_size_x = 2, local_size_y = 3, local_size_z = 4) in;
@@ -815,11 +815,13 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    EXPECT_FALSE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
     EXPECT_EQ(
         gen.Diagnostics().str(),
-        R"(error: override-expressions should have been removed with the SubstituteOverride transform)");
+        R"(error: override-expressions should have been removed with the SubstituteOverride transform
+error: override-expressions should have been removed with the SubstituteOverride transform
+error: override-expressions should have been removed with the SubstituteOverride transform
+error: override-expressions should have been removed with the SubstituteOverride transform)");
 }
 
 TEST_F(GlslGeneratorImplTest_Function, Emit_Function_WithArrayParams) {
@@ -829,8 +831,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void my_func(float a[5]) {
@@ -847,8 +849,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float[5] my_func() {
@@ -911,8 +913,8 @@
     }
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct Data {
diff --git a/src/tint/writer/glsl/generator_impl_identifier_test.cc b/src/tint/writer/glsl/generator_impl_identifier_test.cc
index 3560295..61f0236 100644
--- a/src/tint/writer/glsl/generator_impl_identifier_test.cc
+++ b/src/tint/writer/glsl/generator_impl_identifier_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -29,7 +31,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, i)) << gen.Diagnostics();
+    gen.EmitExpression(out, i);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "foo");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_if_test.cc b/src/tint/writer/glsl/generator_impl_if_test.cc
index b474d2c..8fb302a 100644
--- a/src/tint/writer/glsl/generator_impl_if_test.cc
+++ b/src/tint/writer/glsl/generator_impl_if_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -30,7 +32,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    ASSERT_TRUE(gen.EmitStatement(i)) << gen.Diagnostics();
+    gen.EmitStatement(i);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  if (cond) {
     return;
   }
@@ -52,8 +55,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(i)) << gen.Diagnostics();
+    gen.EmitStatement(i);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  if (cond) {
     return;
   } else {
@@ -77,8 +80,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(i)) << gen.Diagnostics();
+    gen.EmitStatement(i);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  if (cond) {
     return;
   } else {
@@ -92,9 +95,7 @@
     GlobalVar("else_cond", ty.bool_(), builtin::AddressSpace::kPrivate);
 
     auto* else_cond = Expr("else_cond");
-
     auto* else_body = Block(Return());
-
     auto* else_body_2 = Block(Return());
 
     auto* cond = Expr("cond");
@@ -105,8 +106,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(i)) << gen.Diagnostics();
+    gen.EmitStatement(i);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  if (cond) {
     return;
   } else {
diff --git a/src/tint/writer/glsl/generator_impl_import_test.cc b/src/tint/writer/glsl/generator_impl_import_test.cc
index c7e3a97..6f772ea 100644
--- a/src/tint/writer/glsl/generator_impl_import_test.cc
+++ b/src/tint/writer/glsl/generator_impl_import_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -41,7 +43,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -80,7 +83,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -97,7 +101,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(
         out.str(),
         std::string(param.glsl_name) +
@@ -140,7 +145,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f, 2.0f)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -162,7 +168,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(),
               std::string(param.glsl_name) + "(vec3(1.0f, 2.0f, 3.0f), vec3(4.0f, 5.0f, 6.0f))");
 }
@@ -187,7 +194,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1, 2)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -205,7 +213,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f, 2.0f, 3.0f)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -225,7 +234,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(),
               std::string(param.glsl_name) +
                   R"((vec3(1.0f, 2.0f, 3.0f), vec3(4.0f, 5.0f, 6.0f), vec3(7.0f, 8.0f, 9.0f)))");
@@ -246,7 +256,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1, 2, 3)");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
@@ -262,7 +273,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.Diagnostics();
+    gen.EmitCall(out, expr);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), std::string("determinant(var)"));
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_loop_test.cc b/src/tint/writer/glsl/generator_impl_loop_test.cc
index 3d23e1e..714d38b 100644
--- a/src/tint/writer/glsl/generator_impl_loop_test.cc
+++ b/src/tint/writer/glsl/generator_impl_loop_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -33,8 +35,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(l)) << gen.Diagnostics();
+    gen.EmitStatement(l);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     break;
   }
@@ -54,8 +56,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(l)) << gen.Diagnostics();
+    gen.EmitStatement(l);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     break;
     {
@@ -78,8 +80,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(l)) << gen.Diagnostics();
+    gen.EmitStatement(l);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     break;
     {
@@ -115,8 +117,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(outer)) << gen.Diagnostics();
+    gen.EmitStatement(outer);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     while (true) {
       break;
@@ -154,8 +156,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(outer)) << gen.Diagnostics();
+    gen.EmitStatement(outer);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     float lhs = 2.5f;
     float other = 0.0f;
@@ -179,8 +181,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     for(; ; ) {
       return;
@@ -201,8 +203,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     for(int i = 0; ; ) {
       return;
@@ -225,8 +227,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     bool tint_tmp = t;
     if (tint_tmp) {
@@ -253,8 +255,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     for(; true; ) {
       a_statement();
@@ -278,8 +280,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     while (true) {
       bool tint_tmp = t;
@@ -306,8 +308,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     for(; ; i = (i + 1)) {
       return;
@@ -332,8 +334,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     while (true) {
       return;
@@ -358,8 +360,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     for(int i = 0; true; i = (i + 1)) {
       return;
@@ -386,8 +388,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  {
     bool tint_tmp = t;
     if (tint_tmp) {
@@ -422,8 +424,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while(true) {
     return;
   }
@@ -441,8 +443,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while(true) {
     continue;
   }
@@ -465,8 +467,8 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
+    gen.EmitStatement(f);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  while (true) {
     bool tint_tmp = t;
     if (tint_tmp) {
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 cfb36ea..041e72c 100644
--- a/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -118,8 +119,8 @@
     WrapInFunction(Var("expr", ty.f32(), expr));
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct Data {
@@ -169,8 +170,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(p.expected));
 }
 
@@ -222,7 +223,8 @@
 
     GeneratorImpl& gen = SanitizeAndBuild();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(p.expected));
 }
 
@@ -277,8 +279,9 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -330,8 +333,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -378,8 +381,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -426,8 +429,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -473,8 +476,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -526,8 +529,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -587,8 +590,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -649,8 +652,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -710,8 +713,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -770,8 +773,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -831,8 +834,8 @@
     });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     auto* expected =
         R"(#version 310 es
 precision highp float;
@@ -870,7 +873,8 @@
     WrapInFunction(var, expr);
 
     GeneratorImpl& gen = SanitizeAndBuild();
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("my_vec.xyz"));
 }
 
@@ -880,7 +884,8 @@
     WrapInFunction(var, expr);
 
     GeneratorImpl& gen = SanitizeAndBuild();
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("my_vec.gbr"));
 }
 
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 adcfa50..8de0dd0 100644
--- a/src/tint/writer/glsl/generator_impl_module_constant_test.cc
+++ b/src/tint/writer/glsl/generator_impl_module_constant_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -27,8 +29,8 @@
     WrapInFunction(Decl(var));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.EmitProgramConstVariable(var)) << gen.Diagnostics();
+    gen.EmitProgramConstVariable(var);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "const float pos[3] = float[3](1.0f, 2.0f, 3.0f);\n");
 }
 
@@ -40,9 +42,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -60,9 +61,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -80,9 +80,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -100,9 +99,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -120,9 +118,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -142,9 +139,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -163,9 +159,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -183,9 +178,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -203,9 +197,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -225,9 +218,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -246,9 +238,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -266,9 +257,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -288,9 +278,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -309,9 +298,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -332,9 +320,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
diff --git a/src/tint/writer/glsl/generator_impl_return_test.cc b/src/tint/writer/glsl/generator_impl_return_test.cc
index ce082d8..d8ddc4c 100644
--- a/src/tint/writer/glsl/generator_impl_return_test.cc
+++ b/src/tint/writer/glsl/generator_impl_return_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -26,10 +28,9 @@
     WrapInFunction(r);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(r)) << gen.Diagnostics();
+    gen.EmitStatement(r);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  return;\n");
 }
 
@@ -38,10 +39,9 @@
     Func("f", utils::Empty, ty.i32(), utils::Vector{r});
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(r)) << gen.Diagnostics();
+    gen.EmitStatement(r);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  return 123;\n");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_sanitizer_test.cc b/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
index 097b519..5a690a5 100644
--- a/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
+++ b/src/tint/writer/glsl/generator_impl_sanitizer_test.cc
@@ -17,6 +17,8 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -38,8 +40,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -78,8 +80,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -122,8 +124,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -159,8 +161,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -201,8 +203,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -247,8 +249,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
@@ -296,8 +298,8 @@
          });
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 
     auto got = gen.result();
     auto* expect = R"(#version 310 es
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 4251cb3..821b0e9 100644
--- a/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc
+++ b/src/tint/writer/glsl/generator_impl_storage_buffer_test.cc
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/number.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using ::testing::HasSubstr;
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -48,7 +49,8 @@
 
     // TODO(crbug.com/tint/1421) offsets do not currently work on GLSL ES.
     // They will likely require manual padding.
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct Nephews {
@@ -70,8 +72,8 @@
     TestAlign(this);
 
     GeneratorImpl& gen = Build(Version(Version::Standard::kDesktop, 4, 4));
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 440
 
 struct Nephews {
diff --git a/src/tint/writer/glsl/generator_impl_switch_test.cc b/src/tint/writer/glsl/generator_impl_switch_test.cc
index 34184cf..f5b7e1b 100644
--- a/src/tint/writer/glsl/generator_impl_switch_test.cc
+++ b/src/tint/writer/glsl/generator_impl_switch_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -35,10 +37,9 @@
     WrapInFunction(s);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(s)) << gen.Diagnostics();
+    gen.EmitStatement(s);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  switch(cond) {
     case 5: {
       break;
@@ -62,10 +63,9 @@
     WrapInFunction(s);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(s)) << gen.Diagnostics();
+    gen.EmitStatement(s);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(  switch(cond) {
     case 5:
     default: {
diff --git a/src/tint/writer/glsl/generator_impl_test.cc b/src/tint/writer/glsl/generator_impl_test.cc
index e5ce52e..a1b08b7 100644
--- a/src/tint/writer/glsl/generator_impl_test.cc
+++ b/src/tint/writer/glsl/generator_impl_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -32,8 +34,8 @@
     Func("my_func", utils::Empty, ty.void_(), utils::Empty);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void my_func() {
@@ -46,8 +48,8 @@
     Func("my_func", utils::Empty, ty.void_(), utils::Empty);
 
     GeneratorImpl& gen = Build(Version(Version::Standard::kDesktop, 4, 4));
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 440
 
 void my_func() {
@@ -69,8 +71,8 @@
          });
 
     GeneratorImpl& gen = Build(Version(Version::Standard::kES, 3, 1));
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_OES_sample_variables : require
 
@@ -94,8 +96,8 @@
          });
 
     GeneratorImpl& gen = Build(Version(Version::Standard::kDesktop, 4, 4));
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 440
 
 int my_func() {
diff --git a/src/tint/writer/glsl/generator_impl_type_test.cc b/src/tint/writer/glsl/generator_impl_type_test.cc
index 3da9cd7..0e85155 100644
--- a/src/tint/writer/glsl/generator_impl_type_test.cc
+++ b/src/tint/writer/glsl/generator_impl_type_test.cc
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/type/depth_texture.h"
@@ -24,6 +23,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using ::testing::HasSubstr;
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -40,9 +41,9 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, "ary"))
-        << gen.Diagnostics();
+    gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
+                 builtin::Access::kReadWrite, "ary");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "bool ary[4]");
 }
 
@@ -53,9 +54,9 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, "ary"))
-        << gen.Diagnostics();
+    gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
+                 builtin::Access::kReadWrite, "ary");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "bool ary[5][4]");
 }
 
@@ -66,9 +67,9 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, "ary"))
-        << gen.Diagnostics();
+    gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
+                 builtin::Access::kReadWrite, "ary");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "bool ary[6][5][4]");
 }
 
@@ -79,9 +80,9 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
+                 builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "bool[4]");
 }
 
@@ -91,9 +92,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, bool_, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, bool_, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "bool");
 }
 
@@ -103,9 +103,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, f32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, f32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "float");
 }
 
@@ -117,9 +116,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, f16, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, f16, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "float16_t");
 }
 
@@ -129,9 +127,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, i32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, i32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "int");
 }
 
@@ -143,9 +140,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "mat2x3");
 }
 
@@ -159,9 +155,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "f16mat2x3");
 }
 
@@ -176,7 +171,8 @@
 
     TextGenerator::TextBuffer buf;
     auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    gen.EmitStructType(&buf, sem_s);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
   float b;
@@ -196,9 +192,8 @@
 
     auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "S");
 }
 
@@ -210,8 +205,8 @@
     GlobalVar("g", ty.Of(s), builtin::AddressSpace::kPrivate);
 
     GeneratorImpl& gen = SanitizeAndBuild();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(R"(struct S {
   int tint_symbol;
   float tint_symbol_1;
@@ -230,7 +225,8 @@
 
     TextGenerator::TextBuffer buf;
     auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    ASSERT_TRUE(gen.EmitStructType(&buf, sem_s)) << gen.Diagnostics();
+    gen.EmitStructType(&buf, sem_s);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(buf.String(), R"(struct S {
   int a;
   float b;
@@ -245,9 +241,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, u32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, u32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "uint");
 }
 
@@ -258,9 +253,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "vec3");
 }
 
@@ -273,9 +267,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "f16vec3");
 }
 
@@ -285,9 +278,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitType(out, void_, builtin::AddressSpace::kUndefined,
-                             builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, void_, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "void");
 }
 
@@ -297,9 +289,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_FALSE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
-                              builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 }
 
 TEST_F(GlslGeneratorImplTest_Type, EmitSamplerComparison) {
@@ -308,9 +299,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_FALSE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
-                              builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
 }
 
 struct GlslDepthTextureData {
@@ -341,7 +331,8 @@
 
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(params.result));
 }
 INSTANTIATE_TEST_SUITE_P(
@@ -368,8 +359,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("sampler2DMS tex;"));
 }
 
@@ -414,8 +405,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(params.result));
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Type,
@@ -519,9 +510,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(
-        gen.EmitType(out, s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
-        << gen.Diagnostics();
+    gen.EmitType(out, s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, "");
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "highp sampler2DMS");
 }
 
@@ -552,8 +542,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr(params.result));
 }
 INSTANTIATE_TEST_SUITE_P(
diff --git a/src/tint/writer/glsl/generator_impl_unary_op_test.cc b/src/tint/writer/glsl/generator_impl_unary_op_test.cc
index f8548ff..0daaa33 100644
--- a/src/tint/writer/glsl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/glsl/generator_impl_unary_op_test.cc
@@ -15,6 +15,8 @@
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 namespace tint::writer::glsl {
 namespace {
 
@@ -28,7 +30,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "expr");
 }
 
@@ -40,7 +43,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "~(expr)");
 }
 
@@ -53,7 +57,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "expr");
 }
 
@@ -65,7 +70,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "!(expr)");
 }
 
@@ -77,7 +83,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "-(expr)");
 }
 
@@ -88,7 +95,8 @@
     GeneratorImpl& gen = Build();
 
     utils::StringStream out;
-    ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.Diagnostics();
+    gen.EmitExpression(out, op);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(out.str(), "(-2147483647 - 1)");
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_uniform_buffer_test.cc b/src/tint/writer/glsl/generator_impl_uniform_buffer_test.cc
index 9274d7a..2cf8c38 100644
--- a/src/tint/writer/glsl/generator_impl_uniform_buffer_test.cc
+++ b/src/tint/writer/glsl/generator_impl_uniform_buffer_test.cc
@@ -12,9 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using ::testing::HasSubstr;
 
 namespace tint::writer::glsl {
@@ -29,8 +30,8 @@
     GlobalVar("simple", ty.Of(simple), builtin::AddressSpace::kUniform, Group(0_a), Binding(0_a));
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct Simple {
@@ -49,8 +50,8 @@
     GlobalVar("simple", ty.Of(simple), builtin::AddressSpace::kUniform, Group(0_a), Binding(0_a));
 
     GeneratorImpl& gen = Build(Version(Version::Standard::kDesktop, 4, 4));
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 440
 
 struct Simple {
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 d5dbb15..80a1fa0 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
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -31,10 +32,9 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  float a = 0.0f;\n");
 }
 
@@ -44,10 +44,9 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "  float a = 0.0f;\n");
 }
 
@@ -57,10 +56,9 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), "");  // Not a mistake - 'const' is inlined
 }
 
@@ -73,9 +71,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -94,9 +91,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -115,9 +111,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -136,9 +131,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -157,9 +151,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -180,9 +173,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -202,9 +194,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -223,9 +214,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -244,9 +234,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -267,9 +256,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -289,9 +277,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -310,9 +297,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -333,9 +319,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 #extension GL_AMD_gpu_shader_half_float : require
 
@@ -355,9 +340,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -376,9 +360,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -397,9 +380,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -419,9 +401,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 struct S {
@@ -448,9 +429,8 @@
          });
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
-
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 void f() {
@@ -466,10 +446,9 @@
     WrapInFunction(var, Expr("a"));
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(),
                 HasSubstr("  float a[5] = float[5](0.0f, 0.0f, 0.0f, 0.0f, 0.0f);\n"));
 }
@@ -480,10 +459,9 @@
     WrapInFunction(Expr("a"));
 
     GeneratorImpl& gen = Build();
-
     gen.increment_indent();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("  float a = 0.0f;\n"));
 }
 
@@ -494,8 +472,8 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(vec3 a = vec3(0.0f);
 )");
 }
@@ -509,8 +487,8 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(), R"(f16vec3 a = f16vec3(0.0hf);
 )");
 }
@@ -522,8 +500,8 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(),
               R"(mat2x3 a = mat2x3(vec3(0.0f), vec3(0.0f));
 )");
@@ -538,8 +516,8 @@
     WrapInFunction(stmt);
 
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
+    gen.EmitStatement(stmt);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_EQ(gen.result(),
               R"(f16mat2x3 a = f16mat2x3(f16vec3(0.0hf), f16vec3(0.0hf));
 )");
diff --git a/src/tint/writer/glsl/generator_impl_workgroup_var_test.cc b/src/tint/writer/glsl/generator_impl_workgroup_var_test.cc
index d2529f4..6edf313 100644
--- a/src/tint/writer/glsl/generator_impl_workgroup_var_test.cc
+++ b/src/tint/writer/glsl/generator_impl_workgroup_var_test.cc
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "gmock/gmock.h"
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "gmock/gmock.h"
+
 using ::testing::HasSubstr;
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -35,8 +36,8 @@
              WorkgroupSize(1_i),
          });
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("shared float wg;\n"));
 }
 
@@ -51,8 +52,8 @@
              WorkgroupSize(1_i),
          });
     GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    gen.Generate();
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
     EXPECT_THAT(gen.result(), HasSubstr("shared float wg;\n"));
 }
 
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index b37ebdc..5cce6a1 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -330,7 +330,7 @@
         return false;
     }
 
-    const TypeInfo* last_kind = nullptr;
+    const utils::TypeInfo* last_kind = nullptr;
     size_t last_padding_line = 0;
 
     auto* mod = builder_.Sem().Module();
@@ -2794,7 +2794,7 @@
         return false;
     }
 
-    if (!tint::IsAnyOf<ast::BreakStatement>(stmt->body->Last())) {
+    if (!tint::utils::IsAnyOf<ast::BreakStatement>(stmt->body->Last())) {
         line() << "break;";
     }
 
@@ -3066,7 +3066,7 @@
 }
 
 bool GeneratorImpl::EmitUniformVariable(const ast::Var* var, const sem::Variable* sem) {
-    auto binding_point = sem->As<sem::GlobalVariable>()->BindingPoint();
+    auto binding_point = *sem->As<sem::GlobalVariable>()->BindingPoint();
     auto* type = sem->Type()->UnwrapRef();
     auto name = var->name->symbol.Name();
     line() << "cbuffer cbuffer_" << name << RegisterAndSpace('b', binding_point) << " {";
@@ -3095,7 +3095,7 @@
 
     auto* global_sem = sem->As<sem::GlobalVariable>();
     out << RegisterAndSpace(sem->Access() == builtin::Access::kRead ? 't' : 'u',
-                            global_sem->BindingPoint())
+                            *global_sem->BindingPoint())
         << ";";
 
     return true;
@@ -3124,14 +3124,14 @@
 
     if (register_space) {
         auto bp = sem->As<sem::GlobalVariable>()->BindingPoint();
-        out << " : register(" << register_space << bp.binding;
+        out << " : register(" << register_space << bp->binding;
         // Omit the space if it's 0, as it's the default.
         // SM 5.0 doesn't support spaces, so we don't emit them if group is 0 for better
         // compatibility.
-        if (bp.group == 0) {
+        if (bp->group == 0) {
             out << ")";
         } else {
-            out << ", space" << bp.group << ")";
+            out << ", space" << bp->group << ")";
         }
     }
 
diff --git a/src/tint/writer/msl/generator_bench.cc b/src/tint/writer/msl/generator_bench.cc
index 7feb1cf..c58ede5 100644
--- a/src/tint/writer/msl/generator_bench.cc
+++ b/src/tint/writer/msl/generator_bench.cc
@@ -27,7 +27,26 @@
     }
     auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(&program, {});
+        tint::writer::msl::Options gen_options = {};
+        gen_options.array_length_from_uniform.ubo_binding = tint::writer::BindingPoint{0, 30};
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 0}, 0);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 1}, 1);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 2}, 2);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 3}, 3);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 4}, 4);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 5}, 5);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 6}, 6);
+        gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+            tint::writer::BindingPoint{0, 7}, 7);
+
+        auto res = Generate(&program, gen_options);
         if (!res.error.empty()) {
             state.SkipWithError(res.error.c_str());
         }
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 842f6cb..df9ff4b 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -90,7 +90,7 @@
 namespace {
 
 bool last_is_break(const ast::BlockStatement* stmts) {
-    return IsAnyOf<ast::BreakStatement>(stmts->Last());
+    return utils::IsAnyOf<ast::BreakStatement>(stmts->Last());
 }
 
 void PrintF32(utils::StringStream& out, float value) {
@@ -1996,12 +1996,12 @@
         }
         auto* param_sem = program_->Sem().Get<sem::Parameter>(param);
         auto bp = param_sem->BindingPoint();
-        if (TINT_UNLIKELY(bp.group != 0)) {
+        if (TINT_UNLIKELY(bp->group != 0)) {
             TINT_ICE(Writer, diagnostics_) << "encountered non-zero resource group index (use "
                                               "BindingRemapper to fix)";
             return kInvalidBindingIndex;
         }
-        return bp.binding;
+        return bp->binding;
     };
 
     {
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 0a3009c..4a5c90b 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -867,14 +867,14 @@
             [&](const ast::BindingAttribute*) {
                 auto bp = sem->BindingPoint();
                 push_annot(spv::Op::OpDecorate, {Operand(var_id), U32Operand(SpvDecorationBinding),
-                                                 Operand(bp.binding)});
+                                                 Operand(bp->binding)});
                 return true;
             },
             [&](const ast::GroupAttribute*) {
                 auto bp = sem->BindingPoint();
                 push_annot(
                     spv::Op::OpDecorate,
-                    {Operand(var_id), U32Operand(SpvDecorationDescriptorSet), Operand(bp.group)});
+                    {Operand(var_id), U32Operand(SpvDecorationDescriptorSet), Operand(bp->group)});
                 return true;
             },
             [&](const ast::IdAttribute*) {