Import Tint changes from Dawn

Contains a manual fix for GN builds (adding 'tint_build_tintd' definition)

Changes: - 27f0080bbe19e7be70257d6cd4dcc3867e5781b6 [tintd] Implement lsp::TextDocumentSignatureHelpRequest by Ben Clayton <bclayton@google.com>
  - 2f215d1b7844f59c05a2dbb270815f5055da9c18 [tintd] Simplify installation by Ben Clayton <bclayton@google.com>
  - 369d8cdfc497c0ed2f47662c273ea1cd38844893 [tintd] Implement lsp::TextDocumentInlayHintRequest by Ben Clayton <bclayton@google.com>
  - 43dabe99b4d6c4bd4ccbfc1a4495a68f2135ea57 [tintd] Implement lsp::TextDocumentSemanticTokensFullRequ... by Ben Clayton <bclayton@google.com>
  - ef02c4cb844521cef32af3d5a03cd7b94774d30a [tintd] Implement lsp::TextDocument[Prepare]RenameRequest by Ben Clayton <bclayton@google.com>
  - aace4808de8332fbd96e23ada9feb28cff7a3108 [tintd] Stub required notifications by Ben Clayton <bclayton@google.com>
  - 82ac0fd8baab81192ed83c0db3d907e72c12f528 Mark dp4a methods as `@const` in core.def. by dan sinclair <dsinclair@chromium.org>
  - 06821eba8a7bdca9240c599ea9de376736cb2fa5 Re-enable disabled test. by dan sinclair <dsinclair@chromium.org>
  - 1617031fad9a06afeb5ec261bac8e50d9ce78619 Change emitted name for `StageAttribute`. by dan sinclair <dsinclair@chromium.org>
  - f3b93c1b2da8c55aacb49cb0763e6fcf666a8ff1 Add FoldConstants transform by dan sinclair <dsinclair@chromium.org>
  - 2366f480a965d3faa74c655d32781665b719a606 [tintd] Implement lsp::TextDocumentHoverRequest by Ben Clayton <bclayton@google.com>
  - 841ca1a590932d341e21ba46b7ecb21e3b60e841 Fix clang-tidy misc-include-cleaner complaints by David Neto <dneto@google.com>
  - c00db02cbbfedcc03ce23a2f237468d1405a5cda [tint][ir] Skip unreachable continuing blocks by James Price <jrprice@google.com>
  - 4176d450acf1e163378e2ca40325b82479c4dcdf [ir] Emit unreachable for loop body exits by James Price <jrprice@google.com>
  - 02a5e844e0baf819b6c28063d5fdb77831794ef9 [spirv] Fix unreachable handling in MergeReturn by James Price <jrprice@google.com>
  - c69f7695fc76ad93f44dc5c8653af1e365bf4559 [tintd] Implement TextDocumentReferencesRequest by Ben Clayton <bclayton@google.com>
  - ee9f1f67b860f424787ff174363f627aa2ebf41f [tintd] Implement TextDocumentDefinitionRequest by Ben Clayton <bclayton@google.com>
  - 7b1819b647abd1dd097fa62db85ed023998793ea [tintd] Implement TextDocumentDocumentSymbolRequest by Ben Clayton <bclayton@google.com>
  - 3f05fea94ec7aa785921b4b095f610bcbca20588 [tintd] Implement basic document handling. by Ben Clayton <bclayton@google.com>
  - b827111c8e489e61b0971232b0da133ca63a7e44 [tint] Preserve derivatives in DemoteToHelper by James Price <jrprice@google.com>
  - 29b75b83730ce94abbb603f7dd4c0afbadf21192 Revert "Use abseil's build targets instead of custom ones." by Geoff Lang <geofflang@google.com>
  - 8a8c582e290b89d92e129ec31b9b69794a91285d [tintd] Add package-lock.json by Ben Clayton <bclayton@google.com>
  - e770313425483dc500296624f6889e9a20ee8cbf tint_cmd: Flush stderr after emitting non-error diagnostics by Antonio Maiorano <amaiorano@google.com>
  - 11dc848eca01299dbc124f3e41ee8166e7e2c340 [tint][wgsl] Expand module-scope declaration sources to c... by Ben Clayton <bclayton@google.com>
  - 89c05af961c8118aa2b8bacf950f9ced8223b2f7 [tint][core] Move core::intrinsic::Match() from .cc to Co... by Ben Clayton <bclayton@google.com>
  - 0e3657146c65ae3275703e92a087350cc81da33c [tint][wgsl] Stub 'ls' the language server package for WG... by Ben Clayton <bclayton@google.com>
  - 0a7731b7d7855b01b929f7fe364f9c208b225ab5 tint/win: fix TmpFilePath asserting in some msvc crts by Antonio Maiorano <amaiorano@google.com>
  - 65afc844c0176313c8f97c87d3c20b6671aa73ba [tint][utils] Fixes for TerminalIsDark() by Ben Clayton <bclayton@google.com>
  - f9a5b712d89d18e969d08e1f7b90fd0c6197b56b [tint] Add DEPS & build support for jsoncpp, langsvr by Ben Clayton <bclayton@google.com>
  - ad08453d7254b0859b5524b0b5475730755ceb2a [tint][ast] Fix std140 matrix size by Ben Clayton <bclayton@google.com>
  - 2ce42fc95ab7cf7bbf97a9c48876c67c95f9cb78 Use abseil's build targets instead of custom ones. by Geoff Lang <geofflang@google.com>
GitOrigin-RevId: 27f0080bbe19e7be70257d6cd4dcc3867e5781b6
Change-Id: I84800f0b65ba70f93422cce1bfea062070278f56
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/180487
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
diff --git a/scripts/tint_overrides_with_defaults.gni b/scripts/tint_overrides_with_defaults.gni
index 4e33a50..f6bc080 100644
--- a/scripts/tint_overrides_with_defaults.gni
+++ b/scripts/tint_overrides_with_defaults.gni
@@ -106,6 +106,11 @@
     tint_build_ir_binary = tint_has_protobuf
   }
 
+  # Build the tintd language server
+  if (!defined(tint_build_tintd)) {
+    tint_build_tintd = false
+  }
+
   # Build unittests
   if (!defined(tint_build_unittests)) {
     tint_build_unittests = true
diff --git a/src/tint/BUILD.bazel b/src/tint/BUILD.bazel
index b3151e0..a7d7852 100644
--- a/src/tint/BUILD.bazel
+++ b/src/tint/BUILD.bazel
@@ -30,13 +30,14 @@
 load(":flags.bzl", "declare_bool_flag", "declare_os_flag")
 
 # Declares the 'tint_build_*' flags that control what parts of Tint get built
-declare_bool_flag(name = "tint_build_glsl_writer",    default = False)
 declare_bool_flag(name = "tint_build_glsl_validator", default = False)
+declare_bool_flag(name = "tint_build_glsl_writer",    default = False)
 declare_bool_flag(name = "tint_build_hlsl_writer",    default = True)
 declare_bool_flag(name = "tint_build_ir",             default = True)
 declare_bool_flag(name = "tint_build_msl_writer",     default = True)
 declare_bool_flag(name = "tint_build_spv_reader",     default = True)
 declare_bool_flag(name = "tint_build_spv_writer",     default = True)
+declare_bool_flag(name = "tint_build_tintd",          default = False)
 declare_bool_flag(name = "tint_build_wgsl_reader",    default = True)
 declare_bool_flag(name = "tint_build_wgsl_writer",    default = True)
 
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index f8fe3f2..a8ba5d2 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -284,6 +284,9 @@
       "${tint_src_dir}/cmd/tint",
     ]
   }
+  if (tint_build_tintd) {
+    deps += [ "${tint_src_dir}/cmd/tintd" ]
+  }
 }
 
 group("fuzzers") {
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index eb95b38..88fa5f8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -66,6 +66,7 @@
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SYNTAX_TREE_WRITER=$<BOOL:${TINT_BUILD_SYNTAX_TREE_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_TINTD=$<BOOL:${TINT_BUILD_TINTD}>)
 
   if(TINT_BUILD_FUZZERS)
     target_compile_options(${TARGET} PRIVATE "-fsanitize=fuzzer")
@@ -500,6 +501,10 @@
       target_link_libraries(${TARGET} PRIVATE
         absl_strings
       )
+    elseif(${DEPENDENCY} STREQUAL "jsoncpp")
+      target_link_libraries(${TARGET} PRIVATE jsoncpp_static)
+    elseif(${DEPENDENCY} STREQUAL "langsvr")
+      target_link_libraries(${TARGET} PRIVATE langsvr)
     elseif(${DEPENDENCY} STREQUAL "glslang")
       target_link_libraries(${TARGET} PRIVATE glslang)
       if(NOT MSVC)
@@ -654,12 +659,29 @@
 
 
 ################################################################################
+# tintd (VSCode WGSL language server)
+################################################################################
+if(TINT_BUILD_TINTD)
+  # Copy all the files out of cmd/tintd/vscode to {build}/vscode
+  set(VSCODE_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmd/tintd/vscode")
+  set(VSCODE_DST_DIR "${DAWN_BUILD_GEN_DIR}/vscode")
+
+  file(GLOB VSCODE_ASSETS RELATIVE "${VSCODE_SRC_DIR}" "${VSCODE_SRC_DIR}/*")
+  foreach(FILE ${VSCODE_ASSETS})
+    configure_file("${VSCODE_SRC_DIR}/${FILE}" "${VSCODE_DST_DIR}/${FILE}" COPYONLY)
+  endforeach()
+  set_target_properties(tint_cmd_tintd_cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${VSCODE_DST_DIR}")
+endif(TINT_BUILD_TINTD)
+
+
+################################################################################
 # Bespoke target settings
 ################################################################################
-if (MSVC)
+if(MSVC)
   target_sources(tint_api PRIVATE tint.natvis)
 endif()
 
+
 ################################################################################
 # Additional fuzzer tests
 ################################################################################
diff --git a/src/tint/cmd/BUILD.cmake b/src/tint/cmd/BUILD.cmake
index cdee1d2..a7b4bcc 100644
--- a/src/tint/cmd/BUILD.cmake
+++ b/src/tint/cmd/BUILD.cmake
@@ -42,3 +42,4 @@
 include(cmd/remote_compile/BUILD.cmake)
 include(cmd/test/BUILD.cmake)
 include(cmd/tint/BUILD.cmake)
+include(cmd/tintd/BUILD.cmake)
diff --git a/src/tint/cmd/common/helper.cc b/src/tint/cmd/common/helper.cc
index a1fbaf0..a461591 100644
--- a/src/tint/cmd/common/helper.cc
+++ b/src/tint/cmd/common/helper.cc
@@ -27,6 +27,7 @@
 
 #include "src/tint/cmd/common/helper.h"
 
+#include <cstdio>
 #include <iostream>
 #include <utility>
 #include <vector>
@@ -276,6 +277,9 @@
             tint::StyledTextPrinter::Create(stderr)->Print(
                 formatter.Format(info.program.Diagnostics()));
         }
+        // Flush any diagnostics written to stderr. We depend on these being emitted to the console
+        // before the program for end-to-end tests.
+        fflush(stderr);
     }
 
     if (!info.program.IsValid()) {
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index 0eec522..6d99985 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -164,6 +164,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_tintd_and_tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/ls:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_wgsl_reader": [
       "//src/tint/lang/wgsl/inspector:test",
       "//src/tint/lang/wgsl/reader/parser:test",
@@ -218,6 +223,11 @@
 )
 
 alias(
+  name = "tint_build_tintd",
+  actual = "//src/tint:tint_build_tintd_true",
+)
+
+alias(
   name = "tint_build_wgsl_reader",
   actual = "//src/tint:tint_build_wgsl_reader_true",
 )
@@ -283,6 +293,13 @@
     ],
 )
 selects.config_setting_group(
+    name = "tint_build_tintd_and_tint_build_wgsl_reader",
+    match_all = [
+        ":tint_build_tintd",
+        ":tint_build_wgsl_reader",
+    ],
+)
+selects.config_setting_group(
     name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
     match_all = [
         ":tint_build_wgsl_reader",
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 4b50533..a280f99 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -182,6 +182,12 @@
   )
 endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_wgsl_ls_test
+  )
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+
 if(TINT_BUILD_WGSL_READER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_wgsl_inspector_test
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 65fd5c2..59b2b6c 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -170,6 +170,10 @@
       deps += [ "${tint_src_dir}/lang/spirv/writer/ast_raise:unittests" ]
     }
 
+    if (tint_build_tintd && tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/ls:unittests" ]
+    }
+
     if (tint_build_wgsl_reader) {
       deps += [
         "${tint_src_dir}/lang/wgsl/inspector:unittests",
diff --git a/src/tint/cmd/tintd/BUILD.bazel b/src/tint/cmd/tintd/BUILD.bazel
new file mode 100644
index 0000000..3139cdf
--- /dev/null
+++ b/src/tint/cmd/tintd/BUILD.bazel
@@ -0,0 +1,87 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_binary(
+  name = "cmd",
+  srcs = [
+    "main.cc",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_tintd": [
+      
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_tintd_and_tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/ls",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_tintd",
+  actual = "//src/tint:tint_build_tintd_true",
+)
+
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_tintd_and_tint_build_wgsl_reader",
+    match_all = [
+        ":tint_build_tintd",
+        ":tint_build_wgsl_reader",
+    ],
+)
+
diff --git a/src/tint/cmd/tintd/BUILD.cfg b/src/tint/cmd/tintd/BUILD.cfg
new file mode 100644
index 0000000..b103301
--- /dev/null
+++ b/src/tint/cmd/tintd/BUILD.cfg
@@ -0,0 +1,4 @@
+{
+    "cmd": { "OutputName": "tintd" },
+    "condition": "tint_build_tintd && tint_build_wgsl_reader"
+}
diff --git a/src/tint/cmd/tintd/BUILD.cmake b/src/tint/cmd/tintd/BUILD.cmake
new file mode 100644
index 0000000..f950a1f
--- /dev/null
+++ b/src/tint/cmd/tintd/BUILD.cmake
@@ -0,0 +1,74 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+################################################################################
+# Target:    tint_cmd_tintd_cmd
+# Kind:      cmd
+# Condition: TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER
+################################################################################
+tint_add_target(tint_cmd_tintd_cmd cmd
+  cmd/tintd/main.cc
+)
+
+tint_target_add_dependencies(tint_cmd_tintd_cmd cmd
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_TINTD)
+  tint_target_add_external_dependencies(tint_cmd_tintd_cmd cmd
+    "langsvr"
+  )
+endif(TINT_BUILD_TINTD)
+
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_tintd_cmd cmd
+    tint_lang_wgsl_ls
+  )
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+
+tint_target_set_output_name(tint_cmd_tintd_cmd cmd "tintd")
+
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/cmd/tintd/BUILD.gn b/src/tint/cmd/tintd/BUILD.gn
new file mode 100644
index 0000000..af1da9e
--- /dev/null
+++ b/src/tint/cmd/tintd/BUILD.gn
@@ -0,0 +1,65 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+if (tint_build_tintd && tint_build_wgsl_reader) {
+  tint_executable("tintd") {
+    output_name = "tintd"
+    sources = [ "main.cc" ]
+    deps = [
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_tintd) {
+      deps += [ "${tint_src_dir}:langsvr" ]
+    }
+
+    if (tint_build_tintd && tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/ls" ]
+    }
+  }
+}
diff --git a/src/tint/cmd/tintd/main.cc b/src/tint/cmd/tintd/main.cc
new file mode 100644
index 0000000..cf5ea15
--- /dev/null
+++ b/src/tint/cmd/tintd/main.cc
@@ -0,0 +1,75 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#if TINT_BUILD_IS_WIN
+#include <fcntl.h>  // _O_BINARY
+#include <io.h>     // _setmode
+#endif              // TINT_BUILD_IS_WIN
+
+#include <fstream>
+#include <iostream>
+
+#include "src/tint/lang/wgsl/ls/serve.h"
+
+namespace {
+
+class StdinStream : public langsvr::Reader {
+  public:
+    /// @copydoc langsvr::Reader
+    size_t Read(std::byte* out, size_t count) override { return fread(out, 1, count, stdin); }
+};
+
+class StdoutStream : public langsvr::Writer {
+  public:
+    /// @copydoc langsvr::Reader
+    langsvr::Result<langsvr::SuccessType> Write(const std::byte* in, size_t count) override {
+        fwrite(in, 1, count, stdout);
+        fflush(stdout);
+        return langsvr::Success;
+    }
+};
+
+}  // namespace
+
+int main() {
+#if TINT_BUILD_IS_WIN
+    // Change stdin & stdout from text mode to binary mode.
+    // This ensures sequences of \r\n are not changed to \n.
+    _setmode(_fileno(stdin), _O_BINARY);
+    _setmode(_fileno(stdout), _O_BINARY);
+#endif  // TINT_BUILD_IS_WIN
+
+    StdoutStream stdout_stream;
+    StdinStream stdin_stream;
+
+    if (auto res = tint::wgsl::ls::Serve(stdin_stream, stdout_stream); res != tint::Success) {
+        std::cerr << res.Failure();
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/src/tint/cmd/tintd/vscode/extension.js b/src/tint/cmd/tintd/vscode/extension.js
new file mode 100644
index 0000000..c98c364
--- /dev/null
+++ b/src/tint/cmd/tintd/vscode/extension.js
@@ -0,0 +1,75 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+var path = require('path');
+var vscode = require('vscode');
+var langClient = require('vscode-languageclient');
+
+var LanguageClient = langClient.LanguageClient;
+
+// activate() is called when the extension is activated
+function activate(context) {
+    let serverModule = path.join(context.extensionPath, 'tintd');
+    let debugOptions = {};
+
+    // If the extension is launched in debug mode then the debug server options are used
+    // Otherwise the run options are used
+    let serverOptions = {
+        run: { command: serverModule, transport: langClient.stdio },
+        debug: { command: serverModule, transport: langClient.stdio, options: debugOptions }
+    }
+
+    // Options to control the language client
+    let clientOptions = {
+        documentSelector: ['wgsl'],
+        synchronize: {
+            // Synchronize the setting section 'wgsl' to the server
+            configurationSection: 'wgsl',
+            // Notify the server about file changes to .wgsl files contained in the workspace
+            fileEvents: vscode.workspace.createFileSystemWatcher('**/*.wgsl')
+        }
+    }
+
+    // Create the language client and start the client.
+    let disposable = new LanguageClient('wgsl', serverOptions, clientOptions).start();
+
+    // Push the disposable to the context's subscriptions so that the
+    // client can be deactivated on extension deactivation
+    context.subscriptions.push(disposable);
+
+    // Set the language configuration here instead of a language configuration
+    // file to work around https://github.com/microsoft/vscode/issues/42649.
+    vscode.languages.setLanguageConfiguration("wgsl", {
+        comments: { "lineComment": "//" },
+    });
+}
+exports.activate = activate;
+
+// this method is called when your extension is deactivated
+function deactivate() {
+}
+exports.deactivate = deactivate;
diff --git a/src/tint/cmd/tintd/vscode/package-lock.json b/src/tint/cmd/tintd/vscode/package-lock.json
new file mode 100644
index 0000000..ce19ef7
--- /dev/null
+++ b/src/tint/cmd/tintd/vscode/package-lock.json
@@ -0,0 +1,108 @@
+{
+  "name": "tintd",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "tintd",
+      "version": "0.0.1",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "vscode-languageclient": "^8.1.0-next.4"
+      },
+      "engines": {
+        "vscode": "^1.67.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/vscode-jsonrpc": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz",
+      "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/vscode-languageclient": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz",
+      "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==",
+      "dependencies": {
+        "minimatch": "^5.1.0",
+        "semver": "^7.3.7",
+        "vscode-languageserver-protocol": "3.17.3"
+      },
+      "engines": {
+        "vscode": "^1.67.0"
+      }
+    },
+    "node_modules/vscode-languageserver-protocol": {
+      "version": "3.17.3",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz",
+      "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==",
+      "dependencies": {
+        "vscode-jsonrpc": "8.1.0",
+        "vscode-languageserver-types": "3.17.3"
+      }
+    },
+    "node_modules/vscode-languageserver-types": {
+      "version": "3.17.3",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
+      "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    }
+  }
+}
diff --git a/src/tint/cmd/tintd/vscode/package.json b/src/tint/cmd/tintd/vscode/package.json
new file mode 100644
index 0000000..4f5e66a
--- /dev/null
+++ b/src/tint/cmd/tintd/vscode/package.json
@@ -0,0 +1,39 @@
+{
+    "name": "tintd",
+    "description": "Language support for WGSL",
+    "author": "Google",
+    "license": "BSD-3-Clause",
+    "version": "0.0.1",
+    "private": true,
+    "publisher": "Google",
+    "engines": {
+        "vscode": "^1.67.0"
+    },
+    "categories": [
+        "Programming Languages"
+    ],
+    "contributes": {
+        "languages": [
+            {
+                "id": "wgsl",
+                "extensions": [
+                    "wgsl"
+                ]
+            }
+        ],
+        "grammars": [
+            {
+                "language": "wgsl",
+                "scopeName": "source.wgsl",
+                "path": "wgsl.tmLanguage.json"
+            }
+        ]
+    },
+    "dependencies": {
+        "vscode-languageclient": "^8.1.0-next.4"
+    },
+    "activationEvents": [
+        "*"
+    ],
+    "main": "./extension.js"
+}
diff --git a/src/tint/cmd/tintd/vscode/wgsl.tmLanguage.json b/src/tint/cmd/tintd/vscode/wgsl.tmLanguage.json
new file mode 100644
index 0000000..5072a29
--- /dev/null
+++ b/src/tint/cmd/tintd/vscode/wgsl.tmLanguage.json
@@ -0,0 +1,241 @@
+{
+    "name": "WGSL",
+    "scopeName": "source.wgsl",
+    "patterns": [
+        {
+            "include": "#comments"
+        },
+        {
+            "include": "#keywords"
+        },
+        {
+            "include": "#attributes"
+        },
+        {
+            "include": "#functions"
+        },
+        {
+            "include": "#function_calls"
+        },
+        {
+            "include": "#literals"
+        },
+        {
+            "include": "#punctuation"
+        }
+    ],
+    "repository": {
+        "comments": {
+            "comment": "single line comment",
+            "name": "comment.line.double-slash.wgsl",
+            "match": "\\s*//.*"
+        },
+        "functions": {
+            "patterns": [
+                {
+                    "comment": "function definition",
+                    "name": "meta.function.definition.wgsl",
+                    "begin": "\\b(fn)\\s+([A-Za-z0-9_]+)(\\([^)]*\\))\\s+(->)\\s+([^{]+)",
+                    "beginCaptures": {
+                        "1": {
+                            "name": "keyword.other.fn.wgsl"
+                        },
+                        "2": {
+                            "name": "entity.name.function.wgsl"
+                        },
+                        "3": {
+                            "name": "punctuation.brackets.round.wgsl"
+                        },
+                        "4": {
+                            "name": "punctuation.fn.arrow.wgsl"
+                        }
+                    },
+                    "end": "\\{",
+                    "endCaptures": {
+                        "0": {
+                            "name": "punctuation.brackets.curly.wgsl"
+                        }
+                    },
+                    "patterns": [
+                        {
+                            "include": "#comments"
+                        },
+                        {
+                            "include": "#keywords"
+                        },
+                        {
+                            "include": "#function_calls"
+                        },
+                        {
+                            "include": "#literals"
+                        },
+                        {
+                            "include": "#punctuation"
+                        }
+                    ]
+                }
+            ]
+        },
+        "function_calls": {
+            "patterns": [
+                {
+                    "comment": "function call",
+                    "name": "meta.function.call.wgsl",
+                    "begin": "([A-Za-z0-9_]+)(\\()",
+                    "beginCaptures": {
+                        "1": {
+                            "name": "entity.name.function.wgsl"
+                        },
+                        "2": {
+                            "name": "punctuation.brackets.round.wgsl"
+                        }
+                    },
+                    "end": "\\)",
+                    "endCaptures": {
+                        "0": {
+                            "name": "punctuation.brackets.round.wgsl"
+                        }
+                    },
+                    "patterns": [
+                        {
+                            "include": "#comments"
+                        },
+                        {
+                            "include": "#keywords"
+                        },
+                        {
+                            "include": "#function_calls"
+                        },
+                        {
+                            "include": "#literals"
+                        },
+                        {
+                            "include": "#punctuation"
+                        }
+                    ]
+                }
+            ]
+        },
+        "attributes": {
+            "patterns": [
+                {
+                    "comment": "attribute",
+                    "name": "keyword.attribute.wgsl",
+                    "match": "(@[A-Za-z0-9_]+[^(])"
+                }
+            ]
+        },
+        "literals": {
+            "patterns": [
+                {
+                    "comment": "floating point literal",
+                    "name": "constant.numeric.float.wgsl",
+                    "match": "(-?\\b[0-9][0-9]*\\.[0-9][0-9]*)([eE][+-]?[0-9]+)?[fh]?\\b"
+                },
+                {
+                    "comment": "integer literal",
+                    "name": "constant.numeric.integer.wgsl",
+                    "match": "-?\\b0x[0-9a-fA-F]+[iufh]?\\b|\\b0[iufh]?\\b|-?\\b[1-9][0-9]*[iufh]?\\b"
+                },
+                {
+                    "comment": "boolean constant",
+                    "name": "constant.language.boolean.wgsl",
+                    "match": "\\b(true|false)\\b"
+                }
+            ]
+        },
+        "punctuation": {
+            "patterns": [
+                {
+                    "comment": "comma",
+                    "name": "punctuation.comma.wgsl",
+                    "match": ","
+                },
+                {
+                    "comment": "braces",
+                    "name": "punctuation.brackets.curly.wgsl",
+                    "match": "[{}]"
+                },
+                {
+                    "comment": "parentheses",
+                    "name": "punctuation.brackets.round.wgsl",
+                    "match": "[()]"
+                },
+                {
+                    "comment": "semicolon",
+                    "name": "punctuation.semi.wgsl",
+                    "match": ";"
+                },
+                {
+                    "comment": "square brackets",
+                    "name": "punctuation.brackets.square.wgsl",
+                    "match": "[\\[\\]]"
+                },
+                {
+                    "comment": "angle brackets",
+                    "name": "punctuation.brackets.angle.wgsl",
+                    "match": "(?<!=)[<>]"
+                },
+                {
+                    "comment": "function ret",
+                    "name": "punctuation.fn.ret.wgsl",
+                    "match": "(->)"
+                }
+            ]
+        },
+        "keywords": {
+            "patterns": [
+                {
+                    "comment": "other keywords",
+                    "name": "keyword.control.wgsl",
+                    "match": "\\b(alias|break|case|const_assert|continue|continuing|default|diagnostic|discard|else|enable|false|fn|for|if|loop|override|requires|return|struct|switch|true|while)\\b"
+                },
+                {
+                    "comment": "reserved keywords",
+                    "name": "keyword.control.wgsl.reserved",
+                    "match": "\\b(NULL|Self|abstract|active|alignas|alignof|as|asm|asm_fragment|async|attribute|auto|await|become|binding_array|cast|catch|class|co_await|co_return|co_yield|coherent|column_major|common|compile|compile_fragment|concept|const_cast|consteval|constexpr|constinit|crate|debugger|decltype|delete|demote|demote_to_helper|do|dynamic_cast|enum|explicit|export|extends|extern|external|fallthrough|filter|final|finally|friend|from|fxgroup|get|goto|groupshared|highp|impl|implements|import|inline|instanceof|interface|layout|lowp|macro|macro_rules|match|mediump|meta|mod|module|move|mut|mutable|namespace|new|nil|noexcept|noinline|nointerpolation|noperspective|null|nullptr|of|operator|package|packoffset|partition|pass|patch|pixelfragment|precise|precision|premerge|priv|protected|pub|public|readonly|ref|regardless|register|reinterpret_cast|require|resource|restrict|self|set|shared|sizeof|smooth|snorm|static|static_assert|static_cast|std|subroutine|super|target|template|this|thread_local|throw|trait|try|type|typedef|typeid|typename|typeof|union|unless|unorm|unsafe|unsized|use|using|varying|virtual|volatile|wgsl|where|with|writeonly|yield)\\b"
+                },
+                {
+                    "comment": "variable keywords",
+                    "name": "keyword.other.wgsl storage.type.wgsl",
+                    "match": "\\b(const|let|var|override)\\b"
+                },
+                {
+                    "comment": "struct keyword",
+                    "name": "keyword.declaration.struct.wgsl storage.type.wgsl",
+                    "match": "\\b(struct)\\b"
+                },
+                {
+                    "comment": "fn",
+                    "name": "keyword.other.fn.wgsl",
+                    "match": "\\bfn\\b"
+                },
+                {
+                    "comment": "logical operators",
+                    "name": "keyword.operator.logical.wgsl",
+                    "match": "(\\^|\\||\\|\\||&&|<<|>>|!)(?!=)"
+                },
+                {
+                    "comment": "assignment operators",
+                    "name": "keyword.operator.assignment.wgsl",
+                    "match": "(\\+=|-=|\\*=|/=|%=|\\^=|&=|\\|=|<<=|>>=)"
+                },
+                {
+                    "comment": "comparison operators",
+                    "name": "keyword.operator.comparison.wgsl",
+                    "match": "(==|!=|<|<=|>|>=)"
+                },
+                {
+                    "comment": "math operators",
+                    "name": "keyword.operator.math.wgsl",
+                    "match": "(([+%]|(\\*(?!\\w)))(?!=))|(-(?!>))|(/(?!/))"
+                },
+                {
+                    "comment": "member access",
+                    "name": "keyword.operator.access.dot.wgsl",
+                    "match": "\\.(?!\\.)"
+                }
+            ]
+        }
+    }
+}
diff --git a/src/tint/externals.json b/src/tint/externals.json
index dd5d1df..dbdf96d 100644
--- a/src/tint/externals.json
+++ b/src/tint/externals.json
@@ -13,57 +13,69 @@
             "benchmark/benchmark.h"
         ]
     },
+    "jsoncpp": {
+        "IncludePatterns": [
+            "json/**"
+        ],
+        "Condition": "tint_build_tintd"
+    },
+    "langsvr": {
+        "IncludePatterns": [
+            "langsvr/**"
+        ],
+        "Condition": "tint_build_tintd"
+    },
     "metal": {
         "IncludePatterns": [
             "Metal/Metal.h"
         ],
-        "Condition": "tint_build_is_mac",
+        "Condition": "tint_build_is_mac"
     },
     "thread": {
         "IncludePatterns": [
             "thread"
-        ],
+        ]
     },
     "spirv-tools": {
         "IncludePatterns": [
             "spirv-tools/**"
         ],
-        "Condition": "tint_build_spv_reader || tint_build_spv_writer",
+        "Condition": "tint_build_spv_reader || tint_build_spv_writer"
     },
     "spirv-headers": {
         "IncludePatterns": [
             "spirv/**"
         ],
-        "Condition": "tint_build_spv_reader || tint_build_spv_writer",
+        "Condition": "tint_build_spv_reader || tint_build_spv_writer"
     },
     "spirv-opt-internal": {
         "IncludePatterns": [
             "source/opt/**"
         ],
-        "Condition": "tint_build_spv_reader || tint_build_spv_writer",
+        "Condition": "tint_build_spv_reader || tint_build_spv_writer"
     },
     "glslang": {
         "IncludePatterns": [
             "glslang/Public/ShaderLang.h"
         ],
-        "Condition": "tint_build_glsl_validator",
+        "Condition": "tint_build_glsl_validator"
     },
     "glslang-res-limits": {
         "IncludePatterns": [
             "glslang/Public/ResourceLimits.h"
         ],
-        "Condition": "tint_build_glsl_validator",
+        "Condition": "tint_build_glsl_validator"
     },
     "gtest": {
         "IncludePatterns": [
             "gtest/**",
-            "gmock/**",
-        ],
+            "gmock/**"
+        ]
     },
     "winsock": {
         "IncludePatterns": [
             "winsock2.h"
         ],
-        "Condition": "tint_build_is_win",
-    },
+        "Condition": "tint_build_is_win"
+    }
 }
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 7352a85..0f2d0d8 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -391,8 +391,8 @@
 @must_use @const fn distance[T: f32_f16](T, T) -> T
 @must_use @const fn distance[N: num, T: f32_f16](vec<N, T>, vec<N, T>) -> T
 @must_use @const fn dot[N: num, T: fiu32_f16](vec<N, T>, vec<N, T>) -> T
-@must_use fn dot4I8Packed(u32, u32) -> i32
-@must_use fn dot4U8Packed(u32, u32) -> u32
+@must_use @const fn dot4I8Packed(u32, u32) -> i32
+@must_use @const fn dot4U8Packed(u32, u32) -> u32
 @must_use @stage("fragment") fn dpdx(f32) -> f32
 @must_use @stage("fragment") fn dpdx[N: num](vec<N, f32>) -> vec<N, f32>
 @must_use @stage("fragment") fn dpdxCoarse(f32) -> f32
diff --git a/src/tint/lang/core/intrinsic/data.cc b/src/tint/lang/core/intrinsic/data.cc
index db57c64..8165d76 100644
--- a/src/tint/lang/core/intrinsic/data.cc
+++ b/src/tint/lang/core/intrinsic/data.cc
@@ -4261,95 +4261,97 @@
   /* [19] */ &core::constant::Eval::determinant,
   /* [20] */ &core::constant::Eval::distance,
   /* [21] */ &core::constant::Eval::dot,
-  /* [22] */ &core::constant::Eval::exp,
-  /* [23] */ &core::constant::Eval::exp2,
-  /* [24] */ &core::constant::Eval::extractBits,
-  /* [25] */ &core::constant::Eval::faceForward,
-  /* [26] */ &core::constant::Eval::firstLeadingBit,
-  /* [27] */ &core::constant::Eval::firstTrailingBit,
-  /* [28] */ &core::constant::Eval::floor,
-  /* [29] */ &core::constant::Eval::fma,
-  /* [30] */ &core::constant::Eval::fract,
-  /* [31] */ &core::constant::Eval::frexp,
-  /* [32] */ &core::constant::Eval::insertBits,
-  /* [33] */ &core::constant::Eval::inverseSqrt,
-  /* [34] */ &core::constant::Eval::ldexp,
-  /* [35] */ &core::constant::Eval::length,
-  /* [36] */ &core::constant::Eval::log,
-  /* [37] */ &core::constant::Eval::log2,
-  /* [38] */ &core::constant::Eval::max,
-  /* [39] */ &core::constant::Eval::min,
-  /* [40] */ &core::constant::Eval::mix,
-  /* [41] */ &core::constant::Eval::modf,
-  /* [42] */ &core::constant::Eval::normalize,
-  /* [43] */ &core::constant::Eval::pack2x16float,
-  /* [44] */ &core::constant::Eval::pack2x16snorm,
-  /* [45] */ &core::constant::Eval::pack2x16unorm,
-  /* [46] */ &core::constant::Eval::pack4x8snorm,
-  /* [47] */ &core::constant::Eval::pack4x8unorm,
-  /* [48] */ &core::constant::Eval::pack4xI8,
-  /* [49] */ &core::constant::Eval::pack4xU8,
-  /* [50] */ &core::constant::Eval::pack4xI8Clamp,
-  /* [51] */ &core::constant::Eval::pack4xU8Clamp,
-  /* [52] */ &core::constant::Eval::pow,
-  /* [53] */ &core::constant::Eval::quantizeToF16,
-  /* [54] */ &core::constant::Eval::radians,
-  /* [55] */ &core::constant::Eval::reflect,
-  /* [56] */ &core::constant::Eval::refract,
-  /* [57] */ &core::constant::Eval::reverseBits,
-  /* [58] */ &core::constant::Eval::round,
-  /* [59] */ &core::constant::Eval::saturate,
-  /* [60] */ &core::constant::Eval::select_bool,
-  /* [61] */ &core::constant::Eval::select_boolvec,
-  /* [62] */ &core::constant::Eval::sign,
-  /* [63] */ &core::constant::Eval::sin,
-  /* [64] */ &core::constant::Eval::sinh,
-  /* [65] */ &core::constant::Eval::smoothstep,
-  /* [66] */ &core::constant::Eval::sqrt,
-  /* [67] */ &core::constant::Eval::step,
-  /* [68] */ &core::constant::Eval::tan,
-  /* [69] */ &core::constant::Eval::tanh,
-  /* [70] */ &core::constant::Eval::transpose,
-  /* [71] */ &core::constant::Eval::trunc,
-  /* [72] */ &core::constant::Eval::unpack2x16float,
-  /* [73] */ &core::constant::Eval::unpack2x16snorm,
-  /* [74] */ &core::constant::Eval::unpack2x16unorm,
-  /* [75] */ &core::constant::Eval::unpack4x8snorm,
-  /* [76] */ &core::constant::Eval::unpack4x8unorm,
-  /* [77] */ &core::constant::Eval::unpack4xI8,
-  /* [78] */ &core::constant::Eval::unpack4xU8,
-  /* [79] */ &core::constant::Eval::Not,
-  /* [80] */ &core::constant::Eval::Complement,
-  /* [81] */ &core::constant::Eval::UnaryMinus,
-  /* [82] */ &core::constant::Eval::Plus,
-  /* [83] */ &core::constant::Eval::Minus,
-  /* [84] */ &core::constant::Eval::Multiply,
-  /* [85] */ &core::constant::Eval::MultiplyMatVec,
-  /* [86] */ &core::constant::Eval::MultiplyVecMat,
-  /* [87] */ &core::constant::Eval::MultiplyMatMat,
-  /* [88] */ &core::constant::Eval::Divide,
-  /* [89] */ &core::constant::Eval::Modulo,
-  /* [90] */ &core::constant::Eval::Xor,
-  /* [91] */ &core::constant::Eval::And,
-  /* [92] */ &core::constant::Eval::Or,
-  /* [93] */ &core::constant::Eval::LogicalAnd,
-  /* [94] */ &core::constant::Eval::LogicalOr,
-  /* [95] */ &core::constant::Eval::Equal,
-  /* [96] */ &core::constant::Eval::NotEqual,
-  /* [97] */ &core::constant::Eval::LessThan,
-  /* [98] */ &core::constant::Eval::GreaterThan,
-  /* [99] */ &core::constant::Eval::LessThanEqual,
-  /* [100] */ &core::constant::Eval::GreaterThanEqual,
-  /* [101] */ &core::constant::Eval::ShiftLeft,
-  /* [102] */ &core::constant::Eval::ShiftRight,
-  /* [103] */ &core::constant::Eval::Zero,
-  /* [104] */ &core::constant::Eval::Identity,
-  /* [105] */ &core::constant::Eval::Conv,
-  /* [106] */ &core::constant::Eval::VecSplat,
-  /* [107] */ &core::constant::Eval::VecInitS,
-  /* [108] */ &core::constant::Eval::VecInitM,
-  /* [109] */ &core::constant::Eval::MatInitS,
-  /* [110] */ &core::constant::Eval::MatInitV,
+  /* [22] */ &core::constant::Eval::dot4I8Packed,
+  /* [23] */ &core::constant::Eval::dot4U8Packed,
+  /* [24] */ &core::constant::Eval::exp,
+  /* [25] */ &core::constant::Eval::exp2,
+  /* [26] */ &core::constant::Eval::extractBits,
+  /* [27] */ &core::constant::Eval::faceForward,
+  /* [28] */ &core::constant::Eval::firstLeadingBit,
+  /* [29] */ &core::constant::Eval::firstTrailingBit,
+  /* [30] */ &core::constant::Eval::floor,
+  /* [31] */ &core::constant::Eval::fma,
+  /* [32] */ &core::constant::Eval::fract,
+  /* [33] */ &core::constant::Eval::frexp,
+  /* [34] */ &core::constant::Eval::insertBits,
+  /* [35] */ &core::constant::Eval::inverseSqrt,
+  /* [36] */ &core::constant::Eval::ldexp,
+  /* [37] */ &core::constant::Eval::length,
+  /* [38] */ &core::constant::Eval::log,
+  /* [39] */ &core::constant::Eval::log2,
+  /* [40] */ &core::constant::Eval::max,
+  /* [41] */ &core::constant::Eval::min,
+  /* [42] */ &core::constant::Eval::mix,
+  /* [43] */ &core::constant::Eval::modf,
+  /* [44] */ &core::constant::Eval::normalize,
+  /* [45] */ &core::constant::Eval::pack2x16float,
+  /* [46] */ &core::constant::Eval::pack2x16snorm,
+  /* [47] */ &core::constant::Eval::pack2x16unorm,
+  /* [48] */ &core::constant::Eval::pack4x8snorm,
+  /* [49] */ &core::constant::Eval::pack4x8unorm,
+  /* [50] */ &core::constant::Eval::pack4xI8,
+  /* [51] */ &core::constant::Eval::pack4xU8,
+  /* [52] */ &core::constant::Eval::pack4xI8Clamp,
+  /* [53] */ &core::constant::Eval::pack4xU8Clamp,
+  /* [54] */ &core::constant::Eval::pow,
+  /* [55] */ &core::constant::Eval::quantizeToF16,
+  /* [56] */ &core::constant::Eval::radians,
+  /* [57] */ &core::constant::Eval::reflect,
+  /* [58] */ &core::constant::Eval::refract,
+  /* [59] */ &core::constant::Eval::reverseBits,
+  /* [60] */ &core::constant::Eval::round,
+  /* [61] */ &core::constant::Eval::saturate,
+  /* [62] */ &core::constant::Eval::select_bool,
+  /* [63] */ &core::constant::Eval::select_boolvec,
+  /* [64] */ &core::constant::Eval::sign,
+  /* [65] */ &core::constant::Eval::sin,
+  /* [66] */ &core::constant::Eval::sinh,
+  /* [67] */ &core::constant::Eval::smoothstep,
+  /* [68] */ &core::constant::Eval::sqrt,
+  /* [69] */ &core::constant::Eval::step,
+  /* [70] */ &core::constant::Eval::tan,
+  /* [71] */ &core::constant::Eval::tanh,
+  /* [72] */ &core::constant::Eval::transpose,
+  /* [73] */ &core::constant::Eval::trunc,
+  /* [74] */ &core::constant::Eval::unpack2x16float,
+  /* [75] */ &core::constant::Eval::unpack2x16snorm,
+  /* [76] */ &core::constant::Eval::unpack2x16unorm,
+  /* [77] */ &core::constant::Eval::unpack4x8snorm,
+  /* [78] */ &core::constant::Eval::unpack4x8unorm,
+  /* [79] */ &core::constant::Eval::unpack4xI8,
+  /* [80] */ &core::constant::Eval::unpack4xU8,
+  /* [81] */ &core::constant::Eval::Not,
+  /* [82] */ &core::constant::Eval::Complement,
+  /* [83] */ &core::constant::Eval::UnaryMinus,
+  /* [84] */ &core::constant::Eval::Plus,
+  /* [85] */ &core::constant::Eval::Minus,
+  /* [86] */ &core::constant::Eval::Multiply,
+  /* [87] */ &core::constant::Eval::MultiplyMatVec,
+  /* [88] */ &core::constant::Eval::MultiplyVecMat,
+  /* [89] */ &core::constant::Eval::MultiplyMatMat,
+  /* [90] */ &core::constant::Eval::Divide,
+  /* [91] */ &core::constant::Eval::Modulo,
+  /* [92] */ &core::constant::Eval::Xor,
+  /* [93] */ &core::constant::Eval::And,
+  /* [94] */ &core::constant::Eval::Or,
+  /* [95] */ &core::constant::Eval::LogicalAnd,
+  /* [96] */ &core::constant::Eval::LogicalOr,
+  /* [97] */ &core::constant::Eval::Equal,
+  /* [98] */ &core::constant::Eval::NotEqual,
+  /* [99] */ &core::constant::Eval::LessThan,
+  /* [100] */ &core::constant::Eval::GreaterThan,
+  /* [101] */ &core::constant::Eval::LessThanEqual,
+  /* [102] */ &core::constant::Eval::GreaterThanEqual,
+  /* [103] */ &core::constant::Eval::ShiftLeft,
+  /* [104] */ &core::constant::Eval::ShiftRight,
+  /* [105] */ &core::constant::Eval::Zero,
+  /* [106] */ &core::constant::Eval::Identity,
+  /* [107] */ &core::constant::Eval::Conv,
+  /* [108] */ &core::constant::Eval::VecSplat,
+  /* [109] */ &core::constant::Eval::VecInitS,
+  /* [110] */ &core::constant::Eval::VecInitM,
+  /* [111] */ &core::constant::Eval::MatInitS,
+  /* [112] */ &core::constant::Eval::MatInitV,
 };
 
 static_assert(ConstEvalFunctionIndex::CanIndex(kConstEvalFunctions),
@@ -5058,7 +5060,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [64] */
@@ -5069,7 +5071,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(217),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [65] */
@@ -5080,7 +5082,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(106),
+    /* const_eval_fn */ ConstEvalFunctionIndex(108),
   },
   {
     /* [66] */
@@ -5091,7 +5093,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(205),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(107),
+    /* const_eval_fn */ ConstEvalFunctionIndex(109),
   },
   {
     /* [67] */
@@ -5102,7 +5104,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(297),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [68] */
@@ -5113,7 +5115,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(300),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [69] */
@@ -5124,7 +5126,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(303),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [70] */
@@ -5135,7 +5137,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(367),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [71] */
@@ -5146,7 +5148,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(369),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [72] */
@@ -5157,7 +5159,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(371),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [73] */
@@ -5168,7 +5170,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(390),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [74] */
@@ -5179,7 +5181,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(390),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [75] */
@@ -5190,7 +5192,7 @@
     /* templates */ TemplateIndex(48),
     /* parameters */ ParameterIndex(390),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [76] */
@@ -5201,7 +5203,7 @@
     /* templates */ TemplateIndex(50),
     /* parameters */ ParameterIndex(390),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [77] */
@@ -5212,7 +5214,7 @@
     /* templates */ TemplateIndex(52),
     /* parameters */ ParameterIndex(390),
     /* return_matcher_indices */ MatcherIndicesIndex(186),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [78] */
@@ -5641,7 +5643,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [117] */
@@ -5652,7 +5654,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(213),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [118] */
@@ -5663,7 +5665,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(106),
+    /* const_eval_fn */ ConstEvalFunctionIndex(108),
   },
   {
     /* [119] */
@@ -5674,7 +5676,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(205),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(107),
+    /* const_eval_fn */ ConstEvalFunctionIndex(109),
   },
   {
     /* [120] */
@@ -5685,7 +5687,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(297),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [121] */
@@ -5696,7 +5698,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(300),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(108),
+    /* const_eval_fn */ ConstEvalFunctionIndex(110),
   },
   {
     /* [122] */
@@ -5707,7 +5709,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(389),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [123] */
@@ -5718,7 +5720,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(389),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [124] */
@@ -5729,7 +5731,7 @@
     /* templates */ TemplateIndex(48),
     /* parameters */ ParameterIndex(389),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [125] */
@@ -5740,7 +5742,7 @@
     /* templates */ TemplateIndex(50),
     /* parameters */ ParameterIndex(389),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [126] */
@@ -5751,7 +5753,7 @@
     /* templates */ TemplateIndex(52),
     /* parameters */ ParameterIndex(389),
     /* return_matcher_indices */ MatcherIndicesIndex(73),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [127] */
@@ -5872,7 +5874,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [138] */
@@ -5883,7 +5885,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [139] */
@@ -5894,7 +5896,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(354),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [140] */
@@ -5905,7 +5907,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(355),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [141] */
@@ -5916,7 +5918,7 @@
     /* templates */ TemplateIndex(13),
     /* parameters */ ParameterIndex(360),
     /* return_matcher_indices */ MatcherIndicesIndex(22),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [142] */
@@ -5927,7 +5929,7 @@
     /* templates */ TemplateIndex(13),
     /* parameters */ ParameterIndex(359),
     /* return_matcher_indices */ MatcherIndicesIndex(22),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [143] */
@@ -5938,7 +5940,7 @@
     /* templates */ TemplateIndex(22),
     /* parameters */ ParameterIndex(361),
     /* return_matcher_indices */ MatcherIndicesIndex(153),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(87),
   },
   {
     /* [144] */
@@ -5949,7 +5951,7 @@
     /* templates */ TemplateIndex(22),
     /* parameters */ ParameterIndex(363),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(86),
+    /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
     /* [145] */
@@ -5960,7 +5962,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(365),
     /* return_matcher_indices */ MatcherIndicesIndex(26),
-    /* const_eval_fn */ ConstEvalFunctionIndex(87),
+    /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
     /* [146] */
@@ -5971,7 +5973,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [147] */
@@ -5982,7 +5984,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(209),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [148] */
@@ -5993,7 +5995,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(106),
+    /* const_eval_fn */ ConstEvalFunctionIndex(108),
   },
   {
     /* [149] */
@@ -6004,7 +6006,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(205),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(107),
+    /* const_eval_fn */ ConstEvalFunctionIndex(109),
   },
   {
     /* [150] */
@@ -6015,7 +6017,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(388),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [151] */
@@ -6026,7 +6028,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(388),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [152] */
@@ -6037,7 +6039,7 @@
     /* templates */ TemplateIndex(48),
     /* parameters */ ParameterIndex(388),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [153] */
@@ -6048,7 +6050,7 @@
     /* templates */ TemplateIndex(50),
     /* parameters */ ParameterIndex(388),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [154] */
@@ -6059,7 +6061,7 @@
     /* templates */ TemplateIndex(52),
     /* parameters */ ParameterIndex(388),
     /* return_matcher_indices */ MatcherIndicesIndex(126),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [155] */
@@ -6444,7 +6446,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [190] */
@@ -6455,7 +6457,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(391),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [191] */
@@ -6466,7 +6468,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [192] */
@@ -6477,7 +6479,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(209),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [193] */
@@ -6488,7 +6490,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(392),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [194] */
@@ -6499,7 +6501,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(393),
     /* return_matcher_indices */ MatcherIndicesIndex(204),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [195] */
@@ -6510,7 +6512,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [196] */
@@ -6521,7 +6523,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(394),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [197] */
@@ -6532,7 +6534,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [198] */
@@ -6543,7 +6545,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(213),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [199] */
@@ -6554,7 +6556,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(395),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [200] */
@@ -6565,7 +6567,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(396),
     /* return_matcher_indices */ MatcherIndicesIndex(210),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [201] */
@@ -6576,7 +6578,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [202] */
@@ -6587,7 +6589,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(397),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [203] */
@@ -6598,7 +6600,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [204] */
@@ -6609,7 +6611,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(217),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [205] */
@@ -6620,7 +6622,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(398),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [206] */
@@ -6631,7 +6633,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(399),
     /* return_matcher_indices */ MatcherIndicesIndex(216),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [207] */
@@ -6642,7 +6644,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [208] */
@@ -6653,7 +6655,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(400),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [209] */
@@ -6664,7 +6666,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [210] */
@@ -6675,7 +6677,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(209),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [211] */
@@ -6686,7 +6688,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(401),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [212] */
@@ -6697,7 +6699,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(402),
     /* return_matcher_indices */ MatcherIndicesIndex(222),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [213] */
@@ -6708,7 +6710,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [214] */
@@ -6719,7 +6721,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(403),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [215] */
@@ -6730,7 +6732,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [216] */
@@ -6741,7 +6743,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(213),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [217] */
@@ -6752,7 +6754,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(404),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [218] */
@@ -6763,7 +6765,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(405),
     /* return_matcher_indices */ MatcherIndicesIndex(228),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [219] */
@@ -6774,7 +6776,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [220] */
@@ -6785,7 +6787,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(406),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [221] */
@@ -6796,7 +6798,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [222] */
@@ -6807,7 +6809,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(217),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [223] */
@@ -6818,7 +6820,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(407),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [224] */
@@ -6829,7 +6831,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(408),
     /* return_matcher_indices */ MatcherIndicesIndex(234),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [225] */
@@ -6840,7 +6842,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [226] */
@@ -6851,7 +6853,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(409),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [227] */
@@ -6862,7 +6864,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [228] */
@@ -6873,7 +6875,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(209),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [229] */
@@ -6884,7 +6886,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(410),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [230] */
@@ -6895,7 +6897,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(411),
     /* return_matcher_indices */ MatcherIndicesIndex(240),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [231] */
@@ -6906,7 +6908,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [232] */
@@ -6917,7 +6919,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(412),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [233] */
@@ -6928,7 +6930,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [234] */
@@ -6939,7 +6941,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(213),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [235] */
@@ -6950,7 +6952,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(413),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [236] */
@@ -6961,7 +6963,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(414),
     /* return_matcher_indices */ MatcherIndicesIndex(246),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [237] */
@@ -6972,7 +6974,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [238] */
@@ -6983,7 +6985,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(415),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [239] */
@@ -6994,7 +6996,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(109),
+    /* const_eval_fn */ ConstEvalFunctionIndex(111),
   },
   {
     /* [240] */
@@ -7005,7 +7007,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(217),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(110),
+    /* const_eval_fn */ ConstEvalFunctionIndex(112),
   },
   {
     /* [241] */
@@ -7016,7 +7018,7 @@
     /* templates */ TemplateIndex(46),
     /* parameters */ ParameterIndex(416),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [242] */
@@ -7027,7 +7029,7 @@
     /* templates */ TemplateIndex(44),
     /* parameters */ ParameterIndex(417),
     /* return_matcher_indices */ MatcherIndicesIndex(252),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [243] */
@@ -7093,7 +7095,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [249] */
@@ -7104,7 +7106,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [250] */
@@ -7115,7 +7117,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(354),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [251] */
@@ -7126,7 +7128,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(355),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [252] */
@@ -7137,7 +7139,7 @@
     /* templates */ TemplateIndex(13),
     /* parameters */ ParameterIndex(358),
     /* return_matcher_indices */ MatcherIndicesIndex(22),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [253] */
@@ -7148,7 +7150,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [254] */
@@ -7159,7 +7161,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [255] */
@@ -7170,7 +7172,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(354),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [256] */
@@ -7181,7 +7183,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(355),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [257] */
@@ -7192,7 +7194,7 @@
     /* templates */ TemplateIndex(13),
     /* parameters */ ParameterIndex(358),
     /* return_matcher_indices */ MatcherIndicesIndex(22),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [258] */
@@ -7203,7 +7205,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(88),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [259] */
@@ -7214,7 +7216,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(88),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [260] */
@@ -7225,7 +7227,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(354),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(88),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [261] */
@@ -7236,7 +7238,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(355),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(88),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [262] */
@@ -7247,7 +7249,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [263] */
@@ -7258,7 +7260,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [264] */
@@ -7269,7 +7271,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(354),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [265] */
@@ -7280,7 +7282,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(355),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [266] */
@@ -7291,7 +7293,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(93),
   },
   {
     /* [267] */
@@ -7302,7 +7304,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(235),
     /* return_matcher_indices */ MatcherIndicesIndex(41),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(93),
   },
   {
     /* [268] */
@@ -7313,7 +7315,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(93),
   },
   {
     /* [269] */
@@ -7324,7 +7326,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(93),
   },
   {
     /* [270] */
@@ -7335,7 +7337,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(94),
   },
   {
     /* [271] */
@@ -7346,7 +7348,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(235),
     /* return_matcher_indices */ MatcherIndicesIndex(41),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(94),
   },
   {
     /* [272] */
@@ -7357,7 +7359,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(94),
   },
   {
     /* [273] */
@@ -7368,7 +7370,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(94),
   },
   {
     /* [274] */
@@ -7379,7 +7381,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(40),
+    /* const_eval_fn */ ConstEvalFunctionIndex(42),
   },
   {
     /* [275] */
@@ -7390,7 +7392,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(226),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(40),
+    /* const_eval_fn */ ConstEvalFunctionIndex(42),
   },
   {
     /* [276] */
@@ -7401,7 +7403,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(227),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(40),
+    /* const_eval_fn */ ConstEvalFunctionIndex(42),
   },
   {
     /* [277] */
@@ -7412,7 +7414,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(230),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(60),
+    /* const_eval_fn */ ConstEvalFunctionIndex(62),
   },
   {
     /* [278] */
@@ -7423,7 +7425,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(222),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(60),
+    /* const_eval_fn */ ConstEvalFunctionIndex(62),
   },
   {
     /* [279] */
@@ -7434,7 +7436,7 @@
     /* templates */ TemplateIndex(33),
     /* parameters */ ParameterIndex(233),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(61),
+    /* const_eval_fn */ ConstEvalFunctionIndex(63),
   },
   {
     /* [280] */
@@ -7445,7 +7447,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(86),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [281] */
@@ -7456,7 +7458,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(386),
     /* return_matcher_indices */ MatcherIndicesIndex(86),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [282] */
@@ -7467,7 +7469,7 @@
     /* templates */ TemplateIndex(54),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(86),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [283] */
@@ -7478,7 +7480,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [284] */
@@ -7489,7 +7491,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [285] */
@@ -7500,7 +7502,7 @@
     /* templates */ TemplateIndex(55),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [286] */
@@ -7511,7 +7513,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(49),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [287] */
@@ -7522,7 +7524,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(375),
     /* return_matcher_indices */ MatcherIndicesIndex(49),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [288] */
@@ -7533,7 +7535,7 @@
     /* templates */ TemplateIndex(56),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(49),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [289] */
@@ -7544,7 +7546,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(9),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [290] */
@@ -7555,7 +7557,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(387),
     /* return_matcher_indices */ MatcherIndicesIndex(9),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [291] */
@@ -7566,7 +7568,7 @@
     /* templates */ TemplateIndex(57),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(9),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [292] */
@@ -7577,7 +7579,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(105),
   },
   {
     /* [293] */
@@ -7588,7 +7590,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(106),
   },
   {
     /* [294] */
@@ -7599,7 +7601,7 @@
     /* templates */ TemplateIndex(58),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(105),
+    /* const_eval_fn */ ConstEvalFunctionIndex(107),
   },
   {
     /* [295] */
@@ -8050,7 +8052,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(22),
+    /* const_eval_fn */ ConstEvalFunctionIndex(24),
   },
   {
     /* [336] */
@@ -8061,7 +8063,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(22),
+    /* const_eval_fn */ ConstEvalFunctionIndex(24),
   },
   {
     /* [337] */
@@ -8072,7 +8074,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(23),
+    /* const_eval_fn */ ConstEvalFunctionIndex(25),
   },
   {
     /* [338] */
@@ -8083,7 +8085,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(23),
+    /* const_eval_fn */ ConstEvalFunctionIndex(25),
   },
   {
     /* [339] */
@@ -8094,7 +8096,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(16),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(24),
+    /* const_eval_fn */ ConstEvalFunctionIndex(26),
   },
   {
     /* [340] */
@@ -8105,7 +8107,7 @@
     /* templates */ TemplateIndex(27),
     /* parameters */ ParameterIndex(150),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(24),
+    /* const_eval_fn */ ConstEvalFunctionIndex(26),
   },
   {
     /* [341] */
@@ -8116,7 +8118,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(26),
+    /* const_eval_fn */ ConstEvalFunctionIndex(28),
   },
   {
     /* [342] */
@@ -8127,7 +8129,7 @@
     /* templates */ TemplateIndex(27),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(26),
+    /* const_eval_fn */ ConstEvalFunctionIndex(28),
   },
   {
     /* [343] */
@@ -8138,7 +8140,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(27),
+    /* const_eval_fn */ ConstEvalFunctionIndex(29),
   },
   {
     /* [344] */
@@ -8149,7 +8151,7 @@
     /* templates */ TemplateIndex(27),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(27),
+    /* const_eval_fn */ ConstEvalFunctionIndex(29),
   },
   {
     /* [345] */
@@ -8160,7 +8162,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(28),
+    /* const_eval_fn */ ConstEvalFunctionIndex(30),
   },
   {
     /* [346] */
@@ -8171,7 +8173,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(28),
+    /* const_eval_fn */ ConstEvalFunctionIndex(30),
   },
   {
     /* [347] */
@@ -8182,7 +8184,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(29),
+    /* const_eval_fn */ ConstEvalFunctionIndex(31),
   },
   {
     /* [348] */
@@ -8193,7 +8195,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(226),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(29),
+    /* const_eval_fn */ ConstEvalFunctionIndex(31),
   },
   {
     /* [349] */
@@ -8204,7 +8206,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(30),
+    /* const_eval_fn */ ConstEvalFunctionIndex(32),
   },
   {
     /* [350] */
@@ -8215,7 +8217,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(30),
+    /* const_eval_fn */ ConstEvalFunctionIndex(32),
   },
   {
     /* [351] */
@@ -8226,7 +8228,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(162),
-    /* const_eval_fn */ ConstEvalFunctionIndex(31),
+    /* const_eval_fn */ ConstEvalFunctionIndex(33),
   },
   {
     /* [352] */
@@ -8237,7 +8239,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(50),
-    /* const_eval_fn */ ConstEvalFunctionIndex(31),
+    /* const_eval_fn */ ConstEvalFunctionIndex(33),
   },
   {
     /* [353] */
@@ -8248,7 +8250,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(15),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(32),
+    /* const_eval_fn */ ConstEvalFunctionIndex(34),
   },
   {
     /* [354] */
@@ -8259,7 +8261,7 @@
     /* templates */ TemplateIndex(27),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(32),
+    /* const_eval_fn */ ConstEvalFunctionIndex(34),
   },
   {
     /* [355] */
@@ -8270,7 +8272,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(33),
+    /* const_eval_fn */ ConstEvalFunctionIndex(35),
   },
   {
     /* [356] */
@@ -8281,7 +8283,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(33),
+    /* const_eval_fn */ ConstEvalFunctionIndex(35),
   },
   {
     /* [357] */
@@ -8292,7 +8294,7 @@
     /* templates */ TemplateIndex(9),
     /* parameters */ ParameterIndex(306),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(34),
+    /* const_eval_fn */ ConstEvalFunctionIndex(36),
   },
   {
     /* [358] */
@@ -8303,7 +8305,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(308),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(34),
+    /* const_eval_fn */ ConstEvalFunctionIndex(36),
   },
   {
     /* [359] */
@@ -8314,7 +8316,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(35),
+    /* const_eval_fn */ ConstEvalFunctionIndex(37),
   },
   {
     /* [360] */
@@ -8325,7 +8327,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(4),
-    /* const_eval_fn */ ConstEvalFunctionIndex(35),
+    /* const_eval_fn */ ConstEvalFunctionIndex(37),
   },
   {
     /* [361] */
@@ -8336,7 +8338,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(36),
+    /* const_eval_fn */ ConstEvalFunctionIndex(38),
   },
   {
     /* [362] */
@@ -8347,7 +8349,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(36),
+    /* const_eval_fn */ ConstEvalFunctionIndex(38),
   },
   {
     /* [363] */
@@ -8358,7 +8360,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(37),
+    /* const_eval_fn */ ConstEvalFunctionIndex(39),
   },
   {
     /* [364] */
@@ -8369,7 +8371,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(37),
+    /* const_eval_fn */ ConstEvalFunctionIndex(39),
   },
   {
     /* [365] */
@@ -8380,7 +8382,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(38),
+    /* const_eval_fn */ ConstEvalFunctionIndex(40),
   },
   {
     /* [366] */
@@ -8391,7 +8393,7 @@
     /* templates */ TemplateIndex(25),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(38),
+    /* const_eval_fn */ ConstEvalFunctionIndex(40),
   },
   {
     /* [367] */
@@ -8402,7 +8404,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(39),
+    /* const_eval_fn */ ConstEvalFunctionIndex(41),
   },
   {
     /* [368] */
@@ -8413,7 +8415,7 @@
     /* templates */ TemplateIndex(25),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(39),
+    /* const_eval_fn */ ConstEvalFunctionIndex(41),
   },
   {
     /* [369] */
@@ -8424,7 +8426,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(164),
-    /* const_eval_fn */ ConstEvalFunctionIndex(41),
+    /* const_eval_fn */ ConstEvalFunctionIndex(43),
   },
   {
     /* [370] */
@@ -8435,7 +8437,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(56),
-    /* const_eval_fn */ ConstEvalFunctionIndex(41),
+    /* const_eval_fn */ ConstEvalFunctionIndex(43),
   },
   {
     /* [371] */
@@ -8446,7 +8448,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(52),
+    /* const_eval_fn */ ConstEvalFunctionIndex(54),
   },
   {
     /* [372] */
@@ -8457,7 +8459,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(52),
+    /* const_eval_fn */ ConstEvalFunctionIndex(54),
   },
   {
     /* [373] */
@@ -8468,7 +8470,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(375),
     /* return_matcher_indices */ MatcherIndicesIndex(49),
-    /* const_eval_fn */ ConstEvalFunctionIndex(53),
+    /* const_eval_fn */ ConstEvalFunctionIndex(55),
   },
   {
     /* [374] */
@@ -8479,7 +8481,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(376),
     /* return_matcher_indices */ MatcherIndicesIndex(47),
-    /* const_eval_fn */ ConstEvalFunctionIndex(53),
+    /* const_eval_fn */ ConstEvalFunctionIndex(55),
   },
   {
     /* [375] */
@@ -8490,7 +8492,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(54),
+    /* const_eval_fn */ ConstEvalFunctionIndex(56),
   },
   {
     /* [376] */
@@ -8501,7 +8503,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(54),
+    /* const_eval_fn */ ConstEvalFunctionIndex(56),
   },
   {
     /* [377] */
@@ -8512,7 +8514,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(57),
+    /* const_eval_fn */ ConstEvalFunctionIndex(59),
   },
   {
     /* [378] */
@@ -8523,7 +8525,7 @@
     /* templates */ TemplateIndex(27),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(57),
+    /* const_eval_fn */ ConstEvalFunctionIndex(59),
   },
   {
     /* [379] */
@@ -8534,7 +8536,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(58),
+    /* const_eval_fn */ ConstEvalFunctionIndex(60),
   },
   {
     /* [380] */
@@ -8545,7 +8547,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(58),
+    /* const_eval_fn */ ConstEvalFunctionIndex(60),
   },
   {
     /* [381] */
@@ -8556,7 +8558,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(59),
+    /* const_eval_fn */ ConstEvalFunctionIndex(61),
   },
   {
     /* [382] */
@@ -8567,7 +8569,7 @@
     /* templates */ TemplateIndex(13),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(59),
+    /* const_eval_fn */ ConstEvalFunctionIndex(61),
   },
   {
     /* [383] */
@@ -8578,7 +8580,7 @@
     /* templates */ TemplateIndex(36),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(62),
+    /* const_eval_fn */ ConstEvalFunctionIndex(64),
   },
   {
     /* [384] */
@@ -8589,7 +8591,7 @@
     /* templates */ TemplateIndex(35),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(62),
+    /* const_eval_fn */ ConstEvalFunctionIndex(64),
   },
   {
     /* [385] */
@@ -8600,7 +8602,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(63),
+    /* const_eval_fn */ ConstEvalFunctionIndex(65),
   },
   {
     /* [386] */
@@ -8611,7 +8613,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(63),
+    /* const_eval_fn */ ConstEvalFunctionIndex(65),
   },
   {
     /* [387] */
@@ -8622,7 +8624,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(64),
+    /* const_eval_fn */ ConstEvalFunctionIndex(66),
   },
   {
     /* [388] */
@@ -8633,7 +8635,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(64),
+    /* const_eval_fn */ ConstEvalFunctionIndex(66),
   },
   {
     /* [389] */
@@ -8644,7 +8646,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(65),
+    /* const_eval_fn */ ConstEvalFunctionIndex(67),
   },
   {
     /* [390] */
@@ -8655,7 +8657,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(226),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(65),
+    /* const_eval_fn */ ConstEvalFunctionIndex(67),
   },
   {
     /* [391] */
@@ -8666,7 +8668,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(66),
+    /* const_eval_fn */ ConstEvalFunctionIndex(68),
   },
   {
     /* [392] */
@@ -8677,7 +8679,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(66),
+    /* const_eval_fn */ ConstEvalFunctionIndex(68),
   },
   {
     /* [393] */
@@ -8688,7 +8690,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(67),
+    /* const_eval_fn */ ConstEvalFunctionIndex(69),
   },
   {
     /* [394] */
@@ -8699,7 +8701,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(67),
+    /* const_eval_fn */ ConstEvalFunctionIndex(69),
   },
   {
     /* [395] */
@@ -8710,7 +8712,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(68),
+    /* const_eval_fn */ ConstEvalFunctionIndex(70),
   },
   {
     /* [396] */
@@ -8721,7 +8723,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(68),
+    /* const_eval_fn */ ConstEvalFunctionIndex(70),
   },
   {
     /* [397] */
@@ -8732,7 +8734,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(69),
+    /* const_eval_fn */ ConstEvalFunctionIndex(71),
   },
   {
     /* [398] */
@@ -8743,7 +8745,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(69),
+    /* const_eval_fn */ ConstEvalFunctionIndex(71),
   },
   {
     /* [399] */
@@ -8754,7 +8756,7 @@
     /* templates */ TemplateIndex(4),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(71),
+    /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
   {
     /* [400] */
@@ -8765,7 +8767,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(71),
+    /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
   {
     /* [401] */
@@ -8842,7 +8844,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(81),
   },
   {
     /* [408] */
@@ -8853,7 +8855,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(235),
     /* return_matcher_indices */ MatcherIndicesIndex(41),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(81),
   },
   {
     /* [409] */
@@ -8864,7 +8866,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(80),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [410] */
@@ -8875,7 +8877,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(80),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [411] */
@@ -8886,7 +8888,7 @@
     /* templates */ TemplateIndex(36),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(81),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [412] */
@@ -8897,7 +8899,7 @@
     /* templates */ TemplateIndex(36),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(81),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [413] */
@@ -8908,7 +8910,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [414] */
@@ -8919,7 +8921,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [415] */
@@ -8930,7 +8932,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(95),
+    /* const_eval_fn */ ConstEvalFunctionIndex(97),
   },
   {
     /* [416] */
@@ -8941,7 +8943,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(95),
+    /* const_eval_fn */ ConstEvalFunctionIndex(97),
   },
   {
     /* [417] */
@@ -8952,7 +8954,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(96),
+    /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
   {
     /* [418] */
@@ -8963,7 +8965,7 @@
     /* templates */ TemplateIndex(32),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(96),
+    /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
   {
     /* [419] */
@@ -8974,7 +8976,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(97),
+    /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
   {
     /* [420] */
@@ -8985,7 +8987,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(97),
+    /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
   {
     /* [421] */
@@ -8996,7 +8998,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(98),
+    /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
   {
     /* [422] */
@@ -9007,7 +9009,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(98),
+    /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
   {
     /* [423] */
@@ -9018,7 +9020,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(99),
+    /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
   {
     /* [424] */
@@ -9029,7 +9031,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(99),
+    /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
   {
     /* [425] */
@@ -9040,7 +9042,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(1),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(100),
+    /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
   {
     /* [426] */
@@ -9051,7 +9053,7 @@
     /* templates */ TemplateIndex(26),
     /* parameters */ ParameterIndex(221),
     /* return_matcher_indices */ MatcherIndicesIndex(156),
-    /* const_eval_fn */ ConstEvalFunctionIndex(100),
+    /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
   {
     /* [427] */
@@ -9062,7 +9064,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(16),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(101),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [428] */
@@ -9073,7 +9075,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(356),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(101),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [429] */
@@ -9084,7 +9086,7 @@
     /* templates */ TemplateIndex(28),
     /* parameters */ ParameterIndex(16),
     /* return_matcher_indices */ MatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(102),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [430] */
@@ -9095,7 +9097,7 @@
     /* templates */ TemplateIndex(42),
     /* parameters */ ParameterIndex(356),
     /* return_matcher_indices */ MatcherIndicesIndex(44),
-    /* const_eval_fn */ ConstEvalFunctionIndex(102),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [431] */
@@ -9150,7 +9152,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(86),
-    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(22),
   },
   {
     /* [436] */
@@ -9161,7 +9163,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(23),
   },
   {
     /* [437] */
@@ -9172,7 +9174,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(226),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(25),
+    /* const_eval_fn */ ConstEvalFunctionIndex(27),
   },
   {
     /* [438] */
@@ -9183,7 +9185,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(42),
+    /* const_eval_fn */ ConstEvalFunctionIndex(44),
   },
   {
     /* [439] */
@@ -9194,7 +9196,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(377),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(43),
+    /* const_eval_fn */ ConstEvalFunctionIndex(45),
   },
   {
     /* [440] */
@@ -9205,7 +9207,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(377),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(44),
+    /* const_eval_fn */ ConstEvalFunctionIndex(46),
   },
   {
     /* [441] */
@@ -9216,7 +9218,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(377),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(45),
+    /* const_eval_fn */ ConstEvalFunctionIndex(47),
   },
   {
     /* [442] */
@@ -9227,7 +9229,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(378),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(46),
+    /* const_eval_fn */ ConstEvalFunctionIndex(48),
   },
   {
     /* [443] */
@@ -9238,7 +9240,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(378),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(47),
+    /* const_eval_fn */ ConstEvalFunctionIndex(49),
   },
   {
     /* [444] */
@@ -9249,7 +9251,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(379),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(48),
+    /* const_eval_fn */ ConstEvalFunctionIndex(50),
   },
   {
     /* [445] */
@@ -9260,7 +9262,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(380),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(49),
+    /* const_eval_fn */ ConstEvalFunctionIndex(51),
   },
   {
     /* [446] */
@@ -9271,7 +9273,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(379),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(50),
+    /* const_eval_fn */ ConstEvalFunctionIndex(52),
   },
   {
     /* [447] */
@@ -9282,7 +9284,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(380),
     /* return_matcher_indices */ MatcherIndicesIndex(78),
-    /* const_eval_fn */ ConstEvalFunctionIndex(51),
+    /* const_eval_fn */ ConstEvalFunctionIndex(53),
   },
   {
     /* [448] */
@@ -9293,7 +9295,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(149),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(55),
+    /* const_eval_fn */ ConstEvalFunctionIndex(57),
   },
   {
     /* [449] */
@@ -9304,7 +9306,7 @@
     /* templates */ TemplateIndex(8),
     /* parameters */ ParameterIndex(227),
     /* return_matcher_indices */ MatcherIndicesIndex(38),
-    /* const_eval_fn */ ConstEvalFunctionIndex(56),
+    /* const_eval_fn */ ConstEvalFunctionIndex(58),
   },
   {
     /* [450] */
@@ -9326,7 +9328,7 @@
     /* templates */ TemplateIndex(11),
     /* parameters */ ParameterIndex(381),
     /* return_matcher_indices */ MatcherIndicesIndex(14),
-    /* const_eval_fn */ ConstEvalFunctionIndex(70),
+    /* const_eval_fn */ ConstEvalFunctionIndex(72),
   },
   {
     /* [452] */
@@ -9337,7 +9339,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(114),
-    /* const_eval_fn */ ConstEvalFunctionIndex(72),
+    /* const_eval_fn */ ConstEvalFunctionIndex(74),
   },
   {
     /* [453] */
@@ -9348,7 +9350,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(114),
-    /* const_eval_fn */ ConstEvalFunctionIndex(73),
+    /* const_eval_fn */ ConstEvalFunctionIndex(75),
   },
   {
     /* [454] */
@@ -9359,7 +9361,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(114),
-    /* const_eval_fn */ ConstEvalFunctionIndex(74),
+    /* const_eval_fn */ ConstEvalFunctionIndex(76),
   },
   {
     /* [455] */
@@ -9370,7 +9372,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(166),
-    /* const_eval_fn */ ConstEvalFunctionIndex(75),
+    /* const_eval_fn */ ConstEvalFunctionIndex(77),
   },
   {
     /* [456] */
@@ -9381,7 +9383,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(166),
-    /* const_eval_fn */ ConstEvalFunctionIndex(76),
+    /* const_eval_fn */ ConstEvalFunctionIndex(78),
   },
   {
     /* [457] */
@@ -9392,7 +9394,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(168),
-    /* const_eval_fn */ ConstEvalFunctionIndex(77),
+    /* const_eval_fn */ ConstEvalFunctionIndex(79),
   },
   {
     /* [458] */
@@ -9403,7 +9405,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
     /* return_matcher_indices */ MatcherIndicesIndex(170),
-    /* const_eval_fn */ ConstEvalFunctionIndex(78),
+    /* const_eval_fn */ ConstEvalFunctionIndex(80),
   },
   {
     /* [459] */
@@ -9469,7 +9471,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(93),
+    /* const_eval_fn */ ConstEvalFunctionIndex(95),
   },
   {
     /* [465] */
@@ -9480,7 +9482,7 @@
     /* templates */ TemplateIndex(/* invalid */),
     /* parameters */ ParameterIndex(224),
     /* return_matcher_indices */ MatcherIndicesIndex(43),
-    /* const_eval_fn */ ConstEvalFunctionIndex(94),
+    /* const_eval_fn */ ConstEvalFunctionIndex(96),
   },
 };
 
diff --git a/src/tint/lang/core/intrinsic/table.cc b/src/tint/lang/core/intrinsic/table.cc
index 7b71604..b3ab2f6 100644
--- a/src/tint/lang/core/intrinsic/table.cc
+++ b/src/tint/lang/core/intrinsic/table.cc
@@ -153,17 +153,6 @@
                                                VectorRef<const core::type::Type*> template_args,
                                                VectorRef<const core::type::Type*> args);
 
-/// Match constructs a new MatchState
-/// @param context the intrinsic context
-/// @param templates the template state used for matcher evaluation
-/// @param overload the overload being evaluated
-/// @param matcher_indices pointer to a list of matcher indices
-MatchState Match(Context& context,
-                 TemplateState& templates,
-                 const OverloadInfo& overload,
-                 const MatcherIndex* matcher_indices,
-                 EvaluationStage earliest_eval_stage);
-
 // Prints the list of candidates for emitting diagnostics
 void PrintCandidates(StyledText& err,
                      Context& context,
@@ -251,11 +240,11 @@
     if (auto* matcher_indices = context.data[match.overload->return_matcher_indices]) {
         Any any;
         return_type =
-            Match(context, match.templates, *match.overload, matcher_indices, earliest_eval_stage)
+            context.Match(match.templates, *match.overload, matcher_indices, earliest_eval_stage)
                 .Type(&any);
         if (TINT_UNLIKELY(!return_type)) {
             StyledText err;
-            err << "MatchState.Match() returned null";
+            err << "MatchState.MatchState() returned null";
             TINT_ICE() << err.Plain();
             return err;
         }
@@ -322,8 +311,8 @@
         auto* type = template_args[i];
         if (auto* matcher_indices = context.data[tmpl.matcher_indices]) {
             // Ensure type matches the template's matcher.
-            type = Match(context, templates, overload, matcher_indices, earliest_eval_stage)
-                       .Type(type);
+            type =
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage).Type(type);
             if (!type) {
                 MATCH_FAILURE(kMismatchedExplicitTemplateTypePenalty);
                 continue;
@@ -344,7 +333,7 @@
     for (size_t p = 0; p < num_params; p++) {
         auto& parameter = context.data[overload.parameters + p];
         auto* matcher_indices = context.data[parameter.matcher_indices];
-        if (!Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+        if (!context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                  .Type(args[p])) {
             MATCH_FAILURE(kMismatchedParamTypePenalty);
         }
@@ -359,7 +348,7 @@
             continue;
         }
 
-        auto matcher = Match(context, templates, overload, matcher_indices, earliest_eval_stage);
+        auto matcher = context.Match(templates, overload, matcher_indices, earliest_eval_stage);
 
         switch (tmpl.kind) {
             case TemplateInfo::Kind::kType: {
@@ -403,7 +392,7 @@
         auto& parameter = context.data[overload.parameters + p];
         auto* matcher_indices = context.data[parameter.matcher_indices];
         auto* ty =
-            Match(context, templates, overload, matcher_indices, earliest_eval_stage).Type(args[p]);
+            context.Match(templates, overload, matcher_indices, earliest_eval_stage).Type(args[p]);
         parameters.Emplace(ty, parameter.usage);
     }
 
@@ -476,15 +465,6 @@
     return std::move(*best);
 }
 
-MatchState Match(Context& context,
-                 TemplateState& templates,
-                 const OverloadInfo& overload,
-                 const MatcherIndex* matcher_indices,
-                 EvaluationStage earliest_eval_stage) {
-    return MatchState{context.types, context.symbols, templates,          context.data,
-                      overload,      matcher_indices, earliest_eval_stage};
-}
-
 void PrintCandidates(StyledText& ss,
                      Context& context,
                      VectorRef<Candidate> candidates,
@@ -548,7 +528,7 @@
             if (i < template_args.Length()) {
                 auto* matcher_indices = context.data[tmpl.matcher_indices];
                 matched = !matcher_indices ||
-                          Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+                          context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                               .Type(template_args[i]);
             }
 
@@ -573,7 +553,7 @@
 
         bool matched = false;
         if (i < args.Length()) {
-            matched = Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+            matched = context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                           .Type(args[i]);
         }
         all_params_match = all_params_match && matched;
@@ -585,7 +565,7 @@
         if (parameter.usage != ParameterUsage::kNone) {
             ss << style::Variable(parameter.usage, ": ");
         }
-        Match(context, templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
 
         ss << style::Code << " ";
         if (matched) {
@@ -598,7 +578,7 @@
     if (overload.return_matcher_indices.IsValid()) {
         ss << " -> ";
         auto* matcher_indices = context.data[overload.return_matcher_indices];
-        Match(context, templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
     }
 
     bool first = true;
@@ -631,11 +611,11 @@
             if (tmpl.kind == TemplateInfo::Kind::kType) {
                 if (auto* ty = templates.Type(i)) {
                     matched =
-                        Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+                        context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                             .Type(ty);
                 }
             } else {
-                matched = Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+                matched = context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                               .Num(templates.Num(i))
                               .IsValid();
             }
@@ -647,10 +627,10 @@
 
             ss << style::Type(tmpl.name) << style::Plain(" is ");
             if (tmpl.kind == TemplateInfo::Kind::kType) {
-                Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                     .PrintType(ss);
             } else {
-                Match(context, templates, overload, matcher_indices, earliest_eval_stage)
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
                     .PrintNum(ss);
             }
         }
diff --git a/src/tint/lang/core/intrinsic/table.h b/src/tint/lang/core/intrinsic/table.h
index 9594c34..ccedb37 100644
--- a/src/tint/lang/core/intrinsic/table.h
+++ b/src/tint/lang/core/intrinsic/table.h
@@ -34,6 +34,7 @@
 
 #include "src/tint/lang/core/binary_op.h"
 #include "src/tint/lang/core/builtin_fn.h"
+#include "src/tint/lang/core/evaluation_stage.h"
 #include "src/tint/lang/core/intrinsic/ctor_conv.h"
 #include "src/tint/lang/core/intrinsic/table_data.h"
 #include "src/tint/lang/core/parameter_usage.h"
@@ -108,6 +109,18 @@
     core::type::Manager& types;
     /// The symbol table
     SymbolTable& symbols;
+
+    /// @returns a MatchState from the context and arguments.
+    /// @param templates the template state used for matcher evaluation
+    /// @param overload the overload being evaluated
+    /// @param matcher_indices pointer to a list of matcher indices
+    MatchState Match(TemplateState& templates,
+                     const OverloadInfo& overload,
+                     const MatcherIndex* matcher_indices,
+                     EvaluationStage earliest_eval_stage) {
+        return MatchState(types, symbols, templates, data, overload, matcher_indices,
+                          earliest_eval_stage);
+    }
 };
 
 /// Candidate holds information about an overload evaluated for resolution.
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index 21ff1b8..067f38c 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -66,6 +66,7 @@
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
 #include "src/tint/lang/wgsl/ast/transform/offset_first_index.h"
@@ -145,6 +146,8 @@
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
+    manager.Add<ast::transform::FoldConstants>();
+
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
     // ExpandCompoundAssignment must come before BuiltinPolyfill
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index fc2a4b8..ffde096 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -63,6 +63,7 @@
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
@@ -185,6 +186,8 @@
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
+    manager.Add<ast::transform::FoldConstants>();
+
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
     // ExpandCompoundAssignment must come before BuiltinPolyfill
diff --git a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
index 00788ed..770f7ce 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
@@ -588,11 +588,11 @@
   float3 fract;
   float3 whole;
 };
-static const modf_result_vec3_f32 c = {(0.5f).xxx, float3(4.0f, 5.0f, 6.0f)};
 [numthreads(1, 1, 1)]
 void test_function() {
   modf_result_vec3_f32 v = {(0.5f).xxx, float3(1.0f, 2.0f, 3.0f)};
-  v = c;
+  modf_result_vec3_f32 tint_symbol = {(0.5f).xxx, float3(4.0f, 5.0f, 6.0f)};
+  v = tint_symbol;
   return;
 }
 )");
@@ -802,11 +802,11 @@
   float3 fract;
   int3 exp;
 };
-static const frexp_result_vec3_f32 c = {float3(0.5625f, 0.6875f, 0.8125f), (3).xxx};
 [numthreads(1, 1, 1)]
 void test_function() {
   frexp_result_vec3_f32 v = {float3(0.75f, 0.625f, 0.875f), int3(1, 2, 2)};
-  v = c;
+  frexp_result_vec3_f32 tint_symbol = {float3(0.5625f, 0.6875f, 0.8125f), (3).xxx};
+  v = tint_symbol;
   return;
 }
 )");
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index a511c62..6b4d13d 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -72,6 +72,7 @@
 #include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
@@ -147,6 +148,8 @@
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
+    manager.Add<ast::transform::FoldConstants>();
+
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
     // ExpandCompoundAssignment must come before BuiltinPolyfill
diff --git a/src/tint/lang/spirv/reader/ast_parser/import_test.cc b/src/tint/lang/spirv/reader/ast_parser/import_test.cc
index 18bb803..029db34 100644
--- a/src/tint/lang/spirv/reader/ast_parser/import_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/import_test.cc
@@ -67,15 +67,7 @@
     p->DeliberatelyInvalidSpirv();
 }
 
-TEST_F(SpvParserImportTest, DISABLED_Import_NonSemantic_IgnoredExtInsts) {
-    // TODO(crbug.com/tint/1789): The NonSemantic.ClspvReflection.1 instruction
-    // set grammar has changed
-    // but the corresponding feature in Clspv has not yet landed.
-    // See:
-    //   https://github.com/KhronosGroup/SPIRV-Headers/pull/308
-    //   https://github.com/google/clspv/pull/925
-    // Disable this test until upstream has settled.
-
+TEST_F(SpvParserImportTest, Import_NonSemantic_IgnoredExtInsts) {
     // This is the clspv-compiled output of this OpenCL C:
     //    kernel void foo(global int*A) { A=A; }
     // It emits NonSemantic.ClspvReflection.1 extended instructions.
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index 5114ce1..72a0bab 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -46,6 +46,7 @@
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
@@ -65,6 +66,8 @@
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
+    manager.Add<ast::transform::FoldConstants>();
+
     if (options.clamp_frag_depth) {
         manager.Add<ast::transform::ClampFragDepth>();
         data.Add<ast::transform::ClampFragDepth::Config>(tint::DepthRangeOffsets{0, 4});
diff --git a/src/tint/lang/spirv/writer/raise/merge_return.cc b/src/tint/lang/spirv/writer/raise/merge_return.cc
index c954430..350afa4 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return.cc
@@ -209,6 +209,12 @@
                 }
             }
         }
+
+        // If this is a non-empty block that still has no terminator, we need to insert an exit
+        // instruction (unless it is the function's top-level block).
+        if (!block->IsEmpty() && !block->Terminator() && block->Parent()) {
+            ExitFromBlock(block);
+        }
     }
 
     /// Transforms a return instruction.
@@ -262,6 +268,14 @@
             block->Append(b.Store(return_val, ret->Args()[0]));
         }
 
+        // Replace the return instruction with an exit instruction.
+        ExitFromBlock(block);
+        ret->Destroy();
+    }
+
+    /// Append an instruction to @p block that will exit from its containing control instruction.
+    /// @param block the instruction to exit from
+    void ExitFromBlock(core::ir::Block* block) {
         // If the outermost control instruction is expecting exit values, then return them as
         // 'undef' values.
         auto* ctrl = block->Parent();
@@ -270,7 +284,6 @@
 
         // Replace the return instruction with an exit instruction.
         block->Append(b.Exit(ctrl, std::move(exit_args)));
-        ret->Destroy();
     }
 
     /// Builds instructions to create a 'if(continue_execution)' conditional.
diff --git a/src/tint/lang/spirv/writer/raise/merge_return_test.cc b/src/tint/lang/spirv/writer/raise/merge_return_test.cc
index 425f1c0..cc18a71 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return_test.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return_test.cc
@@ -508,6 +508,154 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_MergeReturnTest, IfElse_BothSidesReturn_NestedInAnotherIfWithResults) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+
+    b.Append(func->Block(), [&] {
+        auto* outer = b.If(cond);
+        outer->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.f32()));
+        b.Append(outer->True(), [&] {
+            auto* inner = b.If(cond);
+            b.Append(inner->True(), [&] {  //
+                b.Return(func);
+            });
+            b.Append(inner->False(), [&] {  //
+                b.Return(func);
+            });
+            b.Unreachable();
+        });
+        b.Append(outer->False(), [&] {  //
+            b.Return(func);
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    %3:i32, %4:f32 = if %2 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        if %2 [t: %b4, f: %b5] {  # if_2
+          %b4 = block {  # true
+            ret
+          }
+          %b5 = block {  # false
+            ret
+          }
+        }
+        unreachable
+      }
+      %b3 = block {  # false
+        ret
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    %3:i32, %4:f32 = if %2 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        if %2 [t: %b4, f: %b5] {  # if_2
+          %b4 = block {  # true
+            exit_if  # if_2
+          }
+          %b5 = block {  # false
+            exit_if  # if_2
+          }
+        }
+        exit_if undef, undef  # if_1
+      }
+      %b3 = block {  # false
+        exit_if undef, undef  # if_1
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(MergeReturn);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_MergeReturnTest, IfElse_BothSidesReturn_NestedInLoop) {
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({cond});
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            auto* inner = b.If(cond);
+            b.Append(inner->True(), [&] {  //
+                b.Return(func);
+            });
+            b.Append(inner->False(), [&] {  //
+                b.Return(func);
+            });
+            b.Unreachable();
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block {  # body
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            ret
+          }
+          %b4 = block {  # false
+            ret
+          }
+        }
+        unreachable
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%2:bool):void -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block {  # body
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            exit_if  # if_1
+          }
+          %b4 = block {  # false
+            exit_if  # if_1
+          }
+        }
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(MergeReturn);
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(SpirvWriter_MergeReturnTest, IfElse_ThenStatements) {
     auto* global = b.Var(ty.ptr<private_, i32>());
     mod.root_block->Append(global);
diff --git a/src/tint/lang/wgsl/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index 63b67a8..2b40c72 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -41,6 +41,7 @@
 include(lang/wgsl/inspector/BUILD.cmake)
 include(lang/wgsl/intrinsic/BUILD.cmake)
 include(lang/wgsl/ir/BUILD.cmake)
+include(lang/wgsl/ls/BUILD.cmake)
 include(lang/wgsl/program/BUILD.cmake)
 include(lang/wgsl/reader/BUILD.cmake)
 include(lang/wgsl/resolver/BUILD.cmake)
diff --git a/src/tint/lang/wgsl/ast/stage_attribute.cc b/src/tint/lang/wgsl/ast/stage_attribute.cc
index 8369fa6..f3d8da9 100644
--- a/src/tint/lang/wgsl/ast/stage_attribute.cc
+++ b/src/tint/lang/wgsl/ast/stage_attribute.cc
@@ -42,6 +42,16 @@
 StageAttribute::~StageAttribute() = default;
 
 std::string StageAttribute::Name() const {
+    switch (stage) {
+        case PipelineStage::kVertex:
+            return "vertex";
+        case PipelineStage::kFragment:
+            return "fragment";
+        case PipelineStage::kCompute:
+            return "compute";
+        case PipelineStage::kNone:
+            break;
+    }
     return "stage";
 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
index 8f4e427..afec779 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -52,6 +52,7 @@
     "disable_uniformity_analysis.cc",
     "expand_compound_assignment.cc",
     "first_index_offset.cc",
+    "fold_constants.cc",
     "get_insertion_point.cc",
     "hoist_to_decl_before.cc",
     "manager.cc",
@@ -90,6 +91,7 @@
     "disable_uniformity_analysis.h",
     "expand_compound_assignment.h",
     "first_index_offset.h",
+    "fold_constants.h",
     "get_insertion_point.h",
     "hoist_to_decl_before.h",
     "manager.h",
@@ -160,6 +162,7 @@
     "disable_uniformity_analysis_test.cc",
     "expand_compound_assignment_test.cc",
     "first_index_offset_test.cc",
+    "fold_constants_test.cc",
     "get_insertion_point_test.cc",
     "helper_test.h",
     "hoist_to_decl_before_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index dea32eb..4f2817f 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -65,6 +65,8 @@
   lang/wgsl/ast/transform/expand_compound_assignment.h
   lang/wgsl/ast/transform/first_index_offset.cc
   lang/wgsl/ast/transform/first_index_offset.h
+  lang/wgsl/ast/transform/fold_constants.cc
+  lang/wgsl/ast/transform/fold_constants.h
   lang/wgsl/ast/transform/get_insertion_point.cc
   lang/wgsl/ast/transform/get_insertion_point.h
   lang/wgsl/ast/transform/hoist_to_decl_before.cc
@@ -160,6 +162,7 @@
   lang/wgsl/ast/transform/disable_uniformity_analysis_test.cc
   lang/wgsl/ast/transform/expand_compound_assignment_test.cc
   lang/wgsl/ast/transform/first_index_offset_test.cc
+  lang/wgsl/ast/transform/fold_constants_test.cc
   lang/wgsl/ast/transform/get_insertion_point_test.cc
   lang/wgsl/ast/transform/helper_test.h
   lang/wgsl/ast/transform/hoist_to_decl_before_test.cc
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 70f6014..f0626b0 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -70,6 +70,8 @@
     "expand_compound_assignment.h",
     "first_index_offset.cc",
     "first_index_offset.h",
+    "fold_constants.cc",
+    "fold_constants.h",
     "get_insertion_point.cc",
     "get_insertion_point.h",
     "hoist_to_decl_before.cc",
@@ -161,6 +163,7 @@
         "disable_uniformity_analysis_test.cc",
         "expand_compound_assignment_test.cc",
         "first_index_offset_test.cc",
+        "fold_constants_test.cc",
         "get_insertion_point_test.cc",
         "helper_test.h",
         "hoist_to_decl_before_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
index 1b88f20..30b3982 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
@@ -153,6 +153,14 @@
                             << "write to unhandled address space: " << ref->AddressSpace();
                 }
 
+                // If the RHS has side effects (which may contain derivative operations), we need to
+                // hoist it out to a separate declaration so that it does not get masked.
+                auto* rhs = sem.GetVal(assign->rhs);
+                if (rhs->HasSideEffects()) {
+                    hoist_to_decl_before.Add(rhs, assign->rhs,
+                                             HoistToDeclBefore::VariableKind::kLet);
+                }
+
                 // Mask the assignment using the invocation-discarded flag.
                 ctx.Replace(assign, b.If(b.Not(flag), b.Block(ctx.Clone(assign))));
             },
diff --git a/src/tint/lang/wgsl/ast/transform/demote_to_helper_test.cc b/src/tint/lang/wgsl/ast/transform/demote_to_helper_test.cc
index 0af6dd1..ce447c0 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper_test.cc
@@ -1307,5 +1307,93 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DemoteToHelperTest, Assignment_HoistExplicitDerivative) {
+    auto* src = R"(
+@group(0) @binding(0)
+var<storage, read_write> output : array<f32, 4>;
+
+@fragment
+fn foo(@location(0) in : f32) {
+  if (in == 0.0) {
+    discard;
+  }
+  output[u32(in)] = dpdx(in);
+}
+)";
+
+    auto* expect = R"(
+var<private> tint_discarded = false;
+
+@group(0) @binding(0) var<storage, read_write> output : array<f32, 4>;
+
+@fragment
+fn foo(@location(0) in : f32) {
+  if ((in == 0.0)) {
+    tint_discarded = true;
+  }
+  let tint_symbol : f32 = dpdx(in);
+  if (!(tint_discarded)) {
+    output[u32(in)] = tint_symbol;
+  }
+  if (tint_discarded) {
+    discard;
+  }
+}
+)";
+
+    auto got = Run<DemoteToHelper>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(DemoteToHelperTest, Assignment_HoistImplicitDerivative) {
+    auto* src = R"(
+@group(0) @binding(0)
+var<storage, read_write> output : array<vec4f, 4>;
+
+@group(0) @binding(1)
+var t : texture_2d<f32>;
+
+@group(0) @binding(2)
+var s : sampler;
+
+@fragment
+fn foo(@interpolate(flat) @location(0) in : u32) {
+  if (in == 0) {
+    discard;
+  }
+  output[in] = textureSample(t, s, vec2());
+}
+)";
+
+    auto* expect = R"(
+var<private> tint_discarded = false;
+
+@group(0) @binding(0) var<storage, read_write> output : array<vec4f, 4>;
+
+@group(0) @binding(1) var t : texture_2d<f32>;
+
+@group(0) @binding(2) var s : sampler;
+
+@fragment
+fn foo(@interpolate(flat) @location(0) in : u32) {
+  if ((in == 0)) {
+    tint_discarded = true;
+  }
+  let tint_symbol : vec4<f32> = textureSample(t, s, vec2());
+  if (!(tint_discarded)) {
+    output[in] = tint_symbol;
+  }
+  if (tint_discarded) {
+    discard;
+  }
+}
+)";
+
+    auto got = Run<DemoteToHelper>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/fold_constants.cc b/src/tint/lang/wgsl/ast/transform/fold_constants.cc
new file mode 100644
index 0000000..6eeb7d8
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/fold_constants.cc
@@ -0,0 +1,134 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/constant/splat.h"
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/bool.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/utils/rtti/switch.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::FoldConstants);
+
+namespace tint::ast::transform {
+namespace {
+
+struct State {
+    enum class Splat {
+        kAllowed,
+        kDisallowed,
+    };
+
+    const ast::Expression* Constant(const core::constant::Value* c) {
+        auto composite = [&](Splat splat) -> const ast::Expression* {
+            auto ty = FoldConstants::CreateASTTypeFor(ctx, c->Type());
+            if (c->AllZero()) {
+                return b.Call(ty);
+            }
+            if (splat == Splat::kAllowed && c->Is<core::constant::Splat>()) {
+                return b.Call(ty, Constant(c->Index(0)));
+            }
+
+            Vector<const ast::Expression*, 8> els;
+            for (size_t i = 0, n = c->NumElements(); i < n; i++) {
+                els.Push(Constant(c->Index(i)));
+            }
+            return b.Call(ty, std::move(els));
+        };
+
+        return tint::Switch(
+            c->Type(),  //
+            [&](const core::type::AbstractFloat*) { return b.Expr(c->ValueAs<AFloat>()); },
+            [&](const core::type::AbstractInt*) { return b.Expr(c->ValueAs<AInt>()); },
+            [&](const core::type::I32*) { return b.Expr(c->ValueAs<i32>()); },
+            [&](const core::type::U32*) { return b.Expr(c->ValueAs<u32>()); },
+            [&](const core::type::F32*) { return b.Expr(c->ValueAs<f32>()); },
+            [&](const core::type::F16*) { return b.Expr(c->ValueAs<f16>()); },
+            [&](const core::type::Bool*) { return b.Expr(c->ValueAs<bool>()); },
+            [&](const core::type::Array*) { return composite(Splat::kDisallowed); },
+            [&](const core::type::Vector*) { return composite(Splat::kAllowed); },
+            [&](const core::type::Matrix*) { return composite(Splat::kDisallowed); },
+            [&](const core::type::Struct*) { return composite(Splat::kDisallowed); },
+            TINT_ICE_ON_NO_MATCH);
+    }
+
+    Transform::ApplyResult Run() {
+        ctx.ReplaceAll([&](const Expression* expr) -> const Expression* {
+            auto& sem = ctx.src->Sem();
+            auto* ve = sem.Get<sem::ValueExpression>(expr);
+
+            // No value expression SEM node found
+            if (!ve) {
+                return nullptr;
+            }
+
+            auto* cv = ve->ConstantValue();
+
+            // No constant value for this expression
+            if (!cv) {
+                return nullptr;
+            }
+
+            if (cv->Type()->HoldsAbstract() && !cv->Type()->is_float_scalar() &&
+                !cv->Type()->is_signed_integer_scalar() &&
+                !cv->Type()->is_unsigned_integer_scalar()) {
+                return nullptr;
+            }
+
+            return Constant(cv);
+        });
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+
+    const Program& src;
+    ProgramBuilder b;
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+};
+
+}  // namespace
+
+FoldConstants::FoldConstants() = default;
+
+FoldConstants::~FoldConstants() = default;
+
+Transform::ApplyResult FoldConstants::Apply(const Program& src, const DataMap&, DataMap&) const {
+    State s{src, {}};
+    return s.Run();
+}
+
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/fold_constants.h b/src/tint/lang/wgsl/ast/transform/fold_constants.h
new file mode 100644
index 0000000..c9057cd
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/fold_constants.h
@@ -0,0 +1,66 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_CONSTANTS_H_
+#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_CONSTANTS_H_
+
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+
+namespace tint::ast::transform {
+
+/// A transform that replaces constant expressions with the constant values.
+///
+/// # Example
+/// ```
+/// const a = false && true;
+/// const b = sin(1);
+/// ```
+///
+/// ```
+/// const a = false;
+/// const b = 0.841470;
+/// ```
+class FoldConstants final : public Castable<FoldConstants, Transform> {
+  public:
+    /// Constructor
+    FoldConstants();
+
+    /// Destructor
+    ~FoldConstants() override;
+
+    /// @copydoc Transform::Apply
+    ApplyResult Apply(const Program& program,
+                      const DataMap& inputs,
+                      DataMap& outpus) const override;
+
+  private:
+    const ast::Expression* Constant(const core::constant::Value* c);
+};
+
+}  // namespace tint::ast::transform
+
+#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_CONSTANTS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/fold_constants_test.cc b/src/tint/lang/wgsl/ast/transform/fold_constants_test.cc
new file mode 100644
index 0000000..081f90f
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/fold_constants_test.cc
@@ -0,0 +1,440 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/transform/fold_constants.h"
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::ast::transform {
+namespace {
+
+using FoldConstantsTest = TransformTest;
+
+TEST_F(FoldConstantsTest, NoFolding) {
+    auto* src = R"(
+@vertex
+fn main() -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+@vertex
+fn main() -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, Expression) {
+    auto* src = R"(
+fn foo() -> i32 {
+  return (1 + (2 * 4)) - (3 * 5);
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  return -6i;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, Abstract) {
+    auto* src = R"(
+fn foo() {
+  const v = vec4(1, 2, 3, vec3(0)[1 + 1 - 0]);
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  const v = vec4(1, 2, 3, 0);
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, IndexExpression) {
+    auto* src = R"(
+struct S {
+  a : array<i32>,
+}
+
+@group(0) @binding(0) var<storage> s : S;
+
+fn foo() -> i32 {
+  return s.a[(3 * 5) - (1 + (2 * 4))];
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  a : array<i32>,
+}
+
+@group(0i) @binding(0i) var<storage> s : S;
+
+fn foo() -> i32 {
+  return s.a[6i];
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, IndexAccessorToConst) {
+    auto* src = R"(
+const a : array<i32, 4> = array(0, 1, 2, 3);
+
+fn foo() -> i32 {
+  return a[3];
+}
+)";
+
+    auto* expect = R"(
+const a : array<i32, 4i> = array<i32, 4u>(0i, 1i, 2i, 3i);
+
+fn foo() -> i32 {
+  return 3i;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, SplatParam) {
+    auto* src = R"(
+fn foo() -> i32 {
+  let v = vec3(2 + 5 - 4);
+  return v.x;
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  let v = vec3<i32>(3i);
+  return v.x;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, CallParam) {
+    auto* src = R"(
+fn foo() -> i32 {
+  let v = vec3(2 + 5 - 4, 5 * 9, -2 + -3);
+  return v.x;
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  let v = vec3<i32>(3i, 45i, -5i);
+  return v.x;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, LHSIndexExpressionOnAssign) {
+    auto* src = R"(
+fn foo() -> i32 {
+  var v = vec4(0);
+  v[1 * 2 + (3 - 2)] = 1;
+
+  return v[1];
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  var v = vec4<i32>();
+  v[3i] = 1i;
+  return v[1i];
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, LHSIndexExpressionOnCompoundAssign) {
+    auto* src = R"(
+fn foo() -> i32 {
+  var v = vec4(0);
+  v[1 * 2 + (3 - 2)] += 1;
+
+  return v[1];
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  var v = vec4<i32>();
+  v[3i] += 1i;
+  return v[1i];
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, LHSIndexExpressionOnIncrement) {
+    auto* src = R"(
+fn foo() -> i32 {
+  var v = vec4(0);
+  v[1 * 2 + (3 - 2)]++;
+
+  return v[1];
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> i32 {
+  var v = vec4<i32>();
+  v[3i]++;
+  return v[1i];
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, Attribute) {
+    auto* src = R"(
+struct A {
+  @location(1 + 2)
+  v : i32,
+}
+
+@group(0) @binding(0) var<storage> a : A;
+
+fn foo() -> i32 {
+  return a.v;
+}
+)";
+
+    auto* expect = R"(
+struct A {
+  @location(3i)
+  v : i32,
+}
+
+@group(0i) @binding(0i) var<storage> a : A;
+
+fn foo() -> i32 {
+  return a.v;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, ShortCircut) {
+    auto* src = R"(
+fn foo() -> bool {
+  return false && (1 + 2) == 3;
+}
+)";
+
+    auto* expect = R"(
+fn foo() -> bool {
+  return false;
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, BreakIf) {
+    auto* src = R"(
+fn foo() {
+  loop {
+
+    continuing {
+      break if 4 > 3;
+    }
+  }
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  loop {
+
+    continuing {
+      break if true;
+    }
+  }
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, Switch) {
+    auto* src = R"(
+fn foo() {
+  switch(1 + 2 + (3 * 5)) {
+    case 1 + 2, 3 + 4: {
+      break;
+    }
+    default: {
+      break;
+    }
+  }
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  switch(18i) {
+    case 3i, 7i: {
+      break;
+    }
+    default: {
+      break;
+    }
+  }
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, If) {
+    auto* src = R"(
+fn foo() {
+  if 4 - 2 > 3 {
+    return;
+  }
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  if (false) {
+    return;
+  }
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, For) {
+    auto* src = R"(
+fn foo() {
+  for(var i = 2 + (3 * 4); i < 9 + (5 * 10); i = i + (2 * 3)) {
+  }
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  for(var i = 14i; (i < 59i); i = (i + 6i)) {
+  }
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, While) {
+    auto* src = R"(
+fn foo() {
+  while(2 > 4) {
+  }
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  while(false) {
+  }
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, ConstAssert) {
+    auto* src = R"(
+const_assert 2 < (3 + 5);
+)";
+
+    auto* expect = R"(
+const_assert true;
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(FoldConstantsTest, InternalStruct) {
+    auto* src = R"(
+fn foo() {
+  let a = modf(1.5);
+}
+)";
+
+    auto* expect = R"(
+fn foo() {
+  let a = __modf_result_f32(0.5f, 1.0f);
+}
+)";
+
+    auto got = Run<FoldConstants>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/hoist_to_decl_before.cc b/src/tint/lang/wgsl/ast/transform/hoist_to_decl_before.cc
index f1fa3d8..4c9cd1b 100644
--- a/src/tint/lang/wgsl/ast/transform/hoist_to_decl_before.cc
+++ b/src/tint/lang/wgsl/ast/transform/hoist_to_decl_before.cc
@@ -29,16 +29,13 @@
 
 #include <utility>
 
-#include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/lang/wgsl/sem/if_statement.h"
-#include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/lang/wgsl/sem/while_statement.h"
 #include "src/tint/utils/containers/hashmap.h"
-#include "src/tint/utils/containers/reverse.h"
 #include "src/tint/utils/containers/transform.h"
 
 namespace tint::ast::transform {
diff --git a/src/tint/lang/wgsl/ast/transform/std140.cc b/src/tint/lang/wgsl/ast/transform/std140.cc
index 9dc49b5..42fb0f9 100644
--- a/src/tint/lang/wgsl/ast/transform/std140.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140.cc
@@ -456,6 +456,8 @@
         uint32_t size) {
         // Replace the member with column vectors.
         const auto num_columns = mat->columns();
+        const auto column_size = mat->ColumnType()->Size();
+        const auto column_stride = mat->ColumnStride();
         // Build a struct member for each column of the matrix
         tint::Vector<const StructMember*, 4> out;
         for (uint32_t i = 0; i < num_columns; i++) {
@@ -466,10 +468,13 @@
                 // needs to be applied to the first column vector.
                 attributes.Push(b.MemberAlign(i32(align)));
             }
-            if ((i == num_columns - 1) && mat->Size() != size) {
-                // The matrix was @size() annotated with a larger size than the
-                // natural size for the matrix. This extra padding needs to be
-                // applied to the last column vector.
+            if ((i == num_columns - 1) &&
+                (column_stride * (num_columns - 1) + column_size) != size) {
+                // The matrix size is larger than the individual component vectors.
+                // This occurs with matNx3 matrices, as the last vec3 column has space for one extra
+                // trailing scalar, which is occupied by the matrix. It also applies to matrices
+                // with an explicit @size() attribute.
+                // Apply extra padding needs to the last column vector.
                 attributes.Push(
                     b.MemberSize(AInt(size - mat->ColumnType()->Align() * (num_columns - 1))));
             }
diff --git a/src/tint/lang/wgsl/ast/transform/std140_exhaustive_test.cc b/src/tint/lang/wgsl/ast/transform/std140_exhaustive_test.cc
index f9dca89..18e722f 100644
--- a/src/tint/lang/wgsl/ast/transform/std140_exhaustive_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140_exhaustive_test.cc
@@ -80,11 +80,11 @@
     }
 
     // For each column, replaces "${col_id_for_tmpl}" by column index in `tmpl` to get a string, and
-    // join all these strings with `seperator`. If `tmpl_for_last_column` is not empty, use it
+    // join all these strings with `separator`. If `tmpl_for_last_column` is not empty, use it
     // instead of `tmpl` for the last column.
     std::string JoinTemplatedStringForEachMatrixColumn(
         std::string tmpl,
-        std::string seperator,
+        std::string separator,
         std::string tmpl_for_last_column = "") const {
         std::string result;
         if (tmpl_for_last_column.size() == 0) {
@@ -92,13 +92,13 @@
         }
         for (size_t c = 0; c < columns - 1; c++) {
             if (c > 0) {
-                result += seperator;
+                result += separator;
             }
             std::string string_for_current_column =
                 tint::ReplaceAll(tmpl, "${col_id_for_tmpl}", std::to_string(c));
             result += string_for_current_column;
         }
-        result += seperator;
+        result += separator;
         std::string string_for_last_column = tint::ReplaceAll(
             tmpl_for_last_column, "${col_id_for_tmpl}", std::to_string(columns - 1));
         result += string_for_last_column;
@@ -106,13 +106,17 @@
     }
 
     std::string ExpendedColumnVectors(uint32_t leading_space, std::string name) const {
+        if (rows == 3) {
+            return ExpendedColumnVectorsWithLastSize(leading_space, name,
+                                                     type == MatrixType::f16 ? 8 : 16);
+        }
         std::string space(leading_space, ' ');
         return JoinTemplatedStringForEachMatrixColumn(
             space + name + "${col_id_for_tmpl} : " + ColumnVector() + ",", "\n");
     }
 
-    std::string ExpendedColumnVectorsInline(std::string name, std::string seperator) const {
-        return JoinTemplatedStringForEachMatrixColumn(name + "${col_id_for_tmpl}", seperator);
+    std::string ExpendedColumnVectorsInline(std::string name, std::string separator) const {
+        return JoinTemplatedStringForEachMatrixColumn(name + "${col_id_for_tmpl}", separator);
     }
 
     std::string ExpendedColumnVectorsWithLastSize(uint32_t leading_space,
diff --git a/src/tint/lang/wgsl/ast/transform/std140_f16_test.cc b/src/tint/lang/wgsl/ast/transform/std140_f16_test.cc
index 43ddf38..7c17213 100644
--- a/src/tint/lang/wgsl/ast/transform/std140_f16_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140_f16_test.cc
@@ -121,6 +121,7 @@
 
 struct S2x3F16_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -131,6 +132,7 @@
 struct S3x3F16_std140 {
   m_0 : vec3<f16>,
   m_1 : vec3<f16>,
+  @size(8)
   m_2 : vec3<f16>,
 }
 
@@ -142,6 +144,7 @@
   m_0 : vec3<f16>,
   m_1 : vec3<f16>,
   m_2 : vec3<f16>,
+  @size(8)
   m_3 : vec3<f16>,
 }
 
@@ -223,6 +226,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -262,6 +266,7 @@
   before : i32,
   @align(128i)
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
   after : i32,
 }
@@ -380,6 +385,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -426,6 +432,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -461,6 +468,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -493,6 +501,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -539,6 +548,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -571,6 +581,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -617,6 +628,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -649,6 +661,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -696,6 +709,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -729,6 +743,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -782,6 +797,7 @@
 struct S_std140 {
   m_1 : i32,
   m__0 : vec3<f16>,
+  @size(8)
   m__1 : vec3<f16>,
 }
 
@@ -817,6 +833,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -860,6 +877,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -904,6 +922,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -944,6 +963,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -998,6 +1018,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -1038,6 +1059,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -1093,6 +1115,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -1134,6 +1157,7 @@
 
 struct S_std140 {
   m_0 : vec3<f16>,
+  @size(8)
   m_1 : vec3<f16>,
 }
 
@@ -2294,6 +2318,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2337,6 +2362,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2373,6 +2399,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2409,6 +2436,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2441,6 +2469,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2474,6 +2503,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2522,6 +2552,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2572,6 +2603,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2631,6 +2663,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2686,6 +2719,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2734,6 +2768,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2783,6 +2818,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2828,6 +2864,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2874,6 +2911,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2934,6 +2972,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -2988,6 +3027,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3039,6 +3079,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3083,6 +3124,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3128,6 +3170,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3165,6 +3208,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3203,6 +3247,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3241,6 +3286,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3279,6 +3325,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3313,6 +3360,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3362,6 +3410,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3398,6 +3447,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3448,6 +3498,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3484,6 +3535,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3535,6 +3587,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
@@ -3573,6 +3626,7 @@
 
 struct mat2x3_f16 {
   col0 : vec3<f16>,
+  @size(8)
   col1 : vec3<f16>,
 }
 
diff --git a/src/tint/lang/wgsl/builtin_fn.cc b/src/tint/lang/wgsl/builtin_fn.cc
index a601143..78afd1b 100644
--- a/src/tint/lang/wgsl/builtin_fn.cc
+++ b/src/tint/lang/wgsl/builtin_fn.cc
@@ -752,6 +752,18 @@
         case BuiltinFn::kAtomicStore:
         case BuiltinFn::kAtomicSub:
         case BuiltinFn::kAtomicXor:
+        case BuiltinFn::kDpdx:
+        case BuiltinFn::kDpdxCoarse:
+        case BuiltinFn::kDpdxFine:
+        case BuiltinFn::kDpdy:
+        case BuiltinFn::kDpdyCoarse:
+        case BuiltinFn::kDpdyFine:
+        case BuiltinFn::kFwidth:
+        case BuiltinFn::kFwidthCoarse:
+        case BuiltinFn::kFwidthFine:
+        case BuiltinFn::kTextureSample:
+        case BuiltinFn::kTextureSampleBias:
+        case BuiltinFn::kTextureSampleCompare:
         case BuiltinFn::kTextureStore:
         case BuiltinFn::kWorkgroupUniformLoad:
             return true;
diff --git a/src/tint/lang/wgsl/builtin_fn.cc.tmpl b/src/tint/lang/wgsl/builtin_fn.cc.tmpl
index ee359d0..613a862 100644
--- a/src/tint/lang/wgsl/builtin_fn.cc.tmpl
+++ b/src/tint/lang/wgsl/builtin_fn.cc.tmpl
@@ -123,6 +123,18 @@
         case BuiltinFn::kAtomicStore:
         case BuiltinFn::kAtomicSub:
         case BuiltinFn::kAtomicXor:
+        case BuiltinFn::kDpdx:
+        case BuiltinFn::kDpdxCoarse:
+        case BuiltinFn::kDpdxFine:
+        case BuiltinFn::kDpdy:
+        case BuiltinFn::kDpdyCoarse:
+        case BuiltinFn::kDpdyFine:
+        case BuiltinFn::kFwidth:
+        case BuiltinFn::kFwidthCoarse:
+        case BuiltinFn::kFwidthFine:
+        case BuiltinFn::kTextureSample:
+        case BuiltinFn::kTextureSampleBias:
+        case BuiltinFn::kTextureSampleCompare:
         case BuiltinFn::kTextureStore:
         case BuiltinFn::kWorkgroupUniformLoad:
             return true;
diff --git a/src/tint/lang/wgsl/ls/BUILD.bazel b/src/tint/lang/wgsl/ls/BUILD.bazel
new file mode 100644
index 0000000..25ddd43
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/BUILD.bazel
@@ -0,0 +1,179 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "ls",
+  srcs = [
+    "cancel_request.cc",
+    "change_configuration.cc",
+    "definition.cc",
+    "diagnostics.cc",
+    "document.cc",
+    "file.cc",
+    "hover.cc",
+    "initialize.cc",
+    "inlay_hints.cc",
+    "references.cc",
+    "rename.cc",
+    "sem_tokens.cc",
+    "serve.cc",
+    "server.cc",
+    "set_trace.cc",
+    "signature_help.cc",
+    "symbols.cc",
+  ],
+  hdrs = [
+    "file.h",
+    "sem_token.h",
+    "serve.h",
+    "server.h",
+    "utils.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
+    "//src/tint/lang/wgsl/intrinsic",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    
+  ] + select({
+    ":tint_build_tintd": [
+      
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "definition_test.cc",
+    "diagnostics_test.cc",
+    "helpers_test.cc",
+    "helpers_test.h",
+    "hover_test.cc",
+    "inlay_hints_test.cc",
+    "references_test.cc",
+    "rename_test.cc",
+    "sem_tokens_test.cc",
+    "signature_help_test.cc",
+    "symbols_test.cc",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_tintd": [
+      
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_tintd_and_tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/ls",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_tintd",
+  actual = "//src/tint:tint_build_tintd_true",
+)
+
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_tintd_and_tint_build_wgsl_reader",
+    match_all = [
+        ":tint_build_tintd",
+        ":tint_build_wgsl_reader",
+    ],
+)
+
diff --git a/src/tint/lang/wgsl/ls/BUILD.cfg b/src/tint/lang/wgsl/ls/BUILD.cfg
new file mode 100644
index 0000000..d838768
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_tintd && tint_build_wgsl_reader"
+}
diff --git a/src/tint/lang/wgsl/ls/BUILD.cmake b/src/tint/lang/wgsl/ls/BUILD.cmake
new file mode 100644
index 0000000..611c64d
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/BUILD.cmake
@@ -0,0 +1,173 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+################################################################################
+# Target:    tint_lang_wgsl_ls
+# Kind:      lib
+# Condition: TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER
+################################################################################
+tint_add_target(tint_lang_wgsl_ls lib
+  lang/wgsl/ls/cancel_request.cc
+  lang/wgsl/ls/change_configuration.cc
+  lang/wgsl/ls/definition.cc
+  lang/wgsl/ls/diagnostics.cc
+  lang/wgsl/ls/document.cc
+  lang/wgsl/ls/file.cc
+  lang/wgsl/ls/file.h
+  lang/wgsl/ls/hover.cc
+  lang/wgsl/ls/initialize.cc
+  lang/wgsl/ls/inlay_hints.cc
+  lang/wgsl/ls/references.cc
+  lang/wgsl/ls/rename.cc
+  lang/wgsl/ls/sem_token.h
+  lang/wgsl/ls/sem_tokens.cc
+  lang/wgsl/ls/serve.cc
+  lang/wgsl/ls/serve.h
+  lang/wgsl/ls/server.cc
+  lang/wgsl/ls/server.h
+  lang/wgsl/ls/set_trace.cc
+  lang/wgsl/ls/signature_help.cc
+  lang/wgsl/ls/symbols.cc
+  lang/wgsl/ls/utils.h
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_ls lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_common
+  tint_lang_wgsl_features
+  tint_lang_wgsl_intrinsic
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_wgsl_ls lib
+  "thread"
+)
+
+if(TINT_BUILD_TINTD)
+  tint_target_add_external_dependencies(tint_lang_wgsl_ls lib
+    "langsvr"
+  )
+endif(TINT_BUILD_TINTD)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_ls lib
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+################################################################################
+# Target:    tint_lang_wgsl_ls_test
+# Kind:      test
+# Condition: TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER
+################################################################################
+tint_add_target(tint_lang_wgsl_ls_test test
+  lang/wgsl/ls/definition_test.cc
+  lang/wgsl/ls/diagnostics_test.cc
+  lang/wgsl/ls/helpers_test.cc
+  lang/wgsl/ls/helpers_test.h
+  lang/wgsl/ls/hover_test.cc
+  lang/wgsl/ls/inlay_hints_test.cc
+  lang/wgsl/ls/references_test.cc
+  lang/wgsl/ls/rename_test.cc
+  lang/wgsl/ls/sem_tokens_test.cc
+  lang/wgsl/ls/signature_help_test.cc
+  lang/wgsl/ls/symbols_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_ls_test test
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_wgsl_ls_test test
+  "gtest"
+)
+
+if(TINT_BUILD_TINTD)
+  tint_target_add_external_dependencies(tint_lang_wgsl_ls_test test
+    "langsvr"
+  )
+endif(TINT_BUILD_TINTD)
+
+if(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_ls_test test
+    tint_lang_wgsl_ls
+  )
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/ls/BUILD.gn b/src/tint/lang/wgsl/ls/BUILD.gn
new file mode 100644
index 0000000..6dde581
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/BUILD.gn
@@ -0,0 +1,158 @@
+# Copyright 2024 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_tintd && tint_build_wgsl_reader) {
+  libtint_source_set("ls") {
+    sources = [
+      "cancel_request.cc",
+      "change_configuration.cc",
+      "definition.cc",
+      "diagnostics.cc",
+      "document.cc",
+      "file.cc",
+      "file.h",
+      "hover.cc",
+      "initialize.cc",
+      "inlay_hints.cc",
+      "references.cc",
+      "rename.cc",
+      "sem_token.h",
+      "sem_tokens.cc",
+      "serve.cc",
+      "serve.h",
+      "server.cc",
+      "server.h",
+      "set_trace.cc",
+      "signature_help.cc",
+      "symbols.cc",
+      "utils.h",
+    ]
+    deps = [
+      "${tint_src_dir}:thread",
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
+      "${tint_src_dir}/lang/wgsl/intrinsic",
+      "${tint_src_dir}/lang/wgsl/program",
+      "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/id",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_tintd) {
+      deps += [ "${tint_src_dir}:langsvr" ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_tintd && tint_build_wgsl_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "definition_test.cc",
+        "diagnostics_test.cc",
+        "helpers_test.cc",
+        "helpers_test.h",
+        "hover_test.cc",
+        "inlay_hints_test.cc",
+        "references_test.cc",
+        "rename_test.cc",
+        "sem_tokens_test.cc",
+        "signature_help_test.cc",
+        "symbols_test.cc",
+      ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/sem",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/id",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/reflection",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/symbol",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_tintd) {
+        deps += [ "${tint_src_dir}:langsvr" ]
+      }
+
+      if (tint_build_tintd && tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ls" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ls/cancel_request.cc b/src/tint/lang/wgsl/ls/cancel_request.cc
new file mode 100644
index 0000000..1fb6f08
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/cancel_request.cc
@@ -0,0 +1,39 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/lsp.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(const lsp::CancelRequestNotification&) {
+    return langsvr::Success;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/change_configuration.cc b/src/tint/lang/wgsl/ls/change_configuration.cc
new file mode 100644
index 0000000..430a914
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/change_configuration.cc
@@ -0,0 +1,40 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/lsp.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(
+    const lsp::WorkspaceDidChangeConfigurationNotification&) {
+    return langsvr::Success;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/definition.cc b/src/tint/lang/wgsl/ls/definition.cc
new file mode 100644
index 0000000..d20b269
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/definition.cc
@@ -0,0 +1,52 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/ls/utils.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+typename lsp::TextDocumentDefinitionRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentDefinitionRequest& r) {
+    typename lsp::TextDocumentDefinitionRequest::SuccessType result = lsp::Null{};
+
+    if (auto file = files_.Get(r.text_document.uri)) {
+        if (auto def = (*file)->Definition(Conv(r.position))) {
+            lsp::Location loc;
+            loc.range = Conv(def->definition);
+            loc.uri = r.text_document.uri;
+            result = lsp::Definition{loc};
+        }
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/definition_test.cc b/src/tint/lang/wgsl/ls/definition_test.cc
new file mode 100644
index 0000000..f9d9780
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/definition_test.cc
@@ -0,0 +1,175 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+#include "src/tint/utils/text/unicode.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+using LsDefinitionTest = LsTestWithParam<std::string_view>;
+TEST_P(LsDefinitionTest, Definition) {
+    auto parsed = ParseMarkers(GetParam());
+    ASSERT_EQ(parsed.positions.size(), 1u);
+    ASSERT_LE(parsed.ranges.size(), 1u);
+
+    lsp::TextDocumentDefinitionRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+    req.position = parsed.positions[0];
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (parsed.ranges.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::Definition>());
+        auto definition = *res.Get<lsp::Definition>();
+        ASSERT_TRUE(definition.Is<lsp::Location>());
+        EXPECT_THAT(definition.Get<lsp::Location>()->uri, req.text_document.uri);
+        EXPECT_THAT(definition.Get<lsp::Location>()->range, parsed.ranges[0]);
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(,
+                         LsDefinitionTest,
+                         ::testing::ValuesIn(std::vector<std::string_view>{
+                             R"(
+const「CONST」= 42;
+fn f() { _ = ⧘CONST; }
+)",  // =========================================
+                             R"(
+var<private>「VAR」= 42;
+fn f() { _ = V⧘AR; }
+)",  // =========================================
+                             R"(
+override「OVERRIDE」= 42;
+fn f() { _ = OVERRID⧘E; }
+)",  // =========================================
+                             R"(
+struct「STRUCT」{ i : i32 }
+fn f(s : ⧘STRUCT) {}
+)",  // =========================================
+                             R"(
+struct S {「i」: i32 }
+fn f(s : S) { _ = s.⧘i; }
+)",  // =========================================
+                             R"(
+fn f(「p」: i32) { _ = ⧘p; }
+)",  // =========================================
+                             R"(
+fn f() {
+    const「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    let「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var i = 42;
+    {
+        var「i」= 42;
+        _ = ⧘i;
+    }
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var「i」= 42;
+    {
+        var i = 42;
+    }
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+const i = 42;
+fn f() {
+    var「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+const i = 42;
+fn f(「i」: i32) {
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn「a」() {}
+fn b() { ⧘a(); }
+)",  // =========================================
+                             R"(
+fn b() { ⧘a(); }
+fn「a」() {}
+)",  // =========================================
+                             R"(
+fn f() {
+    let「i」= 42;
+    _ = (max(i⧘, 8) * 5);
+}
+)",  // =========================================
+                             R"(
+const C = m⧘ax(1, 2);
+)",  // =========================================
+                             R"(
+const C : i⧘32 = 42;
+)"}));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/diagnostics.cc b/src/tint/lang/wgsl/ls/diagnostics.cc
new file mode 100644
index 0000000..c4d0a90
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/diagnostics.cc
@@ -0,0 +1,63 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "langsvr/session.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+langsvr::Result<langsvr::SuccessType> Server::PublishDiagnostics(File& file) {
+    lsp::TextDocumentPublishDiagnosticsNotification out;
+    out.uri = file.source->path;
+    for (auto& diag : file.program.Diagnostics()) {
+        lsp::Diagnostic d;
+        d.message = diag.message.Plain();
+        d.range = Conv(diag.source.range);
+        switch (diag.severity) {
+            case diag::Severity::Note:
+                d.severity = lsp::DiagnosticSeverity::kInformation;
+                break;
+            case diag::Severity::Warning:
+                d.severity = lsp::DiagnosticSeverity::kWarning;
+                break;
+            case diag::Severity::Error:
+            case diag::Severity::InternalCompilerError:
+            case diag::Severity::Fatal:
+                d.severity = lsp::DiagnosticSeverity::kError;
+                break;
+        }
+        out.diagnostics.push_back(std::move(d));
+    }
+
+    return session_.Send(out);
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/diagnostics_test.cc b/src/tint/lang/wgsl/ls/diagnostics_test.cc
new file mode 100644
index 0000000..4bb9d53
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/diagnostics_test.cc
@@ -0,0 +1,120 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    const std::string_view wgsl;
+    const std::vector<lsp::Diagnostic> diagnostics;
+};
+
+struct Diagnostic : lsp::Diagnostic {
+    explicit Diagnostic(std::string_view msg) { message = msg; }
+
+    Diagnostic& Range(lsp::Uinteger start_line,
+                      lsp::Uinteger start_column,
+                      lsp::Uinteger end_line,
+                      lsp::Uinteger end_column) {
+        range = lsp::Range{{start_line, start_column}, {end_line, end_column}};
+        return *this;
+    }
+    Diagnostic& Severity(lsp::DiagnosticSeverity s) {
+        severity = s;
+        return *this;
+    }
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.wgsl << "'";
+}
+
+using LsDiagnosticsTest = LsTestWithParam<Case>;
+TEST_P(LsDiagnosticsTest, Diagnostics) {
+    lsp::TextDocumentDocumentSymbolRequest req{};
+    req.text_document.uri = OpenDocument(GetParam().wgsl);
+    ASSERT_EQ(diagnostics_.Length(), 1u);
+    auto& notification = diagnostics_[0];
+    EXPECT_EQ(notification.uri, req.text_document.uri);
+    EXPECT_THAT(notification.diagnostics, testing::ContainerEq(GetParam().diagnostics));
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsDiagnosticsTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 "",
+                                 {},
+                             },
+                             {
+                                 "unknown_symbol",
+                                 {
+                                     Diagnostic{"unexpected token"}
+                                         .Range(0, 0, 0, 14)
+                                         .Severity(lsp::DiagnosticSeverity::kError),
+                                 },
+                             },
+                             {
+                                 "fn f() { return; return; }",
+                                 {
+                                     Diagnostic{"code is unreachable"}
+                                         .Range(0, 17, 0, 23)
+                                         .Severity(lsp::DiagnosticSeverity::kWarning),
+                                 },
+                             },
+                             {
+                                 "const A = B; const B = A;",
+                                 {
+                                     Diagnostic{"cyclic dependency found: 'A' -> 'B' -> 'A'"}
+                                         .Range(0, 0, 0, 11)
+                                         .Severity(lsp::DiagnosticSeverity::kError),
+
+                                     Diagnostic{"const 'A' references const 'B' here"}
+                                         .Range(0, 10, 0, 11)
+                                         .Severity(lsp::DiagnosticSeverity::kInformation),
+
+                                     Diagnostic{"const 'B' references const 'A' here"}
+                                         .Range(0, 23, 0, 24)
+                                         .Severity(lsp::DiagnosticSeverity::kInformation),
+                                 },
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/document.cc b/src/tint/lang/wgsl/ls/document.cc
new file mode 100644
index 0000000..457c720
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/document.cc
@@ -0,0 +1,90 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/reader/reader.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+/// @returns the byte offsets of the start of all the lines in @p str.
+std::vector<size_t> LineOffsets(std::string_view str) {
+    std::vector<size_t> offsets;
+    offsets.push_back(0);
+    for (size_t i = 0, n = str.length(); i < n; i++) {
+        if (str[i] == '\n') {
+            offsets.push_back(i + 1);
+        }
+    }
+    return offsets;
+}
+
+}  // namespace
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(
+    const lsp::TextDocumentDidOpenNotification& n) {
+    auto source = std::make_unique<Source::File>(n.text_document.uri, n.text_document.text);
+    auto program = wgsl::reader::Parse(source.get());
+    auto file =
+        std::make_shared<File>(std::move(source), n.text_document.version, std::move(program));
+    files_.Add(n.text_document.uri, file);
+    return PublishDiagnostics(*file);
+}
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(
+    const lsp::TextDocumentDidCloseNotification& n) {
+    files_.Remove(n.text_document.uri);
+    return langsvr::Success;
+}
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(
+    const lsp::TextDocumentDidChangeNotification& n) {
+    auto file = files_.Get(n.text_document.uri);
+    if (!file) {
+        return langsvr::Failure{"document not found"};
+    }
+
+    auto content = (*file)->source->content.data;
+    for (auto& change : n.content_changes) {
+        if (auto* edit = change.Get<lsp::TextDocumentContentChangePartial>()) {
+            std::vector<size_t> line_offsets = LineOffsets(content);
+            size_t start = line_offsets[edit->range.start.line] + edit->range.start.character;
+            size_t end = line_offsets[edit->range.end.line] + edit->range.end.character;
+            content = content.substr(0, start) + edit->text + content.substr(end);
+        }
+    }
+    auto source = std::make_unique<Source::File>(n.text_document.uri, content);
+    auto program = wgsl::reader::Parse(source.get());
+    *file = std::make_shared<File>(std::move(source), n.text_document.version, std::move(program));
+    return PublishDiagnostics(**file);
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/file.cc b/src/tint/lang/wgsl/ls/file.cc
new file mode 100644
index 0000000..223eb8f
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/file.cc
@@ -0,0 +1,196 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <optional>
+#include <utility>
+
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/identifier_expression.h"
+#include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/ls/file.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/function_expression.h"
+#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/type_expression.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::wgsl::ls {
+
+File::File(std::unique_ptr<Source::File>&& source_, int64_t version_, Program program_)
+    : source(std::move(source_)), version(version_), program(std::move(program_)) {
+    nodes.reserve(program.ASTNodes().Count());
+    for (auto* node : program.ASTNodes().Objects()) {
+        nodes.push_back(node);
+    }
+    std::stable_sort(nodes.begin(), nodes.end(), [](const ast::Node* a, const ast::Node* b) {
+        if (a->source.range.begin < b->source.range.begin) {
+            return true;
+        }
+        if (a->source.range.begin > b->source.range.begin) {
+            return false;
+        }
+        if (a->source.range.end < b->source.range.end) {
+            return true;
+        }
+        if (a->source.range.end > b->source.range.end) {
+            return false;
+        }
+        return false;
+    });
+}
+
+std::vector<Source::Range> File::References(Source::Location l, bool include_declaration) {
+    std::vector<Source::Range> references;
+    auto variable_refs = [&](const sem::Variable* v) {
+        if (include_declaration) {
+            references.push_back(v->Declaration()->name->source.range);
+        }
+        for (auto* user : v->Users()) {
+            references.push_back(user->Declaration()->source.range);
+        }
+    };
+    auto function_refs = [&](const sem::Function* f) {
+        if (include_declaration) {
+            references.push_back(f->Declaration()->name->source.range);
+        }
+        for (auto* call : f->CallSites()) {
+            references.push_back(call->Declaration()->target->source.range);
+        }
+    };
+    auto struct_member = [&](const sem::StructMember* m) {
+        if (include_declaration) {
+            references.push_back(m->Declaration()->name->source.range);
+        }
+        for (auto* node : nodes) {
+            if (auto* access = program.Sem().Get<sem::StructMemberAccess>(node)) {
+                if (access->Member() == m) {
+                    references.push_back(access->Declaration()->member->source.range);
+                }
+            }
+        }
+    };
+    auto struct_ = [&](const sem::Struct* s) {
+        if (include_declaration) {
+            references.push_back(s->Declaration()->name->source.range);
+        }
+        for (auto* node : nodes) {
+            if (auto* ident = node->As<ast::IdentifierExpression>()) {
+                if (auto* te = program.Sem().Get<sem::TypeExpression>(node)) {
+                    if (te->Type() == s) {
+                        references.push_back(ident->source.range);
+                    }
+                }
+            }
+        }
+    };
+
+    Switch(
+        Unwrap(NodeAt<CastableBase>(l)),  //
+        [&](const sem::VariableUser* v) { variable_refs(v->Variable()); },
+        [&](const sem::Variable* v) { variable_refs(v); },
+        [&](const sem::FunctionExpression* e) { function_refs(e->Function()); },
+        [&](const sem::Function* f) { function_refs(f); },
+        [&](const sem::StructMemberAccess* a) {
+            if (auto* member = a->Member()->As<sem::StructMember>()) {
+                struct_member(member);
+            }
+        },
+        [&](const sem::StructMember* m) { struct_member(m); },
+        [&](const sem::TypeExpression* te) {
+            return Switch(te->Type(),  //
+                          [&](const sem::Struct* s) { return struct_(s); });
+        });
+    return references;
+}
+
+std::optional<File::DefinitionResult> File::Definition(Source::Location l) {
+    return Switch<std::optional<DefinitionResult>>(
+        Unwrap(NodeAt<CastableBase>(l)),  //
+        [&](const sem::VariableUser* u) {
+            auto* v = u->Variable();
+            return DefinitionResult{
+                v->Declaration()->name->symbol.Name(),
+                v->Declaration()->name->source.range,
+                u->Declaration()->source.range,
+            };
+        },
+        [&](const sem::Variable* v) {
+            return DefinitionResult{
+                v->Declaration()->name->symbol.Name(),
+                v->Declaration()->name->source.range,
+                v->Declaration()->name->source.range,
+            };
+        },
+        [&](const sem::FunctionExpression* e) {
+            auto* f = e->Function();
+            return DefinitionResult{
+                f->Declaration()->name->symbol.Name(),
+                f->Declaration()->name->source.range,
+                e->Declaration()->source.range,
+            };
+        },
+        [&](const sem::Function* f) {
+            return DefinitionResult{
+                f->Declaration()->name->symbol.Name(),
+                f->Declaration()->name->source.range,
+                f->Declaration()->name->source.range,
+            };
+        },
+        [&](const sem::StructMemberAccess* a) -> std::optional<DefinitionResult> {
+            if (auto* m = a->Member()->As<sem::StructMember>()) {
+                return DefinitionResult{
+                    m->Declaration()->name->symbol.Name(),
+                    m->Declaration()->name->source.range,
+                    a->Declaration()->member->source.range,
+                };
+            }
+            return std::nullopt;
+        },
+        [&](const sem::StructMember* m) {
+            return DefinitionResult{
+                m->Declaration()->name->symbol.Name(),
+                m->Declaration()->name->source.range,
+                m->Declaration()->name->source.range,
+            };
+        },
+        [&](const sem::TypeExpression* te) {
+            return Switch<std::optional<DefinitionResult>>(
+                te->Type(),  //
+                [&](const sem::Struct* s) {
+                    return DefinitionResult{
+                        s->Declaration()->name->symbol.Name(),
+                        s->Declaration()->name->source.range,
+                        te->Declaration()->source.range,
+                    };
+                });
+        });
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/file.h b/src/tint/lang/wgsl/ls/file.h
new file mode 100644
index 0000000..7e6c367
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/file.h
@@ -0,0 +1,125 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_FILE_H_
+#define SRC_TINT_LANG_WGSL_LS_FILE_H_
+
+#include <limits>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include "src/tint/lang/wgsl/ast/node.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/expression.h"
+#include "src/tint/lang/wgsl/sem/load.h"
+#include "src/tint/lang/wgsl/sem/materialize.h"
+#include "src/tint/utils/diagnostic/source.h"
+
+namespace tint::wgsl::ls {
+
+/// File represents an open language-server WGSL file ("document").
+class File {
+  public:
+    /// The source file
+    std::unique_ptr<Source::File> source;
+    /// The current version of the file. Incremented with each change.
+    int64_t version = 0;
+    /// The parsed and resolved Program
+    Program program;
+    /// A source-ordered list of AST nodes.
+    std::vector<const ast::Node*> nodes;
+
+    /// Constructor
+    File(std::unique_ptr<Source::File>&& source_, int64_t version_, Program program_);
+
+    /// @returns all the references to the symbol at the location @p l in the file.
+    /// @param l the source location to lookup the symbol.
+    /// @param include_declaration if true, the declaration of @p l will be included in the returned
+    /// list.
+    std::vector<Source::Range> References(Source::Location l, bool include_declaration);
+
+    /// The result of Definition
+    struct DefinitionResult {
+        // The identifier text
+        std::string text;
+        // The source range of the definition identifier
+        Source::Range definition;
+        // The source range of the reference identifier
+        Source::Range reference;
+    };
+
+    /// @returns the definition of the symbol at the location @p l in the file.
+    std::optional<DefinitionResult> Definition(Source::Location l);
+
+    /// Behaviour of NodeAt()
+    enum class UnwrapMode {
+        /// NodeAt will Unwrap() the semantic node when searching for the template type.
+        kNoUnwrap,
+        /// NodeAt will not Unwrap() the semantic node when searching for the template type.
+        kUnwrap
+    };
+
+    // Default UnwrapMode is to unwrap, unless searching for a sem::Materialize or sem::Load
+    template <typename T>
+    static constexpr UnwrapMode DefaultUnwrapMode =
+        (std::is_same_v<T, sem::Materialize> || std::is_same_v<T, sem::Load>)
+            ? UnwrapMode::kNoUnwrap
+            : UnwrapMode::kUnwrap;
+
+    /// @returns the inner-most semantic node at the location @p l in the file.
+    /// @tparam T the type or subtype of the node to scan for.
+    template <typename T = sem::Node, UnwrapMode UNWRAP_MODE = DefaultUnwrapMode<T>>
+    const T* NodeAt(Source::Location l) const {
+        // TODO(bclayton): This is a brute-force search. Optimize.
+        size_t best_len = std::numeric_limits<uint32_t>::max();
+        const T* best_node = nullptr;
+        for (auto* node : nodes) {
+            if (node->source.range.begin.line == node->source.range.end.line &&
+                node->source.range.begin <= l && node->source.range.end >= l) {
+                auto* sem = program.Sem().Get(node);
+                if constexpr (UNWRAP_MODE == UnwrapMode::kUnwrap) {
+                    sem = Unwrap(sem);
+                }
+                if (auto* cast = As<T, CastFlags::kDontErrorOnImpossibleCast>(sem)) {
+                    size_t len = node->source.range.end.column - node->source.range.begin.column;
+                    if (len < best_len) {
+                        best_len = len;
+                        best_node = cast;
+                    }
+                }
+            }
+        }
+        return best_node;
+    }
+};
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_FILE_H_
diff --git a/src/tint/lang/wgsl/ls/helpers_test.cc b/src/tint/lang/wgsl/ls/helpers_test.cc
new file mode 100644
index 0000000..d996285
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/helpers_test.cc
@@ -0,0 +1,86 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+#include <utility>
+
+namespace tint::wgsl::ls {
+
+namespace lsp = langsvr::lsp;
+
+ParsedMarkers ParseMarkers(std::string_view str) {
+    std::stringstream clean;
+    lsp::Position current_position;
+    std::vector<langsvr::lsp::Position> positions;
+    std::vector<langsvr::lsp::Range> ranges;
+    std::optional<langsvr::lsp::Range> current_range;
+    while (!str.empty()) {
+        auto [codepoint, len] =
+            utf8::Decode(reinterpret_cast<const uint8_t*>(str.data()), str.length());
+        if (codepoint == 0 || len == 0) {
+            break;
+        }
+
+        switch (codepoint) {
+            case '\n':
+                current_position.line++;
+                current_position.character = 0;
+                clean << "\n";
+                break;
+            case U"「"[0]:
+                // Range start. Replace with ' '
+                current_position.character++;
+                current_range = lsp::Range{};
+                current_range->start = current_position;
+                clean << ' ';
+                break;
+            case U"」"[0]:
+                // Range end. Replace with ' '
+                if (current_range) {
+                    current_range->end = current_position;
+                    ranges.push_back(*current_range);
+                    current_range.reset();
+                }
+                clean << ' ';
+                current_position.character++;
+                break;
+            case U"⧘"[0]:
+                // Position. Consume
+                positions.push_back(current_position);
+                break;
+            default:
+                clean << str.substr(0, len);
+                current_position.character++;
+                break;
+        }
+        str = str.substr(len);
+    }
+    return ParsedMarkers{std::move(ranges), std::move(positions), clean.str()};
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/helpers_test.h b/src/tint/lang/wgsl/ls/helpers_test.h
new file mode 100644
index 0000000..d98b7a2
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/helpers_test.h
@@ -0,0 +1,110 @@
+
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_HELPERS_TEST_H_
+#define SRC_TINT_LANG_WGSL_LS_HELPERS_TEST_H_
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/result.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+namespace tint::wgsl::ls {
+
+/// A helper base class for language-server tests.
+/// Provides a server and client session, and helpers for 'opening' WGSL documents.
+/// @note use LsTest or LsTestWithParam<T> instead of using this class directly.
+template <typename BASE>
+class LsTestImpl : public BASE {
+  public:
+    /// Constructor
+    /// Initializes the server and client sessions, forming a bi-directional message channel.
+    /// Registers a langsvr::lsp::TextDocumentPublishDiagnosticsNotification handler for the client
+    /// session, so that diagnostic notifications are added to #diagnostics_.
+    LsTestImpl() {
+        server_session_.SetSender(
+            [&](std::string_view msg) { return client_session_.Receive(msg); });
+        client_session_.SetSender(
+            [&](std::string_view msg) { return server_session_.Receive(msg); });
+
+        client_session_.Register(
+            [&](const langsvr::lsp::TextDocumentPublishDiagnosticsNotification& n) {
+                diagnostics_.Push(n);
+                return langsvr::Success;
+            });
+    }
+
+    /// Constructs a langsvr::lsp::TextDocumentDidOpenNotification from @p wgsl and a new, unique
+    /// URI, and sends this to the server via the #client_session_. Once the server has finished
+    /// processing the notification, the server will respond with a
+    /// langsvr::lsp::TextDocumentPublishDiagnosticsNotification, which is handled by the
+    /// #client_session_ and placed into #diagnostics_.
+    std::string OpenDocument(std::string_view wgsl) {
+        std::string uri = "document-" + std::to_string(next_document_id_++) + ".wgsl";
+        langsvr::lsp::TextDocumentDidOpenNotification notification{};
+        notification.text_document.text = wgsl;
+        notification.text_document.uri = uri;
+        auto res = client_session_.Send(notification);
+        EXPECT_EQ(res, langsvr::Success);
+        return uri;
+    }
+
+    langsvr::Session server_session_;
+    langsvr::Session client_session_;
+    Server server_{server_session_};
+    int next_document_id_ = 0;
+    Vector<langsvr::lsp::TextDocumentPublishDiagnosticsNotification, 4> diagnostics_;
+};
+
+using LsTest = LsTestImpl<testing::Test>;
+
+template <typename T>
+using LsTestWithParam = LsTestImpl<testing::TestWithParam<T>>;
+
+/// Result structure of ParseMarkers
+struct ParsedMarkers {
+    /// All parsed ranges, marked up with '「' and '」'. For example: `「my_range」`
+    std::vector<langsvr::lsp::Range> ranges;
+    /// All parsed positions, marked up with '⧘'. For example: `posi⧘tion`
+    std::vector<langsvr::lsp::Position> positions;
+    /// The string with all markup removed.
+    /// '「' and '」' are replaced with whitespace.
+    /// '⧘' are omitted with no replacement characters.
+    std::string clean;
+};
+
+/// ParseMarkers parses location and range markers from the string @p str.
+ParsedMarkers ParseMarkers(std::string_view str);
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_HELPERS_TEST_H_
diff --git a/src/tint/lang/wgsl/ls/hover.cc b/src/tint/lang/wgsl/ls/hover.cc
new file mode 100644
index 0000000..5ffbe47
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/hover.cc
@@ -0,0 +1,262 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string_view>
+#include "src/tint/lang/core/constant/scalar.h"
+#include "src/tint/lang/core/constant/splat.h"
+#include "src/tint/lang/core/constant/value.h"
+#include "src/tint/lang/core/type/void.h"
+#include "src/tint/lang/wgsl/ast/const.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/identifier_expression.h"
+#include "src/tint/lang/wgsl/ast/let.h"
+#include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/ast/override.h"
+#include "src/tint/lang/wgsl/ast/templated_identifier.h"
+#include "src/tint/lang/wgsl/ast/var.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/behavior.h"
+#include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
+#include "src/tint/lang/wgsl/sem/builtin_fn.h"
+#include "src/tint/lang/wgsl/sem/call.h"
+#include "src/tint/lang/wgsl/sem/call_target.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/function_expression.h"
+#include "src/tint/lang/wgsl/sem/load.h"
+#include "src/tint/lang/wgsl/sem/materialize.h"
+#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/type_expression.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/string_stream.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+lsp::MarkedStringWithLanguage WGSL(std::string wgsl) {
+    lsp::MarkedStringWithLanguage str;
+    str.language = "wgsl";
+    str.value = wgsl;
+    return str;
+}
+
+void PrintConstant(const core::constant::Value* val, StringStream& ss) {
+    Switch(
+        val,  //
+        [&](const core::constant::Scalar<AInt>* s) { ss << s->value; },
+        [&](const core::constant::Scalar<AFloat>* s) { ss << s->value; },
+        [&](const core::constant::Scalar<bool>* s) { ss << s->value; },
+        [&](const core::constant::Scalar<f16>* s) { ss << s->value << "h"; },
+        [&](const core::constant::Scalar<f32>* s) { ss << s->value << "f"; },
+        [&](const core::constant::Scalar<i32>* s) { ss << s->value << "i"; },
+        [&](const core::constant::Scalar<u32>* s) { ss << s->value << "u"; },
+        [&](const core::constant::Splat* s) {
+            ss << s->Type()->FriendlyName() << "(";
+            PrintConstant(s->el, ss);
+            ss << ")";
+        },
+        [&](const core::constant::Composite* s) {
+            ss << s->Type()->FriendlyName() << "(";
+            for (size_t i = 0, n = s->elements.Length(); i < n; i++) {
+                if (i > 0) {
+                    ss << ", ";
+                }
+                PrintConstant(s->elements[i], ss);
+            }
+            ss << ")";
+        });
+}
+
+void Variable(const sem::Variable* v, std::vector<lsp::MarkedString>& out) {
+    StringStream ss;
+    auto* kind = Switch(
+        v->Declaration(),                            //
+        [&](const ast::Var*) { return "var"; },      //
+        [&](const ast::Let*) { return "let"; },      //
+        [&](const ast::Const*) { return "const"; },  //
+        [&](const ast::Override*) { return "override"; });
+    if (kind) {
+        ss << kind << " ";
+    }
+    ss << v->Declaration()->name->symbol.NameView() << " : "
+       << v->Type()->UnwrapRef()->FriendlyName();
+
+    if (auto* init = v->Initializer()) {
+        if (auto* val = init->ConstantValue()) {
+            ss << " = ";
+            PrintConstant(val, ss);
+        }
+    }
+
+    out.push_back(WGSL(ss.str()));
+}
+
+void Function(const sem::Function* f, std::vector<lsp::MarkedString>& out) {
+    StringStream ss;
+    ss << "fn " << f->Declaration()->name->symbol.NameView();
+    ss << "(";
+    bool first = true;
+    for (auto* param : f->Parameters()) {
+        if (!first) {
+            ss << ", ";
+        }
+        first = false;
+        ss << param->Declaration()->name->symbol.NameView() << " : "
+           << param->Type()->FriendlyName();
+    }
+    ss << ")";
+    if (auto* ret = f->ReturnType(); !ret->Is<core::type::Void>()) {
+        ss << " -> " << ret->FriendlyName();
+    }
+    out.push_back(WGSL(ss.str()));
+}
+
+void Call(std::string_view name, const sem::Call* c, std::vector<lsp::MarkedString>& out) {
+    StringStream ss;
+    ss << name << "(";
+    bool first = true;
+    for (auto* param : c->Target()->Parameters()) {
+        if (!first) {
+            ss << ", ";
+        }
+        first = false;
+        if (auto* decl = param->Declaration()) {
+            ss << decl->name->symbol.NameView() << " : ";
+        } else if (auto usage = param->Usage(); usage != core::ParameterUsage::kNone) {
+            ss << param->Usage() << " : ";
+        }
+        ss << param->Type()->FriendlyName();
+    }
+    ss << ")";
+    if (auto* ret = c->Target()->ReturnType(); !ret->Is<core::type::Void>()) {
+        ss << " -> " << ret->FriendlyName();
+    }
+    out.push_back(WGSL(ss.str()));
+}
+
+void Constant(const core::constant::Value* val, std::vector<lsp::MarkedString>& out) {
+    StringStream ss;
+    PrintConstant(val, ss);
+    out.push_back(WGSL(ss.str()));
+}
+
+}  // namespace
+
+typename lsp::TextDocumentHoverRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentHoverRequest& r) {
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    auto* node = (*file)->NodeAt<CastableBase, File::UnwrapMode::kNoUnwrap>(Conv(r.position));
+    if (!node) {
+        return lsp::Null{};
+    }
+
+    std::vector<lsp::MarkedString> strings;
+
+    if (auto* materialize = node->As<sem::Materialize>()) {
+        if (auto* val = materialize->ConstantValue()) {
+            Constant(val, strings);
+            lsp::Hover hover;
+            hover.contents = std::move(strings);
+            hover.range = Conv(materialize->Declaration()->source.range);
+            return hover;
+        }
+    }
+
+    langsvr::Optional<lsp::Range> range;
+    Switch(
+        Unwrap(node),  //
+        [&](const sem::VariableUser* user) {
+            Variable(user->Variable(), strings);
+            range = Conv(user->Declaration()->source.range);
+        },
+        [&](const sem::Variable* v) {
+            Variable(v, strings);
+            range = Conv(v->Declaration()->name->source.range);
+        },
+        [&](const sem::Call* c) {
+            Call(c->Declaration()->target->identifier->symbol.NameView(), c, strings);
+            range = Conv(c->Declaration()->target->source.range);
+        },
+        [&](const sem::FunctionExpression* expr) {
+            Function(expr->Function(), strings);
+            range = Conv(expr->Declaration()->source.range);
+        },
+        [&](const sem::BuiltinEnumExpression<wgsl::BuiltinFn>* fn) {
+            if (auto* call = (*file)->NodeAt<sem::Call>(Conv(r.position))) {
+                Call(str(fn->Value()), call, strings);
+            } else {
+                strings.push_back(WGSL(str(fn->Value())));
+            }
+            range = Conv(fn->Declaration()->source.range);
+        },
+        [&](const sem::TypeExpression* expr) {
+            Switch(
+                expr->Type(),  //
+                [&](const sem::Struct* str) {
+                    strings.push_back(WGSL("struct " + str->Name().Name()));
+                },
+                [&](Default) { strings.push_back(WGSL(expr->Type()->FriendlyName())); });
+            range = Conv(expr->Declaration()->source.range);
+        },
+        [&](const sem::StructMemberAccess* access) {
+            if (auto* member = access->Member()->As<sem::StructMember>()) {
+                StringStream ss;
+                ss << member->Declaration()->name->symbol.NameView() << " : "
+                   << member->Type()->FriendlyName();
+                strings.push_back(WGSL(ss.str()));
+                range = Conv(access->Declaration()->member->source.range);
+            }
+        },
+        [&](const sem::ValueExpression* expr) {
+            if (auto* val = expr->ConstantValue()) {
+                Constant(val, strings);
+                range = Conv(expr->Declaration()->source.range);
+            }
+        });
+
+    if (strings.empty()) {
+        return lsp::Null{};
+    }
+
+    lsp::Hover hover;
+    hover.contents = std::move(strings);
+    hover.range = std::move(range);
+    return hover;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/hover_test.cc b/src/tint/lang/wgsl/ls/hover_test.cc
new file mode 100644
index 0000000..048d8da
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/hover_test.cc
@@ -0,0 +1,311 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    std::string_view markup;
+    std::vector<lsp::MarkedString> expected;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsHoverTest = LsTestWithParam<Case>;
+TEST_P(LsHoverTest, Hover) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.positions.size(), 1u);
+
+    lsp::TextDocumentHoverRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+    req.position = parsed.positions[0];
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (GetParam().expected.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+        ASSERT_TRUE(parsed.ranges.empty());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::Hover>());
+        auto& hover = *res.Get<lsp::Hover>();
+        ASSERT_EQ(parsed.ranges.size(), 1u);
+        ASSERT_TRUE(hover.range);
+        EXPECT_EQ(*hover.range, parsed.ranges[0]);
+        ASSERT_TRUE(hover.contents.Is<std::vector<lsp::MarkedString>>());
+        auto& marked_strings = *hover.contents.Get<std::vector<lsp::MarkedString>>();
+        EXPECT_THAT(marked_strings, testing::ContainerEq(GetParam().expected));
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(
+    ,
+    LsHoverTest,
+    ::testing::ValuesIn(std::vector<Case>{
+        {
+            R"(
+const CONST = 42;
+fn f() { _ =「⧘CONST」; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "const CONST : abstract-int = 42"},
+            },
+        },
+        // =========================================
+        {
+            R"(
+var<private> VAR = 42;
+fn f() { _ = 「V⧘AR」; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "var VAR : i32 = 42i"},
+            },
+        },  // =========================================
+        {
+            R"(
+override OVERRIDE = 42;
+fn f() { _ = 「OVERRID⧘E」 ; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "override OVERRIDE : i32 = 42i"},
+            },
+        },  // =========================================
+        {
+            R"(
+struct STRUCT { i : i32 }
+fn f(s : 「⧘STRUCT」 ) {}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "struct STRUCT"},
+            },
+        },  // =========================================
+        {
+            R"(
+struct S { i : i32 }
+fn f(s : S) { _ = s.「⧘i」 ; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "i : i32"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f(p : i32) { _ =「⧘p」 ; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "p : i32"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    const i = 42;
+    _ =「i⧘」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "const i : abstract-int = 42"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    let i = 42;
+    _ = 「⧘i」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "let i : i32 = 42i"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    var i = 10 + 32;
+     i =「i⧘」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "var i : i32 = 42i"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    var i = 10;
+    {
+        var i = 42;
+         i =「i⧘」;
+    }
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "var i : i32 = 42i"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    var i = 10.0;
+    {
+        var i = 42;
+    }
+    i =「i⧘」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "var i : f32 = 10.0f"},
+            },
+        },  // =========================================
+        {
+            R"(
+const i = 42;
+fn f() {
+    var i = 42u;
+     i =「⧘i」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "var i : u32 = 42u"},
+            },
+        },  // =========================================
+        {
+            R"(
+const i = 42;
+fn f(i : i32) {
+    _ =「i⧘」;
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "i : i32"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn a() {}
+fn b() { 「⧘a」(); }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "fn a()"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn b() { 「⧘a」(); }
+fn a() -> i32 { return 1; }
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "fn a() -> i32"},
+            },
+        },  // =========================================
+        {
+            R"(
+fn f() {
+    _ = max(3f,「⧘5」);
+}
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "5.0f"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C =「m⧘ax」(1, 2);
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl",
+                                              "max(abstract-int, abstract-int) -> abstract-int"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C : 「i⧘32」 = 42;
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "i32"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C = 「1 ⧘+ 2」;
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "3"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C = 「(1 + 2) ⧘* 3」;
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "9"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C = (「1 +⧘ 2」) * 3;
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "3"},
+            },
+        },  // =========================================
+        {
+            R"(
+const C : f32 = 「(1 + 2) *⧘ 3」;
+)",
+            {
+                lsp::MarkedStringWithLanguage{"wgsl", "9.0f"},
+            },
+        },
+    }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/initialize.cc b/src/tint/lang/wgsl/ls/initialize.cc
new file mode 100644
index 0000000..00a71bc
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/initialize.cc
@@ -0,0 +1,39 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/lsp.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(const lsp::InitializedNotification&) {
+    return langsvr::Success;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/inlay_hints.cc b/src/tint/lang/wgsl/ls/inlay_hints.cc
new file mode 100644
index 0000000..6e3a76d
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/inlay_hints.cc
@@ -0,0 +1,93 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/primitives.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/ast/variable.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+typename lsp::TextDocumentInlayHintRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentInlayHintRequest& r) {
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    std::vector<lsp::InlayHint> hints;
+    auto& program = (*file)->program;
+    for (auto* decl : program.AST().TypeDecls()) {
+        if (auto* str = program.Sem().Get<sem::Struct>(decl)) {
+            if (!str->UsedAs(core::AddressSpace::kStorage) &&
+                !str->UsedAs(core::AddressSpace::kUniform)) {
+                continue;
+            }
+            for (auto* member : str->Members()) {
+                auto pos = Conv(member->Declaration()->name->source.range.begin);
+                auto add = [&](std::string text) {
+                    lsp::InlayHint hint;
+                    hint.position = pos;
+                    hint.label = text;
+                    hint.padding_left = true;
+                    hint.padding_right = true;
+                    hints.push_back(hint);
+                };
+                add("offset: " + std::to_string(member->Offset()));
+                add("size: " + std::to_string(member->Size()));
+            }
+        }
+    }
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        if (auto* decl = node->As<ast::Variable>()) {
+            if (!decl->type) {
+                if (auto* variable = program.Sem().Get(decl); variable && variable->Type()) {
+                    lsp::InlayHint hint;
+                    hint.position = Conv(decl->name->source.range.end);
+                    hint.label = " : " + variable->Type()->UnwrapRef()->FriendlyName();
+                    hints.push_back(hint);
+                }
+            }
+        }
+    }
+
+    if (hints.empty()) {
+        return lsp::Null{};
+    }
+
+    return std::move(hints);
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/inlay_hints_test.cc b/src/tint/lang/wgsl/ls/inlay_hints_test.cc
new file mode 100644
index 0000000..454f38a
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/inlay_hints_test.cc
@@ -0,0 +1,184 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Hint {
+    std::string_view label;
+    bool pad_left = false;
+    bool pad_right = false;
+};
+
+struct Case {
+    std::string_view markup;
+    std::vector<Hint> hints;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsInlayHintsTest = LsTestWithParam<Case>;
+TEST_P(LsInlayHintsTest, InlayHints) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.ranges.size(), 0u);
+    ASSERT_EQ(parsed.positions.size(), GetParam().hints.size());
+
+    lsp::TextDocumentInlayHintRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (parsed.positions.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<std::vector<langsvr::lsp::InlayHint>>());
+        auto& got_hints = *res.Get<std::vector<lsp::InlayHint>>();
+        std::vector<lsp::InlayHint> expected_hints;
+        for (size_t i = 0; i < parsed.positions.size(); i++) {
+            auto& hint = GetParam().hints[i];
+            lsp::InlayHint expected{};
+            expected.position = parsed.positions[i];
+            expected.label = lsp::String{hint.label};
+            if (hint.pad_left) {
+                expected.padding_left = true;
+            }
+            if (hint.pad_right) {
+                expected.padding_right = true;
+            }
+            expected_hints.push_back(std::move(expected));
+        }
+        EXPECT_EQ(got_hints, expected_hints);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsInlayHintsTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 R"(
+const CONST⧘ = 42;
+)",
+                                 {
+                                     {
+                                         " : abstract-int",
+                                     },
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+var<private> VAR⧘ = 42;
+)",
+                                 {
+                                     {
+                                         " : i32",
+                                     },
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+override OVERRIDE⧘ = 42;
+)",
+                                 {
+                                     {
+                                         " : i32",
+                                     },
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+struct STRUCT {
+    ⧘⧘a : i32,
+    @size(32) ⧘⧘b : i32,
+    ⧘⧘c : i32,
+}
+@group(0) @binding(0) var<storage> v : STRUCT;
+)",
+                                 {
+                                     {"offset: 0", /* pad_left */ true, /* pad_right */ true},
+                                     {"size: 4", /* pad_left */ true, /* pad_right */ true},
+
+                                     {"offset: 4", /* pad_left */ true, /* pad_right */ true},
+                                     {"size: 32", /* pad_left */ true, /* pad_right */ true},
+
+                                     {"offset: 36", /* pad_left */ true, /* pad_right */ true},
+                                     {"size: 4", /* pad_left */ true, /* pad_right */ true},
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+struct STRUCT {
+    i : i32
+}
+)",
+                                 {},
+                             },  // =========================================
+                             {
+                                 R"(
+const CONST : i32 = 42;
+)",
+                                 {},
+                             },  // =========================================
+                             {
+                                 R"(
+var<private> VAR : f32 = 42;
+)",
+                                 {},
+                             },  // =========================================
+                             {
+                                 R"(
+override OVERRIDE : u32 = 42;
+)",
+                                 {},
+                             },  // =========================================
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/references.cc b/src/tint/lang/wgsl/ls/references.cc
new file mode 100644
index 0000000..9eda4d6
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/references.cc
@@ -0,0 +1,57 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+typename lsp::TextDocumentReferencesRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentReferencesRequest& r) {
+    typename lsp::TextDocumentReferencesRequest::SuccessType result = lsp::Null{};
+
+    if (auto file = files_.Get(r.text_document.uri)) {
+        std::vector<lsp::Location> out;
+        for (auto& ref : (*file)->References(Conv(r.position), r.context.include_declaration)) {
+            lsp::Location loc;
+            loc.range = Conv(ref);
+            loc.uri = r.text_document.uri;
+            out.push_back(std::move(loc));
+        }
+        if (!out.empty()) {
+            result = out;
+        }
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/references_test.cc b/src/tint/lang/wgsl/ls/references_test.cc
new file mode 100644
index 0000000..df732ec
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/references_test.cc
@@ -0,0 +1,293 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    bool include_declaration;
+    std::string_view markup;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsReferencesTest = LsTestWithParam<Case>;
+TEST_P(LsReferencesTest, References) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.positions.size(), 1u);
+
+    lsp::TextDocumentReferencesRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+    req.context.include_declaration = GetParam().include_declaration;
+    req.position = parsed.positions[0];
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (parsed.ranges.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<std::vector<lsp::Location>>());
+        std::vector<lsp::Range> got_ranges;
+        for (auto& location : *res.Get<std::vector<lsp::Location>>()) {
+            EXPECT_EQ(location.uri, req.text_document.uri);
+            got_ranges.push_back(location.range);
+        }
+        EXPECT_THAT(got_ranges, testing::UnorderedElementsAreArray(parsed.ranges));
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(IncludeDeclaration,
+                         LsReferencesTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {/* include_declaration */ true, R"(
+const「CONST」= 42;
+fn f() { _ =「⧘CONST」; }
+const C =「CONST」;
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+var<private>「VAR」= 42;
+fn f() { _ =「V⧘AR」; }
+fn g() { _ = 「VAR」; }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+override「OVERRIDE」= 42;
+fn f() { _ =「OVERRID⧘E」+「OVERRIDE」; }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+struct「STRUCT」{ i : i32 }
+fn f(s :「⧘STRUCT」) { var v : 「STRUCT」; }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+struct S {「i」: i32 }
+fn f(s : S) { _ = s.「⧘i」; }
+fn g(s : S) { _ = s.「i」; }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f(「p」: i32) { _ =「⧘p」* 「p」; }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    const「i」= 42;
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    let「i」= 42;
+    _ =「⧘i」+「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    var「i」= 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    var i = 42;
+    {
+        var「i」= 42;
+        「i」=「⧘i」;
+    }
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    var「i」= 42;
+    {
+        var i = 42;
+    }
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+const i = 42;
+fn f() {
+    var「i」= 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+const i = 42;
+fn f(「i」: i32) {
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn「a」() {}
+fn b() { 「⧘a」(); }
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn b() { 「a⧘」(); }
+fn「a」() {}
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+fn f() {
+    let「i」= 42;
+    _ = (max(「i⧘」, 「i」) * 「i」);
+}
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+const C = m⧘ax(1, 2);
+)"},  // =========================================
+                             {/* include_declaration */ true, R"(
+const C : i⧘32 = 42;
+)"},
+                         }));
+
+INSTANTIATE_TEST_SUITE_P(ExcludeDeclaration,
+                         LsReferencesTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {/* include_declaration */ false, R"(
+const CONST = 42;
+fn f() { _ =「⧘CONST」; }
+const C =「CONST」;
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+var<private> VAR = 42;
+fn f() { _ =「V⧘AR」; }
+fn g() { _ = 「VAR」; }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+override OVERRIDE = 42;
+fn f() { _ =「OVERRID⧘E」+「OVERRIDE」; }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+struct STRUCT { i : i32 }
+fn f(s :「⧘STRUCT」) { var v : 「STRUCT」; }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+struct S { i : i32 }
+fn f(s : S) { _ = s.「⧘i」; }
+fn g(s : S) { _ = s.「i」; }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f( p : i32) { _ =「⧘p」* 「p」; }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    const i = 42;
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    let i = 42;
+    _ =「⧘i」+「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    var i = 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    var i = 42;
+    {
+        var i = 42;
+        「i」=「⧘i」;
+    }
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    var i = 42;
+    {
+        var i = 42;
+    }
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+const i = 42;
+fn f() {
+    var i = 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+const i = 42;
+fn f( i : i32) {
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn a() {}
+fn b() { 「⧘a」(); }
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn b() { 「a⧘」(); }
+fn a() {}
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+fn f() {
+    let i = 42;
+    _ = (max(「i⧘」, 「i」) * 「i」);
+}
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+const C = m⧘ax(1, 2);
+)"},  // =========================================
+                             {/* include_declaration */ false, R"(
+const C : i⧘32 = 42;
+)"},
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/rename.cc b/src/tint/lang/wgsl/ls/rename.cc
new file mode 100644
index 0000000..4cf5447
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/rename.cc
@@ -0,0 +1,95 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "langsvr/lsp/primitives.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/identifier_expression.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/function_expression.h"
+#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+typename lsp::TextDocumentPrepareRenameRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentPrepareRenameRequest& r) {
+    typename lsp::TextDocumentPrepareRenameRequest::SuccessType result = lsp::Null{};
+
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    auto def = (*file)->Definition(Conv(r.position));
+    if (!def) {
+        return lsp::Null{};
+    }
+
+    lsp::PrepareRenamePlaceholder out;
+    out.range = Conv(def->reference);
+    out.placeholder = def->text;
+    return lsp::PrepareRenameResult{out};
+}
+
+typename lsp::TextDocumentRenameRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentRenameRequest& r) {
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    if (!(*file)->Definition(Conv(r.position))) {
+        return lsp::Null{};
+    }
+
+    std::vector<lsp::TextEdit> changes;
+    for (auto& ref : (*file)->References(Conv(r.position), /* include_declaration */ true)) {
+        lsp::TextEdit edit;
+        edit.range = Conv(ref);
+        edit.new_text = r.new_name;
+        changes.emplace_back(std::move(edit));
+    }
+
+    if (changes.empty()) {
+        return lsp::Null{};
+    }
+    std::unordered_map<lsp::DocumentUri, std::vector<lsp::TextEdit>> uri_changes;
+    uri_changes[r.text_document.uri] = std::move(changes);
+
+    lsp::WorkspaceEdit edit;
+    edit.changes = std::move(uri_changes);
+    return std::move(edit);
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/rename_test.cc b/src/tint/lang/wgsl/ls/rename_test.cc
new file mode 100644
index 0000000..8e16670
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/rename_test.cc
@@ -0,0 +1,240 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/comparators.h"
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+#include "src/tint/utils/text/string.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    std::string_view markup;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsRenameTest = LsTestWithParam<Case>;
+TEST_P(LsRenameTest, Rename) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.positions.size(), 1u);
+
+    auto uri = OpenDocument(parsed.clean);
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    // lsp::TextDocumentPrepareRenameRequest
+    {
+        lsp::TextDocumentPrepareRenameRequest req{};
+        req.text_document.uri = uri;
+        req.position = parsed.positions[0];
+
+        auto future = client_session_.Send(req);
+        ASSERT_EQ(future, langsvr::Success);
+        auto res = future->get();
+        if (parsed.ranges.empty()) {
+            ASSERT_TRUE(res.Is<lsp::Null>());
+        } else {
+            // Find the range that holds the position marker.
+            auto range =
+                std::find_if(parsed.ranges.begin(), parsed.ranges.end(),
+                             [&](lsp::Range r) { return lsp::ContainsInclusive(r, req.position); });
+            ASSERT_NE(range, parsed.ranges.end());
+            ASSERT_EQ(range->start.line, range->end.line);
+            auto lines = Split(parsed.clean, "\n");
+            ASSERT_LT(range->start.line, lines.Length());
+            auto line = lines[range->start.line];
+
+            ASSERT_TRUE(res.Is<lsp::PrepareRenameResult>());
+            auto& rename_result = *res.Get<lsp::PrepareRenameResult>();
+            ASSERT_TRUE(rename_result.Is<lsp::PrepareRenamePlaceholder>());
+            auto& placeholder = *rename_result.Get<lsp::PrepareRenamePlaceholder>();
+
+            EXPECT_EQ(*range, placeholder.range);
+            auto expected_placeholder =
+                line.substr(range->start.character, range->end.character - range->start.character);
+            EXPECT_EQ(expected_placeholder, placeholder.placeholder);
+        }
+    }
+
+    // lsp::TextDocumentRenameRequest
+    {
+        lsp::TextDocumentRenameRequest req{};
+        req.text_document.uri = uri;
+        req.position = parsed.positions[0];
+        req.new_name = "RENAMED";
+
+        auto future = client_session_.Send(req);
+        ASSERT_EQ(future, langsvr::Success);
+        auto res = future->get();
+        if (parsed.ranges.empty()) {
+            ASSERT_TRUE(res.Is<lsp::Null>());
+        } else {
+            ASSERT_TRUE(res.Is<lsp::WorkspaceEdit>());
+            auto& edit = *res.Get<lsp::WorkspaceEdit>();
+            ASSERT_TRUE(edit.changes);
+            // There should only be one document in the list of changes, which must be the document
+            // we're renaming.
+            ASSERT_EQ(edit.changes->size(), 1u);
+            ASSERT_NE(edit.changes->find(uri), edit.changes->end());
+
+            auto& uri_changes = edit.changes->at(uri);
+            std::vector<lsp::Range> got_ranges;
+            for (auto& text_edit : uri_changes) {
+                EXPECT_EQ(text_edit.new_text, "RENAMED");
+                got_ranges.push_back(text_edit.range);
+            }
+            EXPECT_THAT(got_ranges, testing::UnorderedElementsAreArray(parsed.ranges));
+        }
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(,
+                         LsRenameTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {R"(
+const「CONST」= 42;
+fn f() { _ =「⧘CONST」; }
+const C =「CONST」;
+)"},  // =========================================
+                             {R"(
+var<private>「VAR」= 42;
+fn f() { _ =「V⧘AR」; }
+fn g() { _ = 「VAR」; }
+)"},  // =========================================
+                             {R"(
+override「OVERRIDE」= 42;
+fn f() { _ =「OVERRID⧘E」+「OVERRIDE」; }
+)"},  // =========================================
+                             {R"(
+struct「STRUCT」{ i : i32 }
+fn f(s :「⧘STRUCT」) { var v : 「STRUCT」; }
+)"},  // =========================================
+                             {R"(
+struct S {「i」: i32 }
+fn f(s : S) { _ = s.「⧘i」; }
+fn g(s : S) { _ = s.「i」; }
+)"},  // =========================================
+                             {R"(
+fn f(「p」: i32) { _ =「⧘p」* 「p」; }
+)"},  // =========================================
+                             {R"(
+fn f() {
+    const「i」= 42;
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {R"(
+fn f() {
+    let「i」= 42;
+    _ =「⧘i」+「i」;
+}
+)"},  // =========================================
+                             {R"(
+fn f() {
+    var「i」= 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {R"(
+fn f() {
+    var i = 42;
+    {
+        var「i」= 42;
+        「i」=「⧘i」;
+    }
+}
+)"},  // =========================================
+                             {R"(
+fn f() {
+    var「i」= 42;
+    {
+        var i = 42;
+    }
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {R"(
+const i = 42;
+fn f() {
+    var「i」= 42;
+    「i」=「⧘i」;
+}
+)"},  // =========================================
+                             {R"(
+const i = 42;
+fn f(「i」: i32) {
+    _ =「⧘i」*「i」;
+}
+)"},  // =========================================
+                             {R"(
+fn「a」() {}
+fn b() { 「⧘a」(); }
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {R"(
+fn b() { 「a⧘」(); }
+fn「a」() {}
+fn c() { 「a」(); }
+)"},  // =========================================
+                             {R"(
+fn f() {
+    let「i」= 42;
+    _ = (max(「i⧘」, 「i」) * 「i」);
+}
+)"},  // =========================================
+                             {R"(
+const C = m⧘ax(1, 2);
+)"},  // =========================================
+                             {R"(
+const C : i⧘32 = 42;
+)"},
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/sem_token.h b/src/tint/lang/wgsl/ls/sem_token.h
new file mode 100644
index 0000000..6a10ccc
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/sem_token.h
@@ -0,0 +1,52 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_SEM_TOKEN_H_
+#define SRC_TINT_LANG_WGSL_LS_SEM_TOKEN_H_
+
+#include <array>
+
+namespace tint::wgsl::ls {
+
+/// SemToken is a struct used to hold an enumerator of token types, and their corresponding names.
+struct SemToken {
+    enum Kind {
+        kEnum,
+        kEnumMember,
+        kFunction,
+        kMember,
+        kType,
+        kVariable,
+    };
+    static constexpr std::array kNames{
+        "enum", "enumMember", "function", "member", "type", "variable",
+    };
+};
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_SEM_TOKEN_H_
diff --git a/src/tint/lang/wgsl/ls/sem_tokens.cc b/src/tint/lang/wgsl/ls/sem_tokens.cc
new file mode 100644
index 0000000..3accd26
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/sem_tokens.cc
@@ -0,0 +1,141 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/comparators.h"
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "src/tint/lang/core/builtin_fn.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/identifier_expression.h"
+#include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/ast/struct.h"
+#include "src/tint/lang/wgsl/ast/struct_member.h"
+#include "src/tint/lang/wgsl/ast/type.h"
+#include "src/tint/lang/wgsl/builtin_fn.h"
+#include "src/tint/lang/wgsl/ls/sem_token.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/function_expression.h"
+#include "src/tint/lang/wgsl/sem/type_expression.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+struct Token {
+    lsp::Position position;
+    size_t kind = 0;
+    size_t length = 0;
+};
+
+Token TokenFromRange(const tint::Source::Range& range, SemToken::Kind kind) {
+    Token tok;
+    tok.position = Conv(range.begin);
+    tok.length = range.end.column - range.begin.column;
+    tok.kind = kind;
+    return tok;
+}
+
+std::optional<SemToken::Kind> TokenKindFor(const sem::Expression* expr) {
+    return Switch<std::optional<SemToken::Kind>>(
+        Unwrap(expr),  //
+        [](const sem::TypeExpression*) { return SemToken::kType; },
+        [](const sem::VariableUser*) { return SemToken::kVariable; },
+        [](const sem::FunctionExpression*) { return SemToken::kFunction; },
+        [](const sem::BuiltinEnumExpression<wgsl::BuiltinFn>*) { return SemToken::kFunction; },
+        [](const sem::BuiltinEnumExpressionBase*) { return SemToken::kEnumMember; },
+        [](tint::Default) { return std::nullopt; });
+}
+
+std::vector<Token> Tokens(File& file) {
+    std::vector<Token> tokens;
+    auto& sem = file.program.Sem();
+    for (auto* node : file.nodes) {
+        Switch(
+            node,  //
+            [&](const ast::IdentifierExpression* expr) {
+                if (auto kind = TokenKindFor(sem.Get(expr))) {
+                    tokens.push_back(TokenFromRange(expr->identifier->source.range, *kind));
+                }
+            },
+            [&](const ast::Struct* str) {
+                tokens.push_back(TokenFromRange(str->name->source.range, SemToken::kType));
+            },
+            [&](const ast::StructMember* member) {
+                tokens.push_back(TokenFromRange(member->name->source.range, SemToken::kMember));
+            },
+            [&](const ast::Variable* var) {
+                tokens.push_back(TokenFromRange(var->name->source.range, SemToken::kVariable));
+            },
+            [&](const ast::Function* fn) {
+                tokens.push_back(TokenFromRange(fn->name->source.range, SemToken::kFunction));
+            },
+            [&](const ast::MemberAccessorExpression* a) {
+                tokens.push_back(TokenFromRange(a->member->source.range, SemToken::kMember));
+            });
+    }
+    return tokens;
+}
+
+}  // namespace
+
+typename lsp::TextDocumentSemanticTokensFullRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentSemanticTokensFullRequest& r) {
+    typename lsp::TextDocumentSemanticTokensFullRequest::SuccessType result;
+
+    if (auto file = files_.Get(r.text_document.uri)) {
+        lsp::SemanticTokens out;
+        // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
+        Token last;
+
+        auto tokens = Tokens(**file);
+        std::sort(tokens.begin(), tokens.end(),
+                  [](const Token& a, const Token& b) { return a.position < b.position; });
+
+        for (auto tok : tokens) {
+            if (last.position.line != tok.position.line) {
+                last.position.character = 0;
+            }
+            out.data.push_back(tok.position.line - last.position.line);
+            out.data.push_back(tok.position.character - last.position.character);
+            out.data.push_back(tok.length);
+            out.data.push_back(static_cast<langsvr::lsp::Uinteger>(tok.kind));
+            out.data.push_back(0);  // modifiers
+            last = tok;
+        }
+
+        result = out;
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/sem_tokens_test.cc b/src/tint/lang/wgsl/ls/sem_tokens_test.cc
new file mode 100644
index 0000000..d7fc9a0
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/sem_tokens_test.cc
@@ -0,0 +1,277 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <cstddef>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+#include "src/tint/lang/wgsl/ls/sem_token.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    std::string_view markup;
+    std::vector<SemToken::Kind> tokens;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+struct RangeAndToken {
+    lsp::Range range;
+    SemToken::Kind token;
+
+    bool operator==(const RangeAndToken& other) const {
+        return range == other.range && token == other.token;
+    }
+};
+
+std::ostream& operator<<(std::ostream& stream, const RangeAndToken& rt) {
+    return stream << "\n" << SemToken::kNames[rt.token] << ": " << rt.range;
+}
+
+using LsSemTokensTest = LsTestWithParam<Case>;
+TEST_P(LsSemTokensTest, SemTokens) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.positions.size(), 0u);
+    ASSERT_EQ(parsed.ranges.size(), GetParam().tokens.size());
+
+    lsp::TextDocumentSemanticTokensFullRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (parsed.ranges.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::SemanticTokens>());
+        std::vector<RangeAndToken> expect;
+        for (size_t i = 0; i < parsed.ranges.size(); i++) {
+            expect.push_back(RangeAndToken{parsed.ranges[i], GetParam().tokens[i]});
+        }
+        // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
+        auto& data = res.Get<lsp::SemanticTokens>()->data;
+        ASSERT_EQ(data.size() % 5, 0u);
+        lsp::Position pos{};
+        std::vector<RangeAndToken> got;
+        for (size_t i = 0; i < data.size(); i += 5) {
+            const auto delta_line = data[i + 0];
+            const auto delta_start = data[i + 1];
+            const auto length = data[i + 2];
+            const auto token_type = data[i + 3];
+            const auto modifiers = data[i + 4];
+
+            pos.line += delta_line;
+            pos.character = (delta_line == 0) ? (pos.character + delta_start) : delta_start;
+            lsp::Range range;
+            range.start = pos;
+            range.end = lsp::Position{pos.line, pos.character + length};
+            auto token = static_cast<SemToken::Kind>(token_type);
+            ASSERT_EQ(modifiers, 0u);
+            got.push_back(RangeAndToken{range, token});
+        }
+        EXPECT_EQ(got, expect);
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(,
+                         LsSemTokensTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 R"(
+const「CONST」= 42;
+fn 「f」() { _=「CONST」; }
+const 「C」=「CONST」;
+)",
+                                 {
+                                     /* 'CONST' */ SemToken::kVariable,
+                                     /* 'f'     */ SemToken::kFunction,
+                                     /* 'CONST' */ SemToken::kVariable,
+                                     /* 'C'     */ SemToken::kVariable,
+                                     /* 'CONST' */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+var<「private」>「VAR」= 42;
+fn「f」() { _ =「VAR」; }
+fn「g」() { _ = 「VAR」; }
+)",
+                                 {
+                                     /* 'private' */ SemToken::kEnumMember,
+                                     /* 'VAR'     */ SemToken::kVariable,
+                                     /* 'f'       */ SemToken::kFunction,
+                                     /* 'VAR'     */ SemToken::kVariable,
+                                     /* 'g'       */ SemToken::kFunction,
+                                     /* 'VAR'     */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+override「OVERRIDE」= 42;
+fn「f」() { _ =「OVERRIDE」+「OVERRIDE」; }
+)",
+                                 {
+                                     /* 'OVERRIDE' */ SemToken::kVariable,
+                                     /* 'f'        */ SemToken::kFunction,
+                                     /* 'OVERRIDE' */ SemToken::kVariable,
+                                     /* 'OVERRIDE' */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+struct「STRUCT」{「i」:「i32」}
+fn「f」(「s」:「STRUCT」) { var「v」:「STRUCT」; }
+)",
+                                 {
+                                     /* 'STRUCT' */ SemToken::kType,
+                                     /* 'i'      */ SemToken::kMember,
+                                     /* 'i32'    */ SemToken::kType,
+                                     /* 'f'      */ SemToken::kFunction,
+                                     /* 's'      */ SemToken::kVariable,
+                                     /* 'STRUCT' */ SemToken::kType,
+                                     /* 'v'      */ SemToken::kVariable,
+                                     /* 'STRUCT' */ SemToken::kType,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+struct「S」{「i」: 「i32」 }
+fn「f」(「s」 : 「S」) { _ = 「s」.「i」; }
+fn「g」(「s」 : 「S」) { _ = 「s」.「i」; }
+)",
+                                 {
+                                     /* 'S'      */ SemToken::kType,
+                                     /* 'i'      */ SemToken::kMember,
+                                     /* 'i32'    */ SemToken::kType,
+                                     /* 'f'      */ SemToken::kFunction,
+                                     /* 's'      */ SemToken::kVariable,
+                                     /* 'S'      */ SemToken::kType,
+                                     /* 's'      */ SemToken::kVariable,
+                                     /* 'i'      */ SemToken::kMember,
+                                     /* 'g'      */ SemToken::kFunction,
+                                     /* 's'      */ SemToken::kVariable,
+                                     /* 'S'      */ SemToken::kType,
+                                     /* 's'      */ SemToken::kVariable,
+                                     /* 'i'      */ SemToken::kMember,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+fn「f」(「p」:「i32」) { _ =「p」*「p」; }
+)",
+                                 {
+                                     /* 'f'    */ SemToken::kFunction,
+                                     /* 'p'    */ SemToken::kVariable,
+                                     /* 'i32'  */ SemToken::kType,
+                                     /* 'p'    */ SemToken::kVariable,
+                                     /* 'p'    */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+fn「f」() {
+    const「i」= 42;
+    _ =「i」*「i」;
+}
+)",
+                                 {
+                                     /* 'f' */ SemToken::kFunction,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+fn「f」() {
+    let「i」= 42;
+    _ =「i」+「i」;
+}
+)",
+                                 {
+                                     /* 'f' */ SemToken::kFunction,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+fn「f」() {
+    var「i」= 42;
+    「i」=「i」;
+}
+)",
+                                 {
+                                     /* 'f' */ SemToken::kFunction,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                     /* 'i' */ SemToken::kVariable,
+                                 },
+                             },  // =========================================
+                             {
+                                 R"(
+fn「f」() {
+    let「i」= 42;
+    _ = (「max」(「i」, 「i」) * 「i」);
+}
+)",
+                                 {
+                                     /* 'f'   */ SemToken::kFunction,
+                                     /* 'i'   */ SemToken::kVariable,
+                                     /* 'max' */ SemToken::kFunction,
+                                     /* 'i'   */ SemToken::kVariable,
+                                     /* 'i'   */ SemToken::kVariable,
+                                     /* 'i'   */ SemToken::kVariable,
+                                 },
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/serve.cc b/src/tint/lang/wgsl/ls/serve.cc
new file mode 100644
index 0000000..241eee1
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/serve.cc
@@ -0,0 +1,123 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/serve.h"
+
+#include <stdio.h>
+#include <string>
+
+#include "langsvr/content_stream.h"
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/session.h"
+
+#include "src/tint/lang/wgsl/ls/server.h"
+#include "src/tint/utils/macros/compiler.h"
+#include "src/tint/utils/macros/defer.h"
+
+////////////////////////////////////////////////////////////////////////////////
+// Debug switches
+////////////////////////////////////////////////////////////////////////////////
+#define LOG_TO_FILE 0        // Log all raw protocol messages to "log.txt"
+#define WAIT_FOR_DEBUGGER 0  // Wait for a debugger to attach on startup
+
+#if WAIT_FOR_DEBUGGER
+#include <unistd.h>
+#include <thread>
+#endif
+
+#if LOG_TO_FILE
+#define LOG(msg, ...)                          \
+    {                                          \
+        fprintf(log, msg "\n", ##__VA_ARGS__); \
+        fflush(log);                           \
+    }                                          \
+    TINT_REQUIRE_SEMICOLON
+#else
+#define LOG(...) TINT_REQUIRE_SEMICOLON
+#endif
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+#if LOG_TO_FILE
+FILE* log = nullptr;
+void TintInternalCompilerErrorReporter(const tint::InternalCompilerError& err) {
+    if (log) {
+        LOG("\n--------------------------------------------------------------");
+        LOG("%s:%d %s", err.File(), static_cast<int>(err.Line()), err.Message().c_str());
+        LOG("--------------------------------------------------------------\n");
+    }
+}
+#endif
+
+}  // namespace
+
+Result<SuccessType> Serve(langsvr::Reader& reader, langsvr::Writer& writer) {
+#if LOG_TO_FILE
+    log = fopen("log.txt", "wb");
+    TINT_DEFER(fclose(log));
+    tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+#endif
+
+#if WAIT_FOR_DEBUGGER
+    LOG("waiting for debugger. pid: %s", std::to_string(getpid()).c_str());
+    std::this_thread::sleep_for(std::chrono::seconds(10));
+#endif
+
+    langsvr::Session session;
+    session.SetSender([&](std::string_view response) {  //
+        LOG("<< %s", std::string(response).c_str());
+        return langsvr::WriteContent(writer, response);
+    });
+
+    Server server(session);
+
+    LOG("Running...");
+
+    while (!server.ShuttingDown()) {
+        auto msg = langsvr::ReadContent(reader);
+        if (msg != langsvr::Success) {
+            LOG("ERROR: %s", msg.Failure().reason.c_str());
+            break;
+        }
+        LOG(">> %s", msg.Get().c_str());
+
+        auto res = session.Receive(msg.Get());
+        if (res != langsvr::Success) {
+            LOG("ERROR: %s", res.Failure().reason.c_str());
+            break;
+        }
+
+        LOG("----------------");
+    }
+
+    LOG("Shutting down");
+    return Success;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/serve.h b/src/tint/lang/wgsl/ls/serve.h
new file mode 100644
index 0000000..774d119
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/serve.h
@@ -0,0 +1,43 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_SERVE_H_
+#define SRC_TINT_LANG_WGSL_LS_SERVE_H_
+
+#include "langsvr/reader.h"
+#include "langsvr/writer.h"
+#include "src/tint/utils/result/result.h"
+
+namespace tint::wgsl::ls {
+
+/// Serve creates a WGSL language server that reads from @p reader and writes to @p writer.
+/// Blocks until the server is shutdown by the client.
+Result<SuccessType> Serve(langsvr::Reader& reader, langsvr::Writer& writer);
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_SERVE_H_
diff --git a/src/tint/lang/wgsl/ls/server.cc b/src/tint/lang/wgsl/ls/server.cc
new file mode 100644
index 0000000..6b526de
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/server.cc
@@ -0,0 +1,116 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "langsvr/session.h"
+
+#include "src/tint/lang/wgsl/ls/sem_token.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+Server::Server(langsvr::Session& session) : session_(session) {
+    session.Register([&](const lsp::InitializeRequest&) {
+        lsp::InitializeResult result;
+        result.capabilities.definition_provider = true;
+        result.capabilities.document_symbol_provider = [] {
+            lsp::DocumentSymbolOptions opts;
+            return opts;
+        }();
+        result.capabilities.hover_provider = true;
+        result.capabilities.inlay_hint_provider = true;
+        result.capabilities.references_provider = [] {
+            lsp::ReferenceOptions opts;
+            return opts;
+        }();
+        result.capabilities.text_document_sync = [] {
+            lsp::TextDocumentSyncOptions opts;
+            opts.open_close = true;
+            opts.change = lsp::TextDocumentSyncKind::kIncremental;
+            return opts;
+        }();
+        result.capabilities.rename_provider = [] {
+            lsp::RenameOptions opts;
+            opts.prepare_provider = true;
+            return opts;
+        }();
+        result.capabilities.semantic_tokens_provider = [] {
+            lsp::SemanticTokensOptions opts;
+            opts.full = true;
+            for (auto name : SemToken::kNames) {
+                opts.legend.token_types.push_back(name);
+            }
+            return opts;
+        }();
+        result.capabilities.signature_help_provider = [] {
+            lsp::SignatureHelpOptions opts;
+            return opts;
+        }();
+        return result;
+    });
+
+    session.Register([&](const lsp::ShutdownRequest&) {
+        shutting_down_ = true;
+        return lsp::Null{};
+    });
+
+    // Notification handlers
+    session.Register([&](const lsp::CancelRequestNotification& n) { return Handle(n); });
+    session.Register([&](const lsp::InitializedNotification& n) { return Handle(n); });
+    session.Register([&](const lsp::SetTraceNotification& n) { return Handle(n); });
+    session.Register([&](const lsp::TextDocumentDidChangeNotification& n) { return Handle(n); });
+    session.Register([&](const lsp::TextDocumentDidCloseNotification& n) { return Handle(n); });
+    session.Register([&](const lsp::TextDocumentDidOpenNotification& n) { return Handle(n); });
+    session.Register(
+        [&](const lsp::WorkspaceDidChangeConfigurationNotification& n) { return Handle(n); });
+
+    // Request handlers
+    session.Register([&](const lsp::TextDocumentDefinitionRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentDocumentSymbolRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentHoverRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentInlayHintRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentPrepareRenameRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentReferencesRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentRenameRequest& r) { return Handle(r); });
+    session.Register(
+        [&](const lsp::TextDocumentSemanticTokensFullRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentSignatureHelpRequest& r) { return Handle(r); });
+}
+
+Server::~Server() = default;
+
+Server::Logger::~Logger() {
+    lsp::WindowLogMessageNotification n;
+    n.type = lsp::MessageType::kLog;
+    n.message = msg.str();
+    (void)session.Send(n);
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/server.h b/src/tint/lang/wgsl/ls/server.h
new file mode 100644
index 0000000..e0df2f3
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/server.h
@@ -0,0 +1,165 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_SERVER_H_
+#define SRC_TINT_LANG_WGSL_LS_SERVER_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/session.h"
+
+#include "src/tint/lang/wgsl/ls/file.h"
+#include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace tint::wgsl::ls {
+
+/// The language server state object.
+class Server {
+  public:
+    /// Constructor
+    /// @param session the LSP session.
+    explicit Server(langsvr::Session& session);
+
+    /// Destructor
+    ~Server();
+
+    /// @returns true if the server has been requested to shut down.
+    bool ShuttingDown() const { return shutting_down_; }
+
+  private:
+    ////////////////////////////////////////////////////////////////////////////
+    // Requests
+    ////////////////////////////////////////////////////////////////////////////
+
+    /// Handler for langsvr::lsp::TextDocumentDefinitionRequest
+    typename langsvr::lsp::TextDocumentDefinitionRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentDefinitionRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentDocumentSymbolRequest
+    typename langsvr::lsp::TextDocumentDocumentSymbolRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentDocumentSymbolRequest& r);
+
+    /// Handler for langsvr::lsp::TextDocumentHoverRequest
+    typename langsvr::lsp::TextDocumentHoverRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentHoverRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentInlayHintRequest
+    typename langsvr::lsp::TextDocumentInlayHintRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentInlayHintRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentPrepareRenameRequest
+    typename langsvr::lsp::TextDocumentPrepareRenameRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentPrepareRenameRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentReferencesRequest
+    typename langsvr::lsp::TextDocumentReferencesRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentReferencesRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentRenameRequest
+    typename langsvr::lsp::TextDocumentRenameRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentRenameRequest&);
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Notifications
+    ////////////////////////////////////////////////////////////////////////////
+
+    /// Handler for langsvr::lsp::CancelRequestNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::CancelRequestNotification&);
+
+    /// Handler for langsvr::lsp::InitializedNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::InitializedNotification&);
+
+    /// Handler for langsvr::lsp::SetTraceNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::SetTraceNotification&);
+
+    /// Handler for langsvr::lsp::TextDocumentSignatureHelpRequest
+    typename langsvr::lsp::TextDocumentSignatureHelpRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentSignatureHelpRequest&);
+
+    /// Handler for langsvr::lsp::TextDocumentDidOpenNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::TextDocumentDidOpenNotification&);
+
+    /// Handler for langsvr::lsp::TextDocumentDidCloseNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::TextDocumentDidCloseNotification&);
+
+    /// Handler for langsvr::lsp::TextDocumentDidChangeNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::TextDocumentDidChangeNotification&);
+
+    /// Handler for langsvr::lsp::TextDocumentSemanticTokensFullRequest
+    typename langsvr::lsp::TextDocumentSemanticTokensFullRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentSemanticTokensFullRequest&);
+
+    /// Handler for langsvr::lsp::WorkspaceDidChangeConfigurationNotification
+    langsvr::Result<langsvr::SuccessType>  //
+    Handle(const langsvr::lsp::WorkspaceDidChangeConfigurationNotification&);
+
+    /// Publishes the tint::Program diagnostics to the server via a
+    /// TextDocumentPublishDiagnosticsNotification.
+    langsvr::Result<langsvr::SuccessType>  //
+    PublishDiagnostics(File& file);
+
+    /// Logger is a string-stream like utility for logging to the client.
+    /// Append message content with '<<'. The message is sent when the logger is destructed.
+    struct Logger {
+        ~Logger();
+
+        /// @brief Appends @p value to the log message
+        /// @return this logger
+        template <typename T>
+        Logger& operator<<(T&& value) {
+            msg << std::forward<T>(value);
+            return *this;
+        }
+
+        langsvr::Session& session;
+        StringStream msg{};
+    };
+
+    /// Log constructs a new Logger to send a log message to the client.
+    Logger Log() { return Logger{session_}; }
+
+    /// The LSP session.
+    langsvr::Session& session_;
+    /// Map of URI to File.
+    Hashmap<std::string, std::shared_ptr<File>, 8> files_;
+    /// True if the server has been asked to shutdown.
+    bool shutting_down_ = false;
+};
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_SERVER_H_
diff --git a/src/tint/lang/wgsl/ls/set_trace.cc b/src/tint/lang/wgsl/ls/set_trace.cc
new file mode 100644
index 0000000..477b3d7
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/set_trace.cc
@@ -0,0 +1,39 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "langsvr/lsp/lsp.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+langsvr::Result<langsvr::SuccessType> Server::Handle(const lsp::SetTraceNotification&) {
+    return langsvr::Success;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/signature_help.cc b/src/tint/lang/wgsl/ls/signature_help.cc
new file mode 100644
index 0000000..401e621
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/signature_help.cc
@@ -0,0 +1,213 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/core/intrinsic/table.h"
+#include "src/tint/lang/wgsl/intrinsic/dialect.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/call.h"
+#include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+std::vector<lsp::ParameterInformation> Params(const core::intrinsic::TableData& data,
+                                              const core::intrinsic::OverloadInfo& overload) {
+    std::vector<lsp::ParameterInformation> params;
+    for (size_t i = 0; i < overload.num_parameters; i++) {
+        lsp::ParameterInformation param_out;
+        auto& param_in = data[overload.parameters + i];
+        if (param_in.usage != core::ParameterUsage::kNone) {
+            param_out.label = std::string(core::ToString(param_in.usage));
+        } else {
+            param_out.label = "param-" + std::to_string(i);
+        }
+        params.push_back(std::move(param_out));
+    }
+    return params;
+}
+
+size_t CalcParamIndex(const Source& call_source, const Source::Location& carat) {
+    size_t index = 0;
+    int depth = 0;
+
+    auto start = call_source.range.begin;
+    auto end = std::min(call_source.range.end, carat);
+    auto& lines = call_source.file->content.lines;
+
+    for (auto line = start.line; line <= end.line; line++) {
+        auto start_column = line == start.line ? start.column : 0;
+        auto end_column = line == end.line ? end.column : 0;
+        auto text = lines[line - 1].substr(start_column - 1, end_column - start_column);
+        for (char c : text) {
+            switch (c) {
+                case '(':
+                case '[':
+                    depth++;
+                    break;
+                case ')':
+                case ']':
+                    depth--;
+                    break;
+                case ',':
+                    if (depth == 1) {
+                        index++;
+                    }
+            }
+        }
+    }
+    return index;
+}
+
+void PrintOverload(StyledText& ss,
+                   core::intrinsic::Context& context,
+                   const core::intrinsic::OverloadInfo& overload,
+                   std::string_view intrinsic_name) {
+    // Restore old style before returning.
+    auto prev_style = ss.Style();
+    TINT_DEFER(ss << prev_style);
+
+    core::intrinsic::TemplateState templates;
+
+    auto earliest_eval_stage = core::EvaluationStage::kConstant;
+
+    ss << style::Code << style::Function(intrinsic_name);
+
+    if (overload.num_explicit_templates > 0) {
+        ss << "<";
+        for (size_t i = 0; i < overload.num_explicit_templates; i++) {
+            const auto& tmpl = context.data[overload.templates + i];
+            if (i > 0) {
+                ss << ", ";
+            }
+            ss << style::Type(tmpl.name) << " ";
+        }
+        ss << ">";
+    }
+
+    ss << "(";
+    for (size_t i = 0; i < overload.num_parameters; i++) {
+        const auto& parameter = context.data[overload.parameters + i];
+        auto* matcher_indices = context.data[parameter.matcher_indices];
+
+        if (i > 0) {
+            ss << ", ";
+        }
+
+        if (parameter.usage != core::ParameterUsage::kNone) {
+            ss << style::Variable(parameter.usage, ": ");
+        }
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+    }
+    ss << ")";
+    if (overload.return_matcher_indices.IsValid()) {
+        ss << " -> ";
+        auto* matcher_indices = context.data[overload.return_matcher_indices];
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+    }
+
+    bool first = true;
+    auto separator = [&] {
+        ss << style::Plain(first ? " where:\n     " : "\n     ");
+        first = false;
+    };
+
+    for (size_t i = 0; i < overload.num_templates; i++) {
+        auto& tmpl = context.data[overload.templates + i];
+        if (auto* matcher_indices = context.data[tmpl.matcher_indices]) {
+            separator();
+
+            ss << style::Type(tmpl.name) << style::Plain(" is ");
+            if (tmpl.kind == core::intrinsic::TemplateInfo::Kind::kType) {
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
+                    .PrintType(ss);
+            } else {
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
+                    .PrintNum(ss);
+            }
+        }
+    }
+}
+
+}  // namespace
+
+typename lsp::TextDocumentSignatureHelpRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentSignatureHelpRequest& r) {
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    auto& program = (*file)->program;
+    auto pos = Conv(r.position);
+
+    auto call = (*file)->NodeAt<sem::Call>(pos);
+    if (!call) {
+        return lsp::Null{};
+    }
+
+    lsp::SignatureHelp help;
+    help.active_parameter = CalcParamIndex(call->Declaration()->source, pos);
+    Switch(call->Target(),  //
+           [&](const sem::BuiltinFn* target) {
+               auto& data = wgsl::intrinsic::Dialect::kData;
+               auto& builtins = data.builtins;
+               auto& intrinsic_info = builtins[static_cast<size_t>(target->Fn())];
+               std::string name{wgsl::str(target->Fn())};
+
+               for (size_t i = 0; i < intrinsic_info.num_overloads; i++) {
+                   auto& overload = data[intrinsic_info.overloads + i];
+
+                   auto params = Params(data, overload);
+
+                   auto type_mgr = core::type::Manager::Wrap(program.Types());
+                   auto symbols = SymbolTable::Wrap(program.Symbols());
+
+                   StyledText ss;
+                   core::intrinsic::Context ctx{data, type_mgr, symbols};
+                   PrintOverload(ss, ctx, overload, name);
+
+                   lsp::SignatureInformation sig;
+                   sig.parameters = params;
+                   sig.label = ss.Plain();
+                   help.signatures.push_back(sig);
+
+                   if (&overload == &target->Overload()) {
+                       help.active_signature = i;
+                   }
+               }
+           });
+
+    return help;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/signature_help_test.cc b/src/tint/lang/wgsl/ls/signature_help_test.cc
new file mode 100644
index 0000000..ceaf52a
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/signature_help_test.cc
@@ -0,0 +1,281 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+std::vector<lsp::SignatureInformation> MaxSignatures() {
+    std::vector<lsp::SignatureInformation> out;
+
+    {
+        std::vector<lsp::ParameterInformation> parameters;
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-0"),
+            /* documentation */ {},
+        });
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-1"),
+            /* documentation */ {},
+        });
+
+        lsp::SignatureInformation sig{};
+        sig.label = R"('max(T, T) -> T' where:
+     'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16')";
+        sig.parameters = std::move(parameters);
+
+        out.push_back(std::move(sig));
+    }
+
+    {
+        std::vector<lsp::ParameterInformation> parameters;
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-0"),
+            /* documentation */ {},
+        });
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-1"),
+            /* documentation */ {},
+        });
+
+        lsp::SignatureInformation sig{};
+        sig.label = R"('max(vecN<T>, vecN<T>) -> vecN<T>' where:
+     'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16')";
+        sig.parameters = std::move(parameters);
+
+        out.push_back(std::move(sig));
+    }
+
+    return out;
+}
+
+struct Case {
+    std::string_view markup;
+    std::vector<lsp::SignatureInformation> signatures;
+    lsp::Uinteger active_signature = 0;
+    lsp::Uinteger active_parameter = 0;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsSignatureHelpTest = LsTestWithParam<Case>;
+TEST_P(LsSignatureHelpTest, SignatureHelp) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.ranges.size(), 0u);
+    ASSERT_EQ(parsed.positions.size(), 1u);
+
+    lsp::TextDocumentSignatureHelpRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+    req.position = parsed.positions[0];
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (GetParam().signatures.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::SignatureHelp>());
+        auto& got = *res.Get<lsp::SignatureHelp>();
+        EXPECT_EQ(got.signatures, GetParam().signatures);
+        EXPECT_EQ(got.active_signature, GetParam().active_signature);
+        EXPECT_EQ(got.active_parameter, GetParam().active_parameter);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsSignatureHelpTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 R"(
+const C = max(⧘1, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C : i32 = max(⧘1, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(⧘1, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1⧘, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1,⧘ 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, 2⧘);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(⧘ vec2(1), vec2(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(⧘ vec3(1), vec3(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec4(1) ⧘, vec4(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec2(1),⧘ vec2(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec3(1), vec3(2) ⧘);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = min(max(1, ⧘2), max(3, 4));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+fn f() {
+    let x = max(1, ⧘2);
+}
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max( (1 + (2⧘) * 3), 4);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, (2 + (3⧘) * 4));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max( array(1,2,3)[1⧘], 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, array(1,2,3)[⧘2]);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, 2) ⧘;
+)",
+                                 {},
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/symbols.cc b/src/tint/lang/wgsl/ls/symbols.cc
new file mode 100644
index 0000000..78771ba
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/symbols.cc
@@ -0,0 +1,95 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/const.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/ast/alias.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/ast/struct.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::wgsl::ls {
+
+namespace lsp = langsvr::lsp;
+
+typename lsp::TextDocumentDocumentSymbolRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentDocumentSymbolRequest& r) {
+    typename lsp::TextDocumentDocumentSymbolRequest::SuccessType result = lsp::Null{};
+
+    std::vector<lsp::DocumentSymbol> symbols;
+    if (auto file = files_.Get(r.text_document.uri)) {
+        for (auto* decl : (*file)->program.AST().Functions()) {
+            lsp::DocumentSymbol sym;
+            sym.range = Conv(decl->source.range);
+            sym.selection_range = Conv(decl->name->source.range);
+            sym.kind = lsp::SymbolKind::kFunction;
+            sym.name = decl->name->symbol.NameView();
+            symbols.push_back(sym);
+        }
+        for (auto* decl : (*file)->program.AST().GlobalVariables()) {
+            lsp::DocumentSymbol sym;
+            sym.range = Conv(decl->source.range);
+            sym.selection_range = Conv(decl->name->source.range);
+            sym.kind =
+                decl->Is<ast::Const>() ? lsp::SymbolKind::kConstant : lsp::SymbolKind::kVariable;
+            sym.name = decl->name->symbol.NameView();
+            symbols.push_back(sym);
+        }
+        for (auto* decl : (*file)->program.AST().TypeDecls()) {
+            Switch(
+                decl,  //
+                [&](const ast::Struct* str) {
+                    lsp::DocumentSymbol sym;
+                    sym.range = Conv(str->source.range);
+                    sym.selection_range = Conv(decl->name->source.range);
+                    sym.kind = lsp::SymbolKind::kStruct;
+                    sym.name = decl->name->symbol.NameView();
+                    symbols.push_back(sym);
+                },
+                [&](const ast::Alias* str) {
+                    lsp::DocumentSymbol sym;
+                    sym.range = Conv(str->source.range);
+                    sym.selection_range = Conv(decl->name->source.range);
+                    // TODO(bclayton): Is there a better symbol kind?
+                    sym.kind = lsp::SymbolKind::kObject;
+                    sym.name = decl->name->symbol.NameView();
+                    symbols.push_back(sym);
+                });
+        }
+    }
+
+    if (!symbols.empty()) {
+        result = std::move(symbols);
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/symbols_test.cc b/src/tint/lang/wgsl/ls/symbols_test.cc
new file mode 100644
index 0000000..33820c9
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/symbols_test.cc
@@ -0,0 +1,161 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+struct Case {
+    const std::string_view wgsl;
+    const std::vector<lsp::DocumentSymbol> symbols;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.wgsl << "'";
+}
+
+struct Symbol : lsp::DocumentSymbol {
+    explicit Symbol(std::string_view n) { name = n; }
+
+    Symbol& Kind(lsp::SymbolKind k) {
+        kind = k;
+        return *this;
+    }
+
+    Symbol& Range(lsp::Uinteger start_line,
+                  lsp::Uinteger start_column,
+                  lsp::Uinteger end_line,
+                  lsp::Uinteger end_column) {
+        range = lsp::Range{{start_line, start_column}, {end_line, end_column}};
+        return *this;
+    }
+
+    Symbol& SelectionRange(lsp::Uinteger start_line,
+                           lsp::Uinteger start_column,
+                           lsp::Uinteger end_line,
+                           lsp::Uinteger end_column) {
+        selection_range = lsp::Range{{start_line, start_column}, {end_line, end_column}};
+        return *this;
+    }
+};
+
+using LsSymbolsTest = LsTestWithParam<Case>;
+TEST_P(LsSymbolsTest, Symbols) {
+    lsp::TextDocumentDocumentSymbolRequest req{};
+    req.text_document.uri = OpenDocument(GetParam().wgsl);
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (GetParam().symbols.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<std::vector<lsp::DocumentSymbol>>());
+        EXPECT_THAT(*res.Get<std::vector<lsp::DocumentSymbol>>(),
+                    testing::ContainerEq(GetParam().symbols));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsSymbolsTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 "",
+                                 {},
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "const C = 1;\n"
+                                 /* 1 */ "/* blah */ var V : i32 = 2i;\n"
+                                 /* 2 */ "override O = 3f;\n",
+                                 {
+                                     Symbol{"C"}
+                                         .Kind(lsp::SymbolKind::kConstant)
+                                         .Range(0, 0, 0, 11)
+                                         .SelectionRange(0, 6, 0, 7),
+                                     Symbol{"V"}
+                                         .Kind(lsp::SymbolKind::kVariable)
+                                         .Range(1, 11, 1, 27)
+                                         .SelectionRange(1, 15, 1, 16),
+                                     Symbol{"O"}
+                                         .Kind(lsp::SymbolKind::kVariable)
+                                         .Range(2, 0, 2, 15)
+                                         .SelectionRange(2, 9, 2, 10),
+                                 },
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "fn fa() {}\n"
+                                 /* 1 */ "/* blah */ fn fb() -> i32 {\n"
+                                 /* 2 */ "  return 1;\n"
+                                 /* 3 */ "} // blah",
+                                 {
+                                     Symbol{"fa"}
+                                         .Kind(lsp::SymbolKind::kFunction)
+                                         .Range(0, 0, 0, 10)
+                                         .SelectionRange(0, 3, 0, 5),
+                                     Symbol{"fb"}
+                                         .Kind(lsp::SymbolKind::kFunction)
+                                         .Range(1, 11, 3, 1)
+                                         .SelectionRange(1, 14, 1, 16),
+                                 },
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "struct s1 { i : i32 }\n"
+                                 /* 1 */ "alias A = i32;\n"
+                                 /* 2 */ "/* blah */ struct s2 {\n"
+                                 /* 3 */ "  a : i32,\n"
+                                 /* 4 */ "} // blah",
+                                 {
+                                     Symbol{"s1"}
+                                         .Kind(lsp::SymbolKind::kStruct)
+                                         .Range(0, 0, 0, 21)
+                                         .SelectionRange(0, 7, 0, 9),
+                                     Symbol{"A"}
+                                         .Kind(lsp::SymbolKind::kObject)
+                                         .Range(1, 0, 1, 13)
+                                         .SelectionRange(1, 6, 1, 7),
+                                     Symbol{"s2"}
+                                         .Kind(lsp::SymbolKind::kStruct)
+                                         .Range(2, 11, 4, 1)
+                                         .SelectionRange(2, 18, 2, 20),
+                                 },
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/utils.h b/src/tint/lang/wgsl/ls/utils.h
new file mode 100644
index 0000000..921fbc1
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/utils.h
@@ -0,0 +1,79 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_LS_UTILS_H_
+#define SRC_TINT_LANG_WGSL_LS_UTILS_H_
+
+#include "langsvr/lsp/lsp.h"
+#include "src/tint/lang/wgsl/sem/value_expression.h"
+#include "src/tint/utils/diagnostic/source.h"
+#include "src/tint/utils/rtti/castable.h"
+
+// Forward declarations
+namespace tint::sem {
+class Node;
+}
+
+namespace tint::wgsl::ls {
+
+/// @return the langsvr::lsp::Position @p pos converted to a tint::Source::Location
+inline Source::Location Conv(langsvr::lsp::Position pos) {
+    Source::Location loc;
+    loc.line = static_cast<uint32_t>(pos.line + 1);
+    loc.column = static_cast<uint32_t>(pos.character + 1);
+    return loc;
+}
+
+/// @return the tint::Source::Location @p loc converted to a langsvr::lsp::Position
+inline langsvr::lsp::Position Conv(Source::Location loc) {
+    langsvr::lsp::Position pos;
+    pos.line = loc.line - 1;
+    pos.character = loc.column - 1;
+    return pos;
+}
+
+/// @return the tint::Source::Range @p rng converted to a langsvr::lsp::Range
+inline langsvr::lsp::Range Conv(Source::Range rng) {
+    langsvr::lsp::Range out;
+    out.start = Conv(rng.begin);
+    out.end = Conv(rng.end);
+    return out;
+}
+
+/// @returns the sem::Load() and sem::Materialize() unwrapped sem::ValueExpression, if `T` is a
+/// sem::ValueExpression, otherwise returns @p node.
+template <typename T>
+const T* Unwrap(const T* node) {
+    if (auto* expr = As<sem::ValueExpression, CastFlags::kDontErrorOnImpossibleCast>(node)) {
+        return As<T>(expr->Unwrap());
+    }
+    return node;
+}
+
+}  // namespace tint::wgsl::ls
+
+#endif  // SRC_TINT_LANG_WGSL_LS_UTILS_H_
diff --git a/src/tint/lang/wgsl/reader/parser/global_constant_decl_test.cc b/src/tint/lang/wgsl/reader/parser/global_constant_decl_test.cc
index c80de54..904da07 100644
--- a/src/tint/lang/wgsl/reader/parser/global_constant_decl_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/global_constant_decl_test.cc
@@ -61,9 +61,14 @@
     ast::CheckIdentifier(c->type, "f32");
 
     EXPECT_EQ(c->source.range.begin.line, 1u);
-    EXPECT_EQ(c->source.range.begin.column, 7u);
+    EXPECT_EQ(c->source.range.begin.column, 1u);
     EXPECT_EQ(c->source.range.end.line, 1u);
-    EXPECT_EQ(c->source.range.end.column, 8u);
+    EXPECT_EQ(c->source.range.end.column, 19u);
+
+    EXPECT_EQ(c->name->source.range.begin.line, 1u);
+    EXPECT_EQ(c->name->source.range.begin.column, 7u);
+    EXPECT_EQ(c->name->source.range.end.line, 1u);
+    EXPECT_EQ(c->name->source.range.end.column, 8u);
 
     ASSERT_NE(c->initializer, nullptr);
     EXPECT_TRUE(c->initializer->Is<ast::LiteralExpression>());
@@ -85,9 +90,14 @@
     EXPECT_EQ(c->type, nullptr);
 
     EXPECT_EQ(c->source.range.begin.line, 1u);
-    EXPECT_EQ(c->source.range.begin.column, 7u);
+    EXPECT_EQ(c->source.range.begin.column, 1u);
     EXPECT_EQ(c->source.range.end.line, 1u);
-    EXPECT_EQ(c->source.range.end.column, 8u);
+    EXPECT_EQ(c->source.range.end.column, 13u);
+
+    EXPECT_EQ(c->name->source.range.begin.line, 1u);
+    EXPECT_EQ(c->name->source.range.begin.column, 7u);
+    EXPECT_EQ(c->name->source.range.end.line, 1u);
+    EXPECT_EQ(c->name->source.range.end.column, 8u);
 
     ASSERT_NE(c->initializer, nullptr);
     EXPECT_TRUE(c->initializer->Is<ast::LiteralExpression>());
@@ -137,9 +147,14 @@
     ast::CheckIdentifier(override->type, "f32");
 
     EXPECT_EQ(override->source.range.begin.line, 1u);
-    EXPECT_EQ(override->source.range.begin.column, 17u);
+    EXPECT_EQ(override->source.range.begin.column, 8u);
     EXPECT_EQ(override->source.range.end.line, 1u);
-    EXPECT_EQ(override->source.range.end.column, 18u);
+    EXPECT_EQ(override->source.range.end.column, 29u);
+
+    EXPECT_EQ(override->name->source.range.begin.line, 1u);
+    EXPECT_EQ(override->name->source.range.begin.column, 17u);
+    EXPECT_EQ(override->name->source.range.end.line, 1u);
+    EXPECT_EQ(override->name->source.range.end.column, 18u);
 
     ASSERT_NE(override->initializer, nullptr);
     EXPECT_TRUE(override->initializer->Is<ast::LiteralExpression>());
@@ -167,9 +182,14 @@
     ast::CheckIdentifier(override->type, "f32");
 
     EXPECT_EQ(override->source.range.begin.line, 1u);
-    EXPECT_EQ(override->source.range.begin.column, 18u);
+    EXPECT_EQ(override->source.range.begin.column, 9u);
     EXPECT_EQ(override->source.range.end.line, 1u);
-    EXPECT_EQ(override->source.range.end.column, 19u);
+    EXPECT_EQ(override->source.range.end.column, 30u);
+
+    EXPECT_EQ(override->name->source.range.begin.line, 1u);
+    EXPECT_EQ(override->name->source.range.begin.column, 18u);
+    EXPECT_EQ(override->name->source.range.end.line, 1u);
+    EXPECT_EQ(override->name->source.range.end.column, 19u);
 
     ASSERT_NE(override->initializer, nullptr);
     EXPECT_TRUE(override->initializer->Is<ast::LiteralExpression>());
@@ -197,9 +217,14 @@
     ast::CheckIdentifier(override->type, "f32");
 
     EXPECT_EQ(override->source.range.begin.line, 1u);
-    EXPECT_EQ(override->source.range.begin.column, 10u);
+    EXPECT_EQ(override->source.range.begin.column, 1u);
     EXPECT_EQ(override->source.range.end.line, 1u);
-    EXPECT_EQ(override->source.range.end.column, 11u);
+    EXPECT_EQ(override->source.range.end.column, 22u);
+
+    EXPECT_EQ(override->name->source.range.begin.line, 1u);
+    EXPECT_EQ(override->name->source.range.begin.column, 10u);
+    EXPECT_EQ(override->name->source.range.end.line, 1u);
+    EXPECT_EQ(override->name->source.range.end.column, 11u);
 
     ASSERT_NE(override->initializer, nullptr);
     EXPECT_TRUE(override->initializer->Is<ast::LiteralExpression>());
diff --git a/src/tint/lang/wgsl/reader/parser/global_variable_decl_test.cc b/src/tint/lang/wgsl/reader/parser/global_variable_decl_test.cc
index 4105d12..d53b6f7 100644
--- a/src/tint/lang/wgsl/reader/parser/global_variable_decl_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/global_variable_decl_test.cc
@@ -48,9 +48,14 @@
     ast::CheckIdentifier(var->declared_address_space, "private");
 
     EXPECT_EQ(var->source.range.begin.line, 1u);
-    EXPECT_EQ(var->source.range.begin.column, 14u);
+    EXPECT_EQ(var->source.range.begin.column, 1u);
     EXPECT_EQ(var->source.range.end.line, 1u);
-    EXPECT_EQ(var->source.range.end.column, 15u);
+    EXPECT_EQ(var->source.range.end.column, 21u);
+
+    EXPECT_EQ(var->name->source.range.begin.line, 1u);
+    EXPECT_EQ(var->name->source.range.begin.column, 14u);
+    EXPECT_EQ(var->name->source.range.end.line, 1u);
+    EXPECT_EQ(var->name->source.range.end.column, 15u);
 
     ASSERT_EQ(var->initializer, nullptr);
 }
@@ -72,9 +77,14 @@
     ast::CheckIdentifier(var->declared_address_space, "private");
 
     EXPECT_EQ(var->source.range.begin.line, 1u);
-    EXPECT_EQ(var->source.range.begin.column, 14u);
+    EXPECT_EQ(var->source.range.begin.column, 1u);
     EXPECT_EQ(var->source.range.end.line, 1u);
-    EXPECT_EQ(var->source.range.end.column, 15u);
+    EXPECT_EQ(var->source.range.end.column, 26u);
+
+    EXPECT_EQ(var->name->source.range.begin.line, 1u);
+    EXPECT_EQ(var->name->source.range.begin.column, 14u);
+    EXPECT_EQ(var->name->source.range.end.line, 1u);
+    EXPECT_EQ(var->name->source.range.end.column, 15u);
 
     ASSERT_NE(var->initializer, nullptr);
     ASSERT_TRUE(var->initializer->Is<ast::FloatLiteralExpression>());
@@ -97,9 +107,14 @@
     ast::CheckIdentifier(var->declared_address_space, "uniform");
 
     EXPECT_EQ(var->source.range.begin.line, 1u);
-    EXPECT_EQ(var->source.range.begin.column, 36u);
+    EXPECT_EQ(var->source.range.begin.column, 23u);
     EXPECT_EQ(var->source.range.end.line, 1u);
-    EXPECT_EQ(var->source.range.end.column, 37u);
+    EXPECT_EQ(var->source.range.end.column, 43u);
+
+    EXPECT_EQ(var->name->source.range.begin.line, 1u);
+    EXPECT_EQ(var->name->source.range.begin.column, 36u);
+    EXPECT_EQ(var->name->source.range.end.line, 1u);
+    EXPECT_EQ(var->name->source.range.end.column, 37u);
 
     ASSERT_EQ(var->initializer, nullptr);
 
@@ -127,9 +142,14 @@
     ast::CheckIdentifier(var->declared_address_space, "uniform");
 
     EXPECT_EQ(var->source.range.begin.line, 1u);
-    EXPECT_EQ(var->source.range.begin.column, 36u);
+    EXPECT_EQ(var->source.range.begin.column, 23u);
     EXPECT_EQ(var->source.range.end.line, 1u);
-    EXPECT_EQ(var->source.range.end.column, 37u);
+    EXPECT_EQ(var->source.range.end.column, 43u);
+
+    EXPECT_EQ(var->name->source.range.begin.line, 1u);
+    EXPECT_EQ(var->name->source.range.begin.column, 36u);
+    EXPECT_EQ(var->name->source.range.end.line, 1u);
+    EXPECT_EQ(var->name->source.range.end.column, 37u);
 
     ASSERT_EQ(var->initializer, nullptr);
 
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 315d701..d4ba3ca 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -28,6 +28,7 @@
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
 
 #include <limits>
+#include <utility>
 
 #include "src/tint/lang/core/attribute.h"
 #include "src/tint/lang/core/type/depth_texture.h"
@@ -50,6 +51,7 @@
 #include "src/tint/lang/wgsl/ast/stage_attribute.h"
 #include "src/tint/lang/wgsl/ast/switch_statement.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
+#include "src/tint/lang/wgsl/ast/var.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
 #include "src/tint/lang/wgsl/reader/parser/classify_template_args.h"
@@ -666,6 +668,7 @@
 // global_variable_decl
 //  : variable_attribute_list* variable_decl (EQUAL expression)?
 Maybe<const ast::Variable*> Parser::global_variable_decl(AttributeList& attrs) {
+    MultiTokenSource decl_source(this);
     auto decl = variable_decl();
     if (decl.errored) {
         return Failure::kErrored;
@@ -688,7 +691,7 @@
 
     TINT_DEFER(attrs.Clear());
 
-    return builder_.create<ast::Var>(decl->source,                // source
+    return builder_.create<ast::Var>(decl_source(),               // source
                                      builder_.Ident(decl->name),  // symbol
                                      decl->type,                  // type
                                      decl->address_space,         // address space
@@ -705,6 +708,7 @@
 Maybe<const ast::Variable*> Parser::global_constant_decl(AttributeList& attrs) {
     bool is_overridable = false;
     const char* use = nullptr;
+    MultiTokenSource decl_source(this);
     Source source;
     if (match(Token::Type::kConst)) {
         use = "'const' declaration";
@@ -746,17 +750,17 @@
 
     TINT_DEFER(attrs.Clear());
     if (is_overridable) {
-        return builder_.Override(decl->name->source,  // source
-                                 decl->name,          // symbol
-                                 decl->type,          // type
-                                 initializer,         // initializer
-                                 std::move(attrs));   // attributes
+        return builder_.Override(decl_source(),      // source
+                                 decl->name,         // symbol
+                                 decl->type,         // type
+                                 initializer,        // initializer
+                                 std::move(attrs));  // attributes
     }
-    return builder_.GlobalConst(decl->name->source,  // source
-                                decl->name,          // symbol
-                                decl->type,          // type
-                                initializer,         // initializer
-                                std::move(attrs));   // attributes
+    return builder_.GlobalConst(decl_source(),      // source
+                                decl->name,         // symbol
+                                decl->type,         // type
+                                initializer,        // initializer
+                                std::move(attrs));  // attributes
 }
 
 // variable_decl
@@ -974,7 +978,7 @@
 // struct_decl
 //   : STRUCT IDENT struct_body_decl
 Maybe<const ast::Struct*> Parser::struct_decl() {
-    auto& t = peek();
+    MultiTokenSource source(this);
 
     if (!match(Token::Type::kStruct)) {
         return Failure::kNoMatch;
@@ -990,7 +994,7 @@
         return Failure::kErrored;
     }
 
-    return builder_.Structure(t.source(), name.value, std::move(body.value));
+    return builder_.Structure(source(), name.value, std::move(body.value));
 }
 
 // struct_body_decl
diff --git a/src/tint/lang/wgsl/reader/parser/struct_decl_test.cc b/src/tint/lang/wgsl/reader/parser/struct_decl_test.cc
index 9733936..353e5fe 100644
--- a/src/tint/lang/wgsl/reader/parser/struct_decl_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/struct_decl_test.cc
@@ -46,6 +46,16 @@
     ASSERT_EQ(s->members.Length(), 2u);
     EXPECT_EQ(s->members[0]->name->symbol, p->builder().Symbols().Register("a"));
     EXPECT_EQ(s->members[1]->name->symbol, p->builder().Symbols().Register("b"));
+
+    EXPECT_EQ(s->source.range.begin.line, 2u);
+    EXPECT_EQ(s->source.range.begin.column, 1u);
+    EXPECT_EQ(s->source.range.end.line, 5u);
+    EXPECT_EQ(s->source.range.end.column, 2u);
+
+    EXPECT_EQ(s->name->source.range.begin.line, 2u);
+    EXPECT_EQ(s->name->source.range.begin.column, 8u);
+    EXPECT_EQ(s->name->source.range.end.line, 2u);
+    EXPECT_EQ(s->name->source.range.end.column, 9u);
 }
 
 TEST_F(WGSLParserTest, StructDecl_Unicode_Parses) {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index f54aba6..b838c02 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -575,22 +575,31 @@
 
         ControlStackScope scope(this, loop_inst);
 
+        auto& body_behaviors = program_.Sem().Get(stmt->body)->Behaviors();
         {
             TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Body());
 
             EmitStatements(stmt->body->statements);
 
-            // The current block didn't `break`, `return` or `continue`, go to the continuing block.
+            // The current block didn't `break`, `return` or `continue`, go to the continuing block
+            // or mark the end of the block as unreachable.
             if (NeedTerminator()) {
-                SetTerminator(builder_.Continue(loop_inst));
+                if (body_behaviors.Contains(sem::Behavior::kNext)) {
+                    SetTerminator(builder_.Continue(loop_inst));
+                } else {
+                    SetTerminator(builder_.Unreachable());
+                }
             }
         }
 
-        {
+        // Emit a continuing block if it is reachable.
+        if (body_behaviors.Contains(sem::Behavior::kNext) ||
+            body_behaviors.Contains(sem::Behavior::kContinue)) {
             TINT_SCOPED_ASSIGNMENT(current_block_, loop_inst->Continuing());
             if (stmt->continuing) {
                 EmitBlock(stmt->continuing);
             }
+
             // Branch back to the start block if the continue target didn't terminate already
             if (NeedTerminator()) {
                 SetTerminator(builder_.NextIteration(loop_inst));
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
index 9867cb4..8ae7aa1 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
@@ -267,13 +267,10 @@
   %b1 = block {
     if true [t: %b2] {  # if_1
       %b2 = block {  # true
-        loop [b: %b3, c: %b4] {  # loop_1
+        loop [b: %b3] {  # loop_1
           %b3 = block {  # body
             exit_loop  # loop_1
           }
-          %b4 = block {  # continuing
-            next_iteration %b3
-          }
         }
         exit_if  # if_1
       }
@@ -296,19 +293,16 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3] {  # loop_1
+    loop [b: %b2] {  # loop_1
       %b2 = block {  # body
         exit_loop  # loop_1
       }
-      %b3 = block {  # continuing
-        next_iteration %b2
-      }
     }
     ret
   }
@@ -465,19 +459,16 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3] {  # loop_1
+    loop [b: %b2] {  # loop_1
       %b2 = block {  # body
         ret
       }
-      %b3 = block {  # continuing
-        next_iteration %b2
-      }
     }
     unreachable
   }
@@ -506,22 +497,19 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
     EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3] {  # loop_1
+    loop [b: %b2] {  # loop_1
       %b2 = block {  # body
         ret
       }
-      %b3 = block {  # continuing
-        break_if true %b2
-      }
     }
-    if true [t: %b4] {  # if_1
-      %b4 = block {  # true
+    if true [t: %b3] {  # if_1
+      %b3 = block {  # true
         ret
       }
     }
@@ -544,26 +532,23 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    EXPECT_EQ(1u, loop->Body()->InboundSiblingBranches().Length());
-    EXPECT_EQ(1u, loop->Continuing()->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, loop->Body()->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, loop->Continuing()->InboundSiblingBranches().Length());
 
     EXPECT_EQ(Disassemble(m),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
-    loop [b: %b2, c: %b3] {  # loop_1
+    loop [b: %b2] {  # loop_1
       %b2 = block {  # body
-        if true [t: %b4, f: %b5] {  # if_1
-          %b4 = block {  # true
+        if true [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
             exit_loop  # loop_1
           }
-          %b5 = block {  # false
+          %b4 = block {  # false
             exit_loop  # loop_1
           }
         }
-        continue %b3
-      }
-      %b3 = block {  # continuing
-        next_iteration %b2
+        unreachable
       }
     }
     ret
@@ -609,27 +594,24 @@
             continue %b5
           }
           %b5 = block {  # continuing
-            loop [b: %b8, c: %b9] {  # loop_3
+            loop [b: %b8] {  # loop_3
               %b8 = block {  # body
                 exit_loop  # loop_3
               }
-              %b9 = block {  # continuing
-                next_iteration %b8
-              }
             }
-            loop [b: %b10, c: %b11] {  # loop_4
-              %b10 = block {  # body
-                continue %b11
+            loop [b: %b9, c: %b10] {  # loop_4
+              %b9 = block {  # body
+                continue %b10
               }
-              %b11 = block {  # continuing
-                break_if true %b10
+              %b10 = block {  # continuing
+                break_if true %b9
               }
             }
             next_iteration %b4
           }
         }
-        if true [t: %b12] {  # if_3
-          %b12 = block {  # true
+        if true [t: %b11] {  # if_3
+          %b11 = block {  # true
             exit_loop  # loop_1
           }
         }
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index a36e21f..0554f9a 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -106,7 +106,7 @@
         case AttributeKind::kSize:
             return o << "@size";
         case AttributeKind::kStageCompute:
-            return o << "@stage(compute)";
+            return o << "@compute";
         case AttributeKind::kStride:
             return o << "@stride";
         case AttributeKind::kWorkgroupSize:
@@ -192,7 +192,7 @@
             },
             TestParams{
                 {AttributeKind::kStageCompute},
-                "1:2 error: '@stage' is not valid for " + thing,
+                "1:2 error: '@compute' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kStride},
@@ -547,7 +547,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for function parameters)",
+            R"(1:2 error: '@compute' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -632,7 +632,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for non-entry point function return types)",
+            R"(1:2 error: '@compute' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -722,7 +722,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for function parameters)",
+            R"(1:2 error: '@compute' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -823,7 +823,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for function parameters)",
+            R"(1:2 error: '@compute' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -930,7 +930,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for function parameters)",
+            R"(1:2 error: '@compute' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1019,7 +1019,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for entry point return types)",
+            R"(1:2 error: '@compute' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1117,7 +1117,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for entry point return types)",
+            R"(1:2 error: '@compute' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1218,7 +1218,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for entry point return types)",
+            R"(1:2 error: '@compute' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1342,7 +1342,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for 'struct' declarations)",
+            R"(1:2 error: '@compute' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1434,7 +1434,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for 'struct' members)",
+            R"(1:2 error: '@compute' is not valid for 'struct' members)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1694,7 +1694,7 @@
                              },
                              TestParams{
                                  {AttributeKind::kStageCompute},
-                                 R"(1:2 error: '@stage' is not valid for 'array' types)",
+                                 R"(1:2 error: '@compute' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kStride},
@@ -1785,7 +1785,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for module-scope 'var')",
+            R"(1:2 error: '@compute' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1889,7 +1889,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for 'const' declaration)",
+            R"(1:2 error: '@compute' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kStride},
@@ -1970,7 +1970,7 @@
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: '@stage' is not valid for 'override' declaration)",
+            R"(1:2 error: '@compute' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kStride},
diff --git a/src/tint/lang/wgsl/resolver/function_validation_test.cc b/src/tint/lang/wgsl/resolver/function_validation_test.cc
index 3de0b09..764af82 100644
--- a/src/tint/lang/wgsl/resolver/function_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/function_validation_test.cc
@@ -456,7 +456,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(56:78 error: duplicate stage attribute
+              R"(56:78 error: duplicate fragment attribute
 12:34 note: first attribute declared here)");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/side_effects_test.cc b/src/tint/lang/wgsl/resolver/side_effects_test.cc
index b9fa38b..7d8bd2c 100644
--- a/src/tint/lang/wgsl/resolver/side_effects_test.cc
+++ b/src/tint/lang/wgsl/resolver/side_effects_test.cc
@@ -108,7 +108,7 @@
 
 TEST_F(SideEffectsTest, Call_Builtin_NoSE) {
     GlobalVar("a", ty.f32(), core::AddressSpace::kPrivate);
-    auto* expr = Call("dpdx", "a");
+    auto* expr = Call("sqrt", "a");
     Func("f", tint::Empty, ty.void_(), Vector{Ignore(expr)},
          Vector{create<ast::StageAttribute>(ast::PipelineStage::kFragment)});
 
@@ -334,30 +334,30 @@
         C("textureSampleCompareLevel",
           Vector{"tdepth2d", "scomp", "vf2", "f"},
           false,
-          true),                                                                          //
-        C("textureSampleGrad", Vector{"t2d", "s2d", "vf2", "vf2", "vf2"}, false, true),   //
-        C("textureSampleLevel", Vector{"t2d", "s2d", "vf2", "f"}, false, true),           //
-        C("transpose", Vector{"m"}, false, true),                                         //
-        C("trunc", Vector{"f"}, false, true),                                             //
-        C("unpack2x16float", Vector{"u"}, false, true),                                   //
-        C("unpack2x16snorm", Vector{"u"}, false, true),                                   //
-        C("unpack2x16unorm", Vector{"u"}, false, true),                                   //
-        C("unpack4x8snorm", Vector{"u"}, false, true),                                    //
-        C("unpack4x8unorm", Vector{"u"}, false, true),                                    //
-        C("storageBarrier", tint::Empty, false, false, ast::PipelineStage::kCompute),     //
-        C("workgroupBarrier", tint::Empty, false, false, ast::PipelineStage::kCompute),   //
-        C("textureSample", Vector{"t2d", "s2d", "vf2"}, false, true),                     //
-        C("textureSampleBias", Vector{"t2d", "s2d", "vf2", "f"}, false, true),            //
-        C("textureSampleCompare", Vector{"tdepth2d", "scomp", "vf2", "f"}, false, true),  //
-        C("dpdx", Vector{"f"}, false, true),                                              //
-        C("dpdxCoarse", Vector{"f"}, false, true),                                        //
-        C("dpdxFine", Vector{"f"}, false, true),                                          //
-        C("dpdy", Vector{"f"}, false, true),                                              //
-        C("dpdyCoarse", Vector{"f"}, false, true),                                        //
-        C("dpdyFine", Vector{"f"}, false, true),                                          //
-        C("fwidth", Vector{"f"}, false, true),                                            //
-        C("fwidthCoarse", Vector{"f"}, false, true),                                      //
-        C("fwidthFine", Vector{"f"}, false, true),                                        //
+          true),                                                                         //
+        C("textureSampleGrad", Vector{"t2d", "s2d", "vf2", "vf2", "vf2"}, false, true),  //
+        C("textureSampleLevel", Vector{"t2d", "s2d", "vf2", "f"}, false, true),          //
+        C("transpose", Vector{"m"}, false, true),                                        //
+        C("trunc", Vector{"f"}, false, true),                                            //
+        C("unpack2x16float", Vector{"u"}, false, true),                                  //
+        C("unpack2x16snorm", Vector{"u"}, false, true),                                  //
+        C("unpack2x16unorm", Vector{"u"}, false, true),                                  //
+        C("unpack4x8snorm", Vector{"u"}, false, true),                                   //
+        C("unpack4x8unorm", Vector{"u"}, false, true),                                   //
+        C("storageBarrier", tint::Empty, false, false, ast::PipelineStage::kCompute),    //
+        C("workgroupBarrier", tint::Empty, false, false, ast::PipelineStage::kCompute),  //
+        C("textureSample", Vector{"t2d", "s2d", "vf2"}, true, true),                     //
+        C("textureSampleBias", Vector{"t2d", "s2d", "vf2", "f"}, true, true),            //
+        C("textureSampleCompare", Vector{"tdepth2d", "scomp", "vf2", "f"}, true, true),  //
+        C("dpdx", Vector{"f"}, true, true),                                              //
+        C("dpdxCoarse", Vector{"f"}, true, true),                                        //
+        C("dpdxFine", Vector{"f"}, true, true),                                          //
+        C("dpdy", Vector{"f"}, true, true),                                              //
+        C("dpdyCoarse", Vector{"f"}, true, true),                                        //
+        C("dpdyFine", Vector{"f"}, true, true),                                          //
+        C("fwidth", Vector{"f"}, true, true),                                            //
+        C("fwidthCoarse", Vector{"f"}, true, true),                                      //
+        C("fwidthFine", Vector{"f"}, true, true),                                        //
 
         // Side-effect builtins
         C("atomicAdd", Vector{"pa", "i"}, true, true),                       //
diff --git a/src/tint/utils/file/tmpfile_windows.cc b/src/tint/utils/file/tmpfile_windows.cc
index 486c2ea..d4112eb 100644
--- a/src/tint/utils/file/tmpfile_windows.cc
+++ b/src/tint/utils/file/tmpfile_windows.cc
@@ -29,6 +29,8 @@
 
 #include "src/tint/utils/file/tmpfile.h"
 
+#include <fcntl.h>
+#include <io.h>
 #include <stdio.h>
 #include <cstdio>
 
@@ -42,11 +44,16 @@
     // creating it, failing if it already exists.
     while (tmpnam_s(name, L_tmpnam - 1) == 0) {
         std::string name_with_ext = std::string(name) + ext;
-        FILE* f = nullptr;
-        // The "x" arg forces the function to fail if the file already exists.
-        fopen_s(&f, name_with_ext.c_str(), "wbx");
-        if (f) {
-            fclose(f);
+
+        // Use MS-specific _sopen_s as it allows us to create the file in exclusive mode (_O_EXCL)
+        // so that it returns an error if the file already exists.
+        int fh = 0;
+        errno_t e = _sopen_s(&fh, name_with_ext.c_str(),                    //
+                             /* _OpenFlag */ _O_RDWR | _O_CREAT | _O_EXCL,  //
+                             /* _ShareFlag */ _SH_DENYNO,
+                             /* _PermissionMode */ _S_IREAD | _S_IWRITE);
+        if (e == 0) {
+            _close(fh);
             return name_with_ext;
         }
     }
diff --git a/src/tint/utils/system/terminal_posix.cc b/src/tint/utils/system/terminal_posix.cc
index fb2d93f..e820774 100644
--- a/src/tint/utils/system/terminal_posix.cc
+++ b/src/tint/utils/system/terminal_posix.cc
@@ -39,6 +39,7 @@
 #include <utility>
 
 #include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/macros/defer.h"
 #include "src/tint/utils/system/env.h"
 #include "src/tint/utils/system/terminal.h"
@@ -47,7 +48,8 @@
 namespace {
 
 std::optional<bool> TerminalIsDarkImpl(FILE* out) {
-    if (!TerminalSupportsColors(out)) {
+    // Check the terminal can be queried, and supports colors.
+    if (!isatty(STDIN_FILENO) || !TerminalSupportsColors(out)) {
         return std::nullopt;
     }
 
@@ -59,17 +61,22 @@
 
     // Store the current attributes for 'out', restore it before returning
     termios original_state{};
-    tcgetattr(out_fd, &original_state);
+    if (tcgetattr(out_fd, &original_state) != 0) {
+        return std::nullopt;
+    }
     TINT_DEFER(tcsetattr(out_fd, TCSADRAIN, &original_state));
 
     // Prevent echoing.
     termios state = original_state;
     state.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON);
-    tcsetattr(out_fd, TCSADRAIN, &state);
+    if (tcsetattr(out_fd, TCSADRAIN, &state) != 0) {
+        return std::nullopt;
+    }
 
     // Emit the device control escape sequence to query the terminal colors.
     static constexpr std::string_view kQuery = "\033]11;?\033\\";
     fwrite(kQuery.data(), 1, kQuery.length(), out);
+    fflush(out);
 
     // Timeout for attempting to read the response.
     static constexpr auto kTimeout = std::chrono::milliseconds(300);
@@ -77,6 +84,22 @@
     // Record the start time.
     auto start = std::chrono::steady_clock::now();
 
+    // Returns true if there's data available on stdin, or false if no data was available after
+    // 100ms.
+    auto poll_stdin = [] {
+        // Note: These macros introduce identifiers that start with `__`.
+        TINT_BEGIN_DISABLE_WARNING(RESERVED_IDENTIFIER);
+        fd_set rfds{};
+        FD_ZERO(&rfds);
+        FD_SET(STDIN_FILENO, &rfds);
+        timeval tv{};
+        tv.tv_sec = 0;
+        tv.tv_usec = 100'000;
+        int res = select(STDIN_FILENO + 1, &rfds, nullptr, nullptr, &tv);
+        return res > 0 && FD_ISSET(STDIN_FILENO, &rfds);
+        TINT_END_DISABLE_WARNING(RESERVED_IDENTIFIER);
+    };
+
     // Helpers for parsing the response.
     Vector<char, 8> peek;
     auto read = [&]() -> std::optional<char> {
@@ -84,6 +107,10 @@
             return peek.Pop();
         }
         while ((std::chrono::steady_clock::now() - start) < kTimeout) {
+            if (!poll_stdin()) {
+                return std::nullopt;
+            }
+
             char c;
             if (fread(&c, 1, 1, stdin) == 1) {
                 return c;
@@ -163,7 +190,7 @@
             return std::nullopt;
     }
 
-    if (!match("\x07") && !match("\x1b\x5c")) {
+    if (!match("\x07") && !match("\x1b")) {
         return std::nullopt;
     }