Make all ast and sem pointers const

And remove a whole load of const_cast hackery.

Semantic nodes may contain internally mutable fields (although only ever modified during resolving), so these are always passed by `const` pointer.

While all AST nodes are internally immutable, we have decided that pointers to AST nodes should also be marked `const`, for consistency.

There's still a collection of const_cast calls in the Resolver. These will be fixed up in a later change.

Bug: tint:745
Change-Id: I046309b8e586772605fc0fe6b2d27f28806d40ef
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/66606
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/fuzzers/tint_ast_fuzzer/mutator.cc b/fuzzers/tint_ast_fuzzer/mutator.cc
index d1b5efe..d1637db 100644
--- a/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -71,7 +71,7 @@
   tint::CloneContext clone_context(&mutated, &program);
   NodeIdMap new_node_id_map;
   clone_context.ReplaceAll(
-      [&node_id_map, &new_node_id_map, &clone_context](ast::Node* node) {
+      [&node_id_map, &new_node_id_map, &clone_context](const ast::Node* node) {
         // Make sure all `tint::ast::` nodes' ids are preserved.
         auto* cloned = tint::As<ast::Node>(node->Clone(&clone_context));
         new_node_id_map.Add(cloned, node_id_map.GetId(node));
diff --git a/fuzzers/tint_ast_fuzzer/node_id_map.cc b/fuzzers/tint_ast_fuzzer/node_id_map.cc
index bca1237..f86b4f4 100644
--- a/fuzzers/tint_ast_fuzzer/node_id_map.cc
+++ b/fuzzers/tint_ast_fuzzer/node_id_map.cc
@@ -22,33 +22,29 @@
 
 NodeIdMap::NodeIdMap() = default;
 
-NodeIdMap::NodeIdMap(const tint::Program& program) : NodeIdMap() {
+NodeIdMap::NodeIdMap(const Program& program) : NodeIdMap() {
   for (const auto* node : program.ASTNodes().Objects()) {
     Add(node, TakeFreshId());
   }
 }
 
 NodeIdMap::IdType NodeIdMap::GetId(const ast::Node* node) const {
-  // Since node is immutable by default, const_cast won't
-  // modify the node structure.
-  auto it = node_to_id_.find(const_cast<ast::Node*>(node));
+  auto it = node_to_id_.find(node);
   return it == node_to_id_.end() ? 0 : it->second;
 }
 
-ast::Node* NodeIdMap::GetNode(IdType id) const {
+const ast::Node* NodeIdMap::GetNode(IdType id) const {
   auto it = id_to_node_.find(id);
   return it == id_to_node_.end() ? nullptr : it->second;
 }
 
 void NodeIdMap::Add(const ast::Node* node, IdType id) {
-  auto* casted_node = const_cast<ast::Node*>(node);
-  assert(!node_to_id_.count(casted_node) &&
-         "The node already exists in the map");
+  assert(!node_to_id_.count(node) && "The node already exists in the map");
   assert(IdIsFreshAndValid(id) && "Id already exists in the map or Id is zero");
   assert(node && "`node` can't be a nullptr");
 
-  node_to_id_[casted_node] = id;
-  id_to_node_[id] = casted_node;
+  node_to_id_[node] = id;
+  id_to_node_[id] = node;
 
   if (id >= fresh_id_) {
     fresh_id_ = id + 1;
diff --git a/fuzzers/tint_ast_fuzzer/node_id_map.h b/fuzzers/tint_ast_fuzzer/node_id_map.h
index 78f1590..2e0131d 100644
--- a/fuzzers/tint_ast_fuzzer/node_id_map.h
+++ b/fuzzers/tint_ast_fuzzer/node_id_map.h
@@ -48,13 +48,13 @@
 
   /// @brief Initializes this instance with all the nodes in the `program`.
   /// @param program - must be valid.
-  explicit NodeIdMap(const tint::Program& program);
+  explicit NodeIdMap(const Program& program);
 
   /// @brief Returns a node for the given `id`.
   /// @param id - any value is accepted.
   /// @return a pointer to some node if `id` exists in this map.
   /// @return `nullptr` otherwise.
-  ast::Node* GetNode(IdType id) const;
+  const ast::Node* GetNode(IdType id) const;
 
   /// @brief Returns an id of the given `node`.
   /// @param node - can be a `nullptr`.
@@ -85,8 +85,8 @@
  private:
   IdType fresh_id_ = 1;
 
-  std::unordered_map<ast::Node*, IdType> node_to_id_;
-  std::unordered_map<IdType, ast::Node*> id_to_node_;
+  std::unordered_map<const ast::Node*, IdType> node_to_id_;
+  std::unordered_map<IdType, const ast::Node*> id_to_node_;
 };
 
 }  // namespace ast_fuzzer
diff --git a/src/ast/alias.cc b/src/ast/alias.cc
index a6983e1..8a6a568 100644
--- a/src/ast/alias.cc
+++ b/src/ast/alias.cc
@@ -21,7 +21,10 @@
 namespace tint {
 namespace ast {
 
-Alias::Alias(ProgramID pid, const Source& src, const Symbol& n, Type* subtype)
+Alias::Alias(ProgramID pid,
+             const Source& src,
+             const Symbol& n,
+             const Type* subtype)
     : Base(pid, src, n), type(subtype) {
   TINT_ASSERT(AST, type);
 }
@@ -30,7 +33,7 @@
 
 Alias::~Alias() = default;
 
-Alias* Alias::Clone(CloneContext* ctx) const {
+const Alias* Alias::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto sym = ctx->Clone(name);
diff --git a/src/ast/alias.h b/src/ast/alias.h
index 7f8acdb..495e470 100644
--- a/src/ast/alias.h
+++ b/src/ast/alias.h
@@ -30,7 +30,10 @@
   /// @param src the source of this node
   /// @param name the symbol for the alias
   /// @param subtype the alias'd type
-  Alias(ProgramID pid, const Source& src, const Symbol& name, Type* subtype);
+  Alias(ProgramID pid,
+        const Source& src,
+        const Symbol& name,
+        const Type* subtype);
   /// Move constructor
   Alias(Alias&&);
   /// Destructor
@@ -39,10 +42,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Alias* Clone(CloneContext* ctx) const override;
+  const Alias* Clone(CloneContext* ctx) const override;
 
   /// the alias type
-  Type* const type;
+  const Type* const type;
 };
 
 }  // namespace ast
diff --git a/src/ast/array.cc b/src/ast/array.cc
index 826efd2..e37de04 100644
--- a/src/ast/array.cc
+++ b/src/ast/array.cc
@@ -25,13 +25,13 @@
 
 namespace {
 // Returns the string representation of an array size expression.
-std::string SizeExprToString(const ast::Expression* size,
+std::string SizeExprToString(const Expression* size,
                              const SymbolTable& symbols) {
-  if (auto* ident = size->As<ast::IdentifierExpression>()) {
+  if (auto* ident = size->As<IdentifierExpression>()) {
     return symbols.NameFor(ident->symbol);
   }
-  if (auto* scalar = size->As<ast::ScalarConstructorExpression>()) {
-    auto* literal = scalar->literal->As<ast::IntLiteral>();
+  if (auto* scalar = size->As<ScalarConstructorExpression>()) {
+    auto* literal = scalar->literal->As<IntLiteral>();
     if (literal) {
       return std::to_string(literal->ValueAsU32());
     }
@@ -44,9 +44,9 @@
 
 Array::Array(ProgramID pid,
              const Source& src,
-             Type* subtype,
-             ast::Expression* cnt,
-             ast::DecorationList decos)
+             const Type* subtype,
+             const Expression* cnt,
+             DecorationList decos)
     : Base(pid, src), type(subtype), count(cnt), decorations(decos) {}
 
 Array::Array(Array&&) = default;
@@ -68,7 +68,7 @@
   return out.str();
 }
 
-Array* Array::Clone(CloneContext* ctx) const {
+const Array* Array::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/array.h b/src/ast/array.h
index 28d8682..7c06f13 100644
--- a/src/ast/array.h
+++ b/src/ast/array.h
@@ -38,9 +38,9 @@
   /// @param decorations the array decorations
   Array(ProgramID pid,
         const Source& src,
-        Type* subtype,
-        ast::Expression* count,
-        ast::DecorationList decorations);
+        const Type* subtype,
+        const Expression* count,
+        DecorationList decorations);
   /// Move constructor
   Array(Array&&);
   ~Array() override;
@@ -57,16 +57,16 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Array* Clone(CloneContext* ctx) const override;
+  const Array* Clone(CloneContext* ctx) const override;
 
   /// the array element type
-  Type* const type;
+  const Type* const type;
 
   /// the array size in elements, or nullptr for a runtime array
-  ast::Expression* const count;
+  const Expression* const count;
 
   /// the array decorations
-  ast::DecorationList const decorations;
+  const DecorationList decorations;
 };
 
 }  // namespace ast
diff --git a/src/ast/array_accessor_expression.cc b/src/ast/array_accessor_expression.cc
index 1f40234..cf2fb7b 100644
--- a/src/ast/array_accessor_expression.cc
+++ b/src/ast/array_accessor_expression.cc
@@ -23,8 +23,8 @@
 
 ArrayAccessorExpression::ArrayAccessorExpression(ProgramID pid,
                                                  const Source& src,
-                                                 Expression* arr,
-                                                 Expression* idx)
+                                                 const Expression* arr,
+                                                 const Expression* idx)
     : Base(pid, src), array(arr), index(idx) {
   TINT_ASSERT(AST, array);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, array, program_id);
@@ -37,7 +37,7 @@
 
 ArrayAccessorExpression::~ArrayAccessorExpression() = default;
 
-ArrayAccessorExpression* ArrayAccessorExpression::Clone(
+const ArrayAccessorExpression* ArrayAccessorExpression::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/array_accessor_expression.h b/src/ast/array_accessor_expression.h
index 543ec1d..0cb1e47 100644
--- a/src/ast/array_accessor_expression.h
+++ b/src/ast/array_accessor_expression.h
@@ -31,8 +31,8 @@
   /// @param idx the index expression
   ArrayAccessorExpression(ProgramID program_id,
                           const Source& source,
-                          Expression* arr,
-                          Expression* idx);
+                          const Expression* arr,
+                          const Expression* idx);
   /// Move constructor
   ArrayAccessorExpression(ArrayAccessorExpression&&);
   ~ArrayAccessorExpression() override;
@@ -41,13 +41,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ArrayAccessorExpression* Clone(CloneContext* ctx) const override;
+  const ArrayAccessorExpression* Clone(CloneContext* ctx) const override;
 
   /// the array
-  Expression* const array;
+  const Expression* const array;
 
   /// the index expression
-  Expression* const index;
+  const Expression* const index;
 };
 
 }  // namespace ast
diff --git a/src/ast/assignment_statement.cc b/src/ast/assignment_statement.cc
index 0031bf6..b6a2907 100644
--- a/src/ast/assignment_statement.cc
+++ b/src/ast/assignment_statement.cc
@@ -23,8 +23,8 @@
 
 AssignmentStatement::AssignmentStatement(ProgramID pid,
                                          const Source& src,
-                                         Expression* l,
-                                         Expression* r)
+                                         const Expression* l,
+                                         const Expression* r)
     : Base(pid, src), lhs(l), rhs(r) {
   TINT_ASSERT(AST, lhs);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
@@ -36,7 +36,7 @@
 
 AssignmentStatement::~AssignmentStatement() = default;
 
-AssignmentStatement* AssignmentStatement::Clone(CloneContext* ctx) const {
+const AssignmentStatement* AssignmentStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* l = ctx->Clone(lhs);
diff --git a/src/ast/assignment_statement.h b/src/ast/assignment_statement.h
index c6fdc5a..ee8fd3d 100644
--- a/src/ast/assignment_statement.h
+++ b/src/ast/assignment_statement.h
@@ -31,8 +31,8 @@
   /// @param rhs the right side of the expression
   AssignmentStatement(ProgramID program_id,
                       const Source& source,
-                      Expression* lhs,
-                      Expression* rhs);
+                      const Expression* lhs,
+                      const Expression* rhs);
   /// Move constructor
   AssignmentStatement(AssignmentStatement&&);
   ~AssignmentStatement() override;
@@ -41,13 +41,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  AssignmentStatement* Clone(CloneContext* ctx) const override;
+  const AssignmentStatement* Clone(CloneContext* ctx) const override;
 
   /// left side expression
-  Expression* const lhs;
+  const Expression* const lhs;
 
   /// right side expression
-  Expression* const rhs;
+  const Expression* const rhs;
 };
 
 }  // namespace ast
diff --git a/src/ast/atomic.cc b/src/ast/atomic.cc
index 635e758..af35e4f 100644
--- a/src/ast/atomic.cc
+++ b/src/ast/atomic.cc
@@ -21,7 +21,7 @@
 namespace tint {
 namespace ast {
 
-Atomic::Atomic(ProgramID pid, const Source& src, Type* const subtype)
+Atomic::Atomic(ProgramID pid, const Source& src, const Type* const subtype)
     : Base(pid, src), type(subtype) {}
 
 std::string Atomic::FriendlyName(const SymbolTable& symbols) const {
@@ -34,10 +34,10 @@
 
 Atomic::~Atomic() = default;
 
-Atomic* Atomic::Clone(CloneContext* ctx) const {
+const Atomic* Atomic::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
-  auto* ty = ctx->Clone(const_cast<Type*>(type));
+  auto* ty = ctx->Clone(type);
   return ctx->dst->create<Atomic>(src, ty);
 }
 
diff --git a/src/ast/atomic.h b/src/ast/atomic.h
index f00d128..4dcc7a0 100644
--- a/src/ast/atomic.h
+++ b/src/ast/atomic.h
@@ -29,7 +29,7 @@
   /// @param pid the identifier of the program that owns this node
   /// @param src the source of this node
   /// @param subtype the pointee type
-  Atomic(ProgramID pid, const Source& src, Type* const subtype);
+  Atomic(ProgramID pid, const Source& src, const Type* const subtype);
   /// Move constructor
   Atomic(Atomic&&);
   ~Atomic() override;
@@ -42,10 +42,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Atomic* Clone(CloneContext* ctx) const override;
+  const Atomic* Clone(CloneContext* ctx) const override;
 
   /// the pointee type
-  Type const* const type;
+  const Type* const type;
 };
 
 }  // namespace ast
diff --git a/src/ast/binary_expression.cc b/src/ast/binary_expression.cc
index f757bfe..f025f44 100644
--- a/src/ast/binary_expression.cc
+++ b/src/ast/binary_expression.cc
@@ -24,8 +24,8 @@
 BinaryExpression::BinaryExpression(ProgramID pid,
                                    const Source& src,
                                    BinaryOp o,
-                                   Expression* l,
-                                   Expression* r)
+                                   const Expression* l,
+                                   const Expression* r)
     : Base(pid, src), op(o), lhs(l), rhs(r) {
   TINT_ASSERT(AST, lhs);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
@@ -38,7 +38,7 @@
 
 BinaryExpression::~BinaryExpression() = default;
 
-BinaryExpression* BinaryExpression::Clone(CloneContext* ctx) const {
+const BinaryExpression* BinaryExpression::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* l = ctx->Clone(lhs);
diff --git a/src/ast/binary_expression.h b/src/ast/binary_expression.h
index bca7aa4..73b4337 100644
--- a/src/ast/binary_expression.h
+++ b/src/ast/binary_expression.h
@@ -55,8 +55,8 @@
   BinaryExpression(ProgramID program_id,
                    const Source& source,
                    BinaryOp op,
-                   Expression* lhs,
-                   Expression* rhs);
+                   const Expression* lhs,
+                   const Expression* rhs);
   /// Move constructor
   BinaryExpression(BinaryExpression&&);
   ~BinaryExpression() override;
@@ -110,14 +110,14 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BinaryExpression* Clone(CloneContext* ctx) const override;
+  const BinaryExpression* Clone(CloneContext* ctx) const override;
 
   /// the binary op type
-  BinaryOp const op;
+  const BinaryOp op;
   /// the left side expression
-  Expression* const lhs;
+  const Expression* const lhs;
   /// the right side expression
-  Expression* const rhs;
+  const Expression* const rhs;
 };
 
 inline bool BinaryExpression::IsArithmetic() const {
diff --git a/src/ast/binding_decoration.cc b/src/ast/binding_decoration.cc
index 1cdc219..6924dfa 100644
--- a/src/ast/binding_decoration.cc
+++ b/src/ast/binding_decoration.cc
@@ -34,7 +34,7 @@
   return "binding";
 }
 
-BindingDecoration* BindingDecoration::Clone(CloneContext* ctx) const {
+const BindingDecoration* BindingDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<BindingDecoration>(src, value);
diff --git a/src/ast/binding_decoration.h b/src/ast/binding_decoration.h
index d9c2c14..1ada708 100644
--- a/src/ast/binding_decoration.h
+++ b/src/ast/binding_decoration.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BindingDecoration* Clone(CloneContext* ctx) const override;
+  const BindingDecoration* Clone(CloneContext* ctx) const override;
 
   /// the binding value
-  uint32_t const value;
+  const uint32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/bitcast_expression.cc b/src/ast/bitcast_expression.cc
index e58b5e5..5a76492 100644
--- a/src/ast/bitcast_expression.cc
+++ b/src/ast/bitcast_expression.cc
@@ -23,8 +23,8 @@
 
 BitcastExpression::BitcastExpression(ProgramID pid,
                                      const Source& src,
-                                     ast::Type* t,
-                                     Expression* e)
+                                     const Type* t,
+                                     const Expression* e)
     : Base(pid, src), type(t), expr(e) {
   TINT_ASSERT(AST, type);
   TINT_ASSERT(AST, expr);
@@ -34,7 +34,7 @@
 BitcastExpression::BitcastExpression(BitcastExpression&&) = default;
 BitcastExpression::~BitcastExpression() = default;
 
-BitcastExpression* BitcastExpression::Clone(CloneContext* ctx) const {
+const BitcastExpression* BitcastExpression::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* t = ctx->Clone(type);
diff --git a/src/ast/bitcast_expression.h b/src/ast/bitcast_expression.h
index a599679..fe2c134 100644
--- a/src/ast/bitcast_expression.h
+++ b/src/ast/bitcast_expression.h
@@ -33,8 +33,8 @@
   /// @param expr the expr
   BitcastExpression(ProgramID program_id,
                     const Source& source,
-                    ast::Type* type,
-                    Expression* expr);
+                    const Type* type,
+                    const Expression* expr);
   /// Move constructor
   BitcastExpression(BitcastExpression&&);
   ~BitcastExpression() override;
@@ -43,12 +43,12 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BitcastExpression* Clone(CloneContext* ctx) const override;
+  const BitcastExpression* Clone(CloneContext* ctx) const override;
 
   /// the target cast type
-  ast::Type* const type;
+  const Type* const type;
   /// the expression
-  Expression* const expr;
+  const Expression* const expr;
 };
 
 }  // namespace ast
diff --git a/src/ast/block_statement.cc b/src/ast/block_statement.cc
index b0642f4..395cc5b 100644
--- a/src/ast/block_statement.cc
+++ b/src/ast/block_statement.cc
@@ -35,7 +35,7 @@
 
 BlockStatement::~BlockStatement() = default;
 
-BlockStatement* BlockStatement::Clone(CloneContext* ctx) const {
+const BlockStatement* BlockStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto stmts = ctx->Clone(statements);
diff --git a/src/ast/block_statement.h b/src/ast/block_statement.h
index f8f39b1..56bdc43 100644
--- a/src/ast/block_statement.h
+++ b/src/ast/block_statement.h
@@ -40,7 +40,7 @@
   bool Empty() const { return statements.empty(); }
 
   /// @returns the last statement in the block or nullptr if block empty
-  Statement* Last() const {
+  const Statement* Last() const {
     return statements.empty() ? nullptr : statements.back();
   }
 
@@ -48,10 +48,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BlockStatement* Clone(CloneContext* ctx) const override;
+  const BlockStatement* Clone(CloneContext* ctx) const override;
 
   /// the statement list
-  StatementList const statements;
+  const StatementList statements;
 };
 
 }  // namespace ast
diff --git a/src/ast/bool.cc b/src/ast/bool.cc
index f7caf9b..1148107 100644
--- a/src/ast/bool.cc
+++ b/src/ast/bool.cc
@@ -31,7 +31,7 @@
   return "bool";
 }
 
-Bool* Bool::Clone(CloneContext* ctx) const {
+const Bool* Bool::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<Bool>(src);
 }
diff --git a/src/ast/bool.h b/src/ast/bool.h
index d3d8702..41625d4 100644
--- a/src/ast/bool.h
+++ b/src/ast/bool.h
@@ -47,7 +47,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Bool* Clone(CloneContext* ctx) const override;
+  const Bool* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/bool_literal.cc b/src/ast/bool_literal.cc
index e6f256e..77e7e28 100644
--- a/src/ast/bool_literal.cc
+++ b/src/ast/bool_literal.cc
@@ -26,7 +26,7 @@
 
 BoolLiteral::~BoolLiteral() = default;
 
-BoolLiteral* BoolLiteral::Clone(CloneContext* ctx) const {
+const BoolLiteral* BoolLiteral::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<BoolLiteral>(src, value);
diff --git a/src/ast/bool_literal.h b/src/ast/bool_literal.h
index da5f571..e385f1d 100644
--- a/src/ast/bool_literal.h
+++ b/src/ast/bool_literal.h
@@ -32,15 +32,14 @@
   BoolLiteral(ProgramID pid, const Source& src, bool value);
   ~BoolLiteral() override;
 
-
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BoolLiteral* Clone(CloneContext* ctx) const override;
+  const BoolLiteral* Clone(CloneContext* ctx) const override;
 
   /// The boolean literal value
-  bool const value;
+  const bool value;
 };
 
 }  // namespace ast
diff --git a/src/ast/break_statement.cc b/src/ast/break_statement.cc
index 8bdde65..1a515cc 100644
--- a/src/ast/break_statement.cc
+++ b/src/ast/break_statement.cc
@@ -28,7 +28,7 @@
 
 BreakStatement::~BreakStatement() = default;
 
-BreakStatement* BreakStatement::Clone(CloneContext* ctx) const {
+const BreakStatement* BreakStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<BreakStatement>(src);
diff --git a/src/ast/break_statement.h b/src/ast/break_statement.h
index 15aeebe..f88212c 100644
--- a/src/ast/break_statement.h
+++ b/src/ast/break_statement.h
@@ -35,7 +35,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BreakStatement* Clone(CloneContext* ctx) const override;
+  const BreakStatement* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/builtin_decoration.cc b/src/ast/builtin_decoration.cc
index db3dfe4..f7e44b6 100644
--- a/src/ast/builtin_decoration.cc
+++ b/src/ast/builtin_decoration.cc
@@ -34,7 +34,7 @@
   return "builtin";
 }
 
-BuiltinDecoration* BuiltinDecoration::Clone(CloneContext* ctx) const {
+const BuiltinDecoration* BuiltinDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<BuiltinDecoration>(src, builtin);
diff --git a/src/ast/builtin_decoration.h b/src/ast/builtin_decoration.h
index d312103..f5a2da4 100644
--- a/src/ast/builtin_decoration.h
+++ b/src/ast/builtin_decoration.h
@@ -40,10 +40,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  BuiltinDecoration* Clone(CloneContext* ctx) const override;
+  const BuiltinDecoration* Clone(CloneContext* ctx) const override;
 
   /// The builtin value
-  Builtin const builtin;
+  const Builtin builtin;
 };
 
 }  // namespace ast
diff --git a/src/ast/call_expression.cc b/src/ast/call_expression.cc
index 43e7a44..d4ffa6c 100644
--- a/src/ast/call_expression.cc
+++ b/src/ast/call_expression.cc
@@ -23,7 +23,7 @@
 
 CallExpression::CallExpression(ProgramID pid,
                                const Source& src,
-                               IdentifierExpression* fn,
+                               const IdentifierExpression* fn,
                                ExpressionList a)
     : Base(pid, src), func(fn), args(a) {
   TINT_ASSERT(AST, func);
@@ -38,7 +38,7 @@
 
 CallExpression::~CallExpression() = default;
 
-CallExpression* CallExpression::Clone(CloneContext* ctx) const {
+const CallExpression* CallExpression::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* fn = ctx->Clone(func);
diff --git a/src/ast/call_expression.h b/src/ast/call_expression.h
index 6693e2f..4bd6fc9 100644
--- a/src/ast/call_expression.h
+++ b/src/ast/call_expression.h
@@ -33,7 +33,7 @@
   /// @param args the arguments
   CallExpression(ProgramID program_id,
                  const Source& source,
-                 IdentifierExpression* func,
+                 const IdentifierExpression* func,
                  ExpressionList args);
   /// Move constructor
   CallExpression(CallExpression&&);
@@ -43,12 +43,12 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  CallExpression* Clone(CloneContext* ctx) const override;
+  const CallExpression* Clone(CloneContext* ctx) const override;
 
   /// The target function
-  IdentifierExpression* const func;
+  const IdentifierExpression* const func;
   /// The arguments
-  ExpressionList const args;
+  const ExpressionList args;
 };
 
 }  // namespace ast
diff --git a/src/ast/call_statement.cc b/src/ast/call_statement.cc
index 540f2ad..f8a24b8 100644
--- a/src/ast/call_statement.cc
+++ b/src/ast/call_statement.cc
@@ -23,7 +23,7 @@
 
 CallStatement::CallStatement(ProgramID pid,
                              const Source& src,
-                             CallExpression* call)
+                             const CallExpression* call)
     : Base(pid, src), expr(call) {
   TINT_ASSERT(AST, expr);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
@@ -33,7 +33,7 @@
 
 CallStatement::~CallStatement() = default;
 
-CallStatement* CallStatement::Clone(CloneContext* ctx) const {
+const CallStatement* CallStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* call = ctx->Clone(expr);
diff --git a/src/ast/call_statement.h b/src/ast/call_statement.h
index 130e7f5..946e7ea 100644
--- a/src/ast/call_statement.h
+++ b/src/ast/call_statement.h
@@ -28,7 +28,7 @@
   /// @param pid the identifier of the program that owns this node
   /// @param src the source of this node for the statement
   /// @param call the function
-  CallStatement(ProgramID pid, const Source& src, CallExpression* call);
+  CallStatement(ProgramID pid, const Source& src, const CallExpression* call);
   /// Move constructor
   CallStatement(CallStatement&&);
   ~CallStatement() override;
@@ -37,10 +37,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  CallStatement* Clone(CloneContext* ctx) const override;
+  const CallStatement* Clone(CloneContext* ctx) const override;
 
   /// The call expression
-  CallExpression* const expr;
+  const CallExpression* const expr;
 };
 
 }  // namespace ast
diff --git a/src/ast/case_statement.cc b/src/ast/case_statement.cc
index 71fe7f9..580f477 100644
--- a/src/ast/case_statement.cc
+++ b/src/ast/case_statement.cc
@@ -24,7 +24,7 @@
 CaseStatement::CaseStatement(ProgramID pid,
                              const Source& src,
                              CaseSelectorList s,
-                             BlockStatement* b)
+                             const BlockStatement* b)
     : Base(pid, src), selectors(s), body(b) {
   TINT_ASSERT(AST, body);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
@@ -38,7 +38,7 @@
 
 CaseStatement::~CaseStatement() = default;
 
-CaseStatement* CaseStatement::Clone(CloneContext* ctx) const {
+const CaseStatement* CaseStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto sel = ctx->Clone(selectors);
diff --git a/src/ast/case_statement.h b/src/ast/case_statement.h
index ed96a11..7b7af0c 100644
--- a/src/ast/case_statement.h
+++ b/src/ast/case_statement.h
@@ -24,7 +24,7 @@
 namespace ast {
 
 /// A list of case literals
-using CaseSelectorList = std::vector<IntLiteral*>;
+using CaseSelectorList = std::vector<const IntLiteral*>;
 
 /// A case statement
 class CaseStatement : public Castable<CaseStatement, Statement> {
@@ -37,7 +37,7 @@
   CaseStatement(ProgramID pid,
                 const Source& src,
                 CaseSelectorList selectors,
-                BlockStatement* body);
+                const BlockStatement* body);
   /// Move constructor
   CaseStatement(CaseStatement&&);
   ~CaseStatement() override;
@@ -49,17 +49,17 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  CaseStatement* Clone(CloneContext* ctx) const override;
+  const CaseStatement* Clone(CloneContext* ctx) const override;
 
   /// The case selectors, empty if none set
-  CaseSelectorList const selectors;
+  const CaseSelectorList selectors;
 
   /// The case body
-  BlockStatement* const body;
+  const BlockStatement* const body;
 };
 
 /// A list of case statements
-using CaseStatementList = std::vector<CaseStatement*>;
+using CaseStatementList = std::vector<const CaseStatement*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/continue_statement.cc b/src/ast/continue_statement.cc
index 222f7f5..9337f7b 100644
--- a/src/ast/continue_statement.cc
+++ b/src/ast/continue_statement.cc
@@ -28,7 +28,7 @@
 
 ContinueStatement::~ContinueStatement() = default;
 
-ContinueStatement* ContinueStatement::Clone(CloneContext* ctx) const {
+const ContinueStatement* ContinueStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<ContinueStatement>(src);
diff --git a/src/ast/continue_statement.h b/src/ast/continue_statement.h
index 73445a3..580c630 100644
--- a/src/ast/continue_statement.h
+++ b/src/ast/continue_statement.h
@@ -35,7 +35,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ContinueStatement* Clone(CloneContext* ctx) const override;
+  const ContinueStatement* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/decoration.h b/src/ast/decoration.h
index d915de3..c3c9258 100644
--- a/src/ast/decoration.h
+++ b/src/ast/decoration.h
@@ -39,7 +39,7 @@
 };
 
 /// A list of decorations
-using DecorationList = std::vector<Decoration*>;
+using DecorationList = std::vector<const Decoration*>;
 
 /// @param decorations the list of decorations to search
 /// @returns true if `decorations` includes a decoration of type `T`
@@ -56,7 +56,7 @@
 /// @param decorations the list of decorations to search
 /// @returns a pointer to `T` from `decorations` if found, otherwise nullptr.
 template <typename T>
-T* GetDecoration(const DecorationList& decorations) {
+const T* GetDecoration(const DecorationList& decorations) {
   for (auto* deco : decorations) {
     if (deco->Is<T>()) {
       return deco->As<T>();
diff --git a/src/ast/depth_multisampled_texture.cc b/src/ast/depth_multisampled_texture.cc
index 5985fee..c870585 100644
--- a/src/ast/depth_multisampled_texture.cc
+++ b/src/ast/depth_multisampled_texture.cc
@@ -46,7 +46,7 @@
   return out.str();
 }
 
-DepthMultisampledTexture* DepthMultisampledTexture::Clone(
+const DepthMultisampledTexture* DepthMultisampledTexture::Clone(
     CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<DepthMultisampledTexture>(src, dim);
diff --git a/src/ast/depth_multisampled_texture.h b/src/ast/depth_multisampled_texture.h
index c274d93..4797e03 100644
--- a/src/ast/depth_multisampled_texture.h
+++ b/src/ast/depth_multisampled_texture.h
@@ -45,7 +45,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  DepthMultisampledTexture* Clone(CloneContext* ctx) const override;
+  const DepthMultisampledTexture* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/depth_texture.cc b/src/ast/depth_texture.cc
index 39f576c..56f222a 100644
--- a/src/ast/depth_texture.cc
+++ b/src/ast/depth_texture.cc
@@ -44,7 +44,7 @@
   return out.str();
 }
 
-DepthTexture* DepthTexture::Clone(CloneContext* ctx) const {
+const DepthTexture* DepthTexture::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<DepthTexture>(src, dim);
 }
diff --git a/src/ast/depth_texture.h b/src/ast/depth_texture.h
index f2e1e10..530dea2 100644
--- a/src/ast/depth_texture.h
+++ b/src/ast/depth_texture.h
@@ -42,7 +42,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  DepthTexture* Clone(CloneContext* ctx) const override;
+  const DepthTexture* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/disable_validation_decoration.cc b/src/ast/disable_validation_decoration.cc
index 69c4605..96f28a2 100644
--- a/src/ast/disable_validation_decoration.cc
+++ b/src/ast/disable_validation_decoration.cc
@@ -47,7 +47,7 @@
   return "<invalid>";
 }
 
-DisableValidationDecoration* DisableValidationDecoration::Clone(
+const DisableValidationDecoration* DisableValidationDecoration::Clone(
     CloneContext* ctx) const {
   return ctx->dst->ASTNodes().Create<DisableValidationDecoration>(
       ctx->dst->ID(), validation);
diff --git a/src/ast/disable_validation_decoration.h b/src/ast/disable_validation_decoration.h
index 2d0fd89..099a669 100644
--- a/src/ast/disable_validation_decoration.h
+++ b/src/ast/disable_validation_decoration.h
@@ -71,10 +71,10 @@
   /// Performs a deep clone of this object using the CloneContext `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned object
-  DisableValidationDecoration* Clone(CloneContext* ctx) const override;
+  const DisableValidationDecoration* Clone(CloneContext* ctx) const override;
 
   /// The validation that this decoration disables
-  DisabledValidation const validation;
+  const DisabledValidation validation;
 };
 
 }  // namespace ast
diff --git a/src/ast/discard_statement.cc b/src/ast/discard_statement.cc
index fbc5d28..c50b989 100644
--- a/src/ast/discard_statement.cc
+++ b/src/ast/discard_statement.cc
@@ -28,7 +28,7 @@
 
 DiscardStatement::~DiscardStatement() = default;
 
-DiscardStatement* DiscardStatement::Clone(CloneContext* ctx) const {
+const DiscardStatement* DiscardStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<DiscardStatement>(src);
diff --git a/src/ast/discard_statement.h b/src/ast/discard_statement.h
index 1284b75..c670656 100644
--- a/src/ast/discard_statement.h
+++ b/src/ast/discard_statement.h
@@ -35,7 +35,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  DiscardStatement* Clone(CloneContext* ctx) const override;
+  const DiscardStatement* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/else_statement.cc b/src/ast/else_statement.cc
index 8c3bd59..02ed656 100644
--- a/src/ast/else_statement.cc
+++ b/src/ast/else_statement.cc
@@ -23,8 +23,8 @@
 
 ElseStatement::ElseStatement(ProgramID pid,
                              const Source& src,
-                             Expression* cond,
-                             BlockStatement* b)
+                             const Expression* cond,
+                             const BlockStatement* b)
     : Base(pid, src), condition(cond), body(b) {
   TINT_ASSERT(AST, body);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
@@ -35,7 +35,7 @@
 
 ElseStatement::~ElseStatement() = default;
 
-ElseStatement* ElseStatement::Clone(CloneContext* ctx) const {
+const ElseStatement* ElseStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* cond = ctx->Clone(condition);
diff --git a/src/ast/else_statement.h b/src/ast/else_statement.h
index 6dcc2c9..53a4ba4 100644
--- a/src/ast/else_statement.h
+++ b/src/ast/else_statement.h
@@ -33,8 +33,8 @@
   /// @param body the else body
   ElseStatement(ProgramID pid,
                 const Source& src,
-                Expression* condition,
-                BlockStatement* body);
+                const Expression* condition,
+                const BlockStatement* body);
   /// Move constructor
   ElseStatement(ElseStatement&&);
   ~ElseStatement() override;
@@ -43,17 +43,17 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ElseStatement* Clone(CloneContext* ctx) const override;
+  const ElseStatement* Clone(CloneContext* ctx) const override;
 
   /// The else condition or nullptr if none set
-  Expression* const condition;
+  const Expression* const condition;
 
   /// The else body
-  BlockStatement* const body;
+  const BlockStatement* const body;
 };
 
 /// A list of else statements
-using ElseStatementList = std::vector<ElseStatement*>;
+using ElseStatementList = std::vector<const ElseStatement*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/expression.h b/src/ast/expression.h
index b5a51f2..408c55a 100644
--- a/src/ast/expression.h
+++ b/src/ast/expression.h
@@ -39,7 +39,7 @@
 };
 
 /// A list of expressions
-using ExpressionList = std::vector<Expression*>;
+using ExpressionList = std::vector<const Expression*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/external_texture.cc b/src/ast/external_texture.cc
index cdd120c..ded9580 100644
--- a/src/ast/external_texture.cc
+++ b/src/ast/external_texture.cc
@@ -33,7 +33,7 @@
   return "texture_external";
 }
 
-ExternalTexture* ExternalTexture::Clone(CloneContext* ctx) const {
+const ExternalTexture* ExternalTexture::Clone(CloneContext* ctx) const {
   return ctx->dst->create<ExternalTexture>();
 }
 
diff --git a/src/ast/external_texture.h b/src/ast/external_texture.h
index 250f466..734ba8e 100644
--- a/src/ast/external_texture.h
+++ b/src/ast/external_texture.h
@@ -42,7 +42,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  ExternalTexture* Clone(CloneContext* ctx) const override;
+  const ExternalTexture* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/f32.cc b/src/ast/f32.cc
index cc5f0d1..b455036 100644
--- a/src/ast/f32.cc
+++ b/src/ast/f32.cc
@@ -31,7 +31,7 @@
   return "f32";
 }
 
-F32* F32::Clone(CloneContext* ctx) const {
+const F32* F32::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<F32>(src);
 }
diff --git a/src/ast/f32.h b/src/ast/f32.h
index 804ea55..4643edc 100644
--- a/src/ast/f32.h
+++ b/src/ast/f32.h
@@ -41,7 +41,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  F32* Clone(CloneContext* ctx) const override;
+  const F32* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/fallthrough_statement.cc b/src/ast/fallthrough_statement.cc
index cac4d86..316405c 100644
--- a/src/ast/fallthrough_statement.cc
+++ b/src/ast/fallthrough_statement.cc
@@ -28,7 +28,8 @@
 
 FallthroughStatement::~FallthroughStatement() = default;
 
-FallthroughStatement* FallthroughStatement::Clone(CloneContext* ctx) const {
+const FallthroughStatement* FallthroughStatement::Clone(
+    CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<FallthroughStatement>(src);
diff --git a/src/ast/fallthrough_statement.h b/src/ast/fallthrough_statement.h
index 282869f..e715710 100644
--- a/src/ast/fallthrough_statement.h
+++ b/src/ast/fallthrough_statement.h
@@ -35,7 +35,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  FallthroughStatement* Clone(CloneContext* ctx) const override;
+  const FallthroughStatement* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/float_literal.cc b/src/ast/float_literal.cc
index 94bc68e..c895983 100644
--- a/src/ast/float_literal.cc
+++ b/src/ast/float_literal.cc
@@ -28,7 +28,7 @@
 
 FloatLiteral::~FloatLiteral() = default;
 
-FloatLiteral* FloatLiteral::Clone(CloneContext* ctx) const {
+const FloatLiteral* FloatLiteral::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<FloatLiteral>(src, value);
diff --git a/src/ast/float_literal.h b/src/ast/float_literal.h
index 990b7e4..469e7a8 100644
--- a/src/ast/float_literal.h
+++ b/src/ast/float_literal.h
@@ -36,10 +36,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  FloatLiteral* Clone(CloneContext* ctx) const override;
+  const FloatLiteral* Clone(CloneContext* ctx) const override;
 
   /// The float literal value
-  float const value;
+  const float value;
 };
 
 }  // namespace ast
diff --git a/src/ast/for_loop_statement.cc b/src/ast/for_loop_statement.cc
index a1eab18..ec54edf 100644
--- a/src/ast/for_loop_statement.cc
+++ b/src/ast/for_loop_statement.cc
@@ -23,10 +23,10 @@
 
 ForLoopStatement::ForLoopStatement(ProgramID pid,
                                    const Source& src,
-                                   Statement* init,
-                                   Expression* cond,
-                                   Statement* cont,
-                                   BlockStatement* b)
+                                   const Statement* init,
+                                   const Expression* cond,
+                                   const Statement* cont,
+                                   const BlockStatement* b)
     : Base(pid, src),
       initializer(init),
       condition(cond),
@@ -44,7 +44,7 @@
 
 ForLoopStatement::~ForLoopStatement() = default;
 
-ForLoopStatement* ForLoopStatement::Clone(CloneContext* ctx) const {
+const ForLoopStatement* ForLoopStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
 
diff --git a/src/ast/for_loop_statement.h b/src/ast/for_loop_statement.h
index 72b5026..5684c12 100644
--- a/src/ast/for_loop_statement.h
+++ b/src/ast/for_loop_statement.h
@@ -33,11 +33,11 @@
   /// @param continuing the optional continuing statement
   /// @param body the loop body
   ForLoopStatement(ProgramID program_id,
-                   const Source& source,
-                   Statement* initializer,
-                   Expression* condition,
-                   Statement* continuing,
-                   BlockStatement* body);
+                   Source const& source,
+                   const Statement* initializer,
+                   const Expression* condition,
+                   const Statement* continuing,
+                   const BlockStatement* body);
   /// Move constructor
   ForLoopStatement(ForLoopStatement&&);
   ~ForLoopStatement() override;
@@ -46,19 +46,19 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ForLoopStatement* Clone(CloneContext* ctx) const override;
+  const ForLoopStatement* Clone(CloneContext* ctx) const override;
 
   /// The initializer statement
-  Statement* const initializer;
+  const Statement* const initializer;
 
   /// The condition expression
-  Expression* const condition;
+  const Expression* const condition;
 
   /// The continuing statement
-  Statement* const continuing;
+  const Statement* const continuing;
 
   /// The loop body block
-  BlockStatement* const body;
+  const BlockStatement* const body;
 };
 
 }  // namespace ast
diff --git a/src/ast/function.cc b/src/ast/function.cc
index fa44f3e..ddbf34f 100644
--- a/src/ast/function.cc
+++ b/src/ast/function.cc
@@ -27,8 +27,8 @@
                    const Source& src,
                    Symbol sym,
                    VariableList parameters,
-                   ast::Type* return_ty,
-                   BlockStatement* b,
+                   const Type* return_ty,
+                   const BlockStatement* b,
                    DecorationList decos,
                    DecorationList return_type_decos)
     : Base(pid, src),
@@ -65,7 +65,7 @@
   return PipelineStage::kNone;
 }
 
-Function* Function::Clone(CloneContext* ctx) const {
+const Function* Function::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto sym = ctx->Clone(symbol);
@@ -77,7 +77,7 @@
   return ctx->dst->create<Function>(src, sym, p, ret, b, decos, ret_decos);
 }
 
-Function* FunctionList::Find(Symbol sym) const {
+const Function* FunctionList::Find(Symbol sym) const {
   for (auto* func : *this) {
     if (func->symbol == sym) {
       return func;
@@ -86,7 +86,7 @@
   return nullptr;
 }
 
-Function* FunctionList::Find(Symbol sym, PipelineStage stage) const {
+const Function* FunctionList::Find(Symbol sym, PipelineStage stage) const {
   for (auto* func : *this) {
     if (func->symbol == sym && func->PipelineStage() == stage) {
       return func;
diff --git a/src/ast/function.h b/src/ast/function.h
index a76c6e1..b546c09 100644
--- a/src/ast/function.h
+++ b/src/ast/function.h
@@ -48,8 +48,8 @@
            const Source& source,
            Symbol symbol,
            VariableList params,
-           ast::Type* return_type,
-           BlockStatement* body,
+           const Type* return_type,
+           const BlockStatement* body,
            DecorationList decorations,
            DecorationList return_type_decorations);
   /// Move constructor
@@ -67,44 +67,44 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  Function* Clone(CloneContext* ctx) const override;
+  const Function* Clone(CloneContext* ctx) const override;
 
   /// The function symbol
-  Symbol const symbol;
+  const Symbol symbol;
 
   /// The function params
-  VariableList const params;
+  const VariableList params;
 
   /// The function return type
-  ast::Type* const return_type;
+  const Type* const return_type;
 
   /// The function body
-  BlockStatement* const body;
+  const BlockStatement* const body;
 
   /// The decorations attached to this function
-  DecorationList const decorations;
+  const DecorationList decorations;
 
   /// The decorations attached to the function return type.
-  DecorationList const return_type_decorations;
+  const DecorationList return_type_decorations;
 };
 
 /// A list of functions
-class FunctionList : public std::vector<Function*> {
+class FunctionList : public std::vector<const Function*> {
  public:
   /// Appends f to the end of the list
   /// @param f the function to append to this list
-  void Add(Function* f) { this->emplace_back(f); }
+  void Add(const Function* f) { this->emplace_back(f); }
 
   /// Returns the function with the given name
   /// @param sym the function symbol to search for
   /// @returns the associated function or nullptr if none exists
-  Function* Find(Symbol sym) const;
+  const Function* Find(Symbol sym) const;
 
   /// Returns the function with the given name
   /// @param sym the function symbol to search for
   /// @param stage the pipeline stage
   /// @returns the associated function or nullptr if none exists
-  Function* Find(Symbol sym, PipelineStage stage) const;
+  const Function* Find(Symbol sym, PipelineStage stage) const;
 
   /// @param stage the pipeline stage
   /// @returns true if the Builder contains an entrypoint function with
diff --git a/src/ast/group_decoration.cc b/src/ast/group_decoration.cc
index 571794e..66f2fab 100644
--- a/src/ast/group_decoration.cc
+++ b/src/ast/group_decoration.cc
@@ -32,7 +32,7 @@
   return "group";
 }
 
-GroupDecoration* GroupDecoration::Clone(CloneContext* ctx) const {
+const GroupDecoration* GroupDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<GroupDecoration>(src, value);
diff --git a/src/ast/group_decoration.h b/src/ast/group_decoration.h
index de14126..e00488f 100644
--- a/src/ast/group_decoration.h
+++ b/src/ast/group_decoration.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  GroupDecoration* Clone(CloneContext* ctx) const override;
+  const GroupDecoration* Clone(CloneContext* ctx) const override;
 
   /// The group value
-  uint32_t const value;
+  const uint32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/i32.cc b/src/ast/i32.cc
index b9145d3..50207c3 100644
--- a/src/ast/i32.cc
+++ b/src/ast/i32.cc
@@ -31,7 +31,7 @@
   return "i32";
 }
 
-I32* I32::Clone(CloneContext* ctx) const {
+const I32* I32::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<I32>(src);
 }
diff --git a/src/ast/i32.h b/src/ast/i32.h
index 63e66e4..8a76cf2 100644
--- a/src/ast/i32.h
+++ b/src/ast/i32.h
@@ -41,7 +41,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  I32* Clone(CloneContext* ctx) const override;
+  const I32* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/identifier_expression.cc b/src/ast/identifier_expression.cc
index da80ff0..f0c50c4 100644
--- a/src/ast/identifier_expression.cc
+++ b/src/ast/identifier_expression.cc
@@ -33,7 +33,8 @@
 
 IdentifierExpression::~IdentifierExpression() = default;
 
-IdentifierExpression* IdentifierExpression::Clone(CloneContext* ctx) const {
+const IdentifierExpression* IdentifierExpression::Clone(
+    CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto sym = ctx->Clone(symbol);
diff --git a/src/ast/identifier_expression.h b/src/ast/identifier_expression.h
index 7977b29..e238cde 100644
--- a/src/ast/identifier_expression.h
+++ b/src/ast/identifier_expression.h
@@ -36,10 +36,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  IdentifierExpression* Clone(CloneContext* ctx) const override;
+  const IdentifierExpression* Clone(CloneContext* ctx) const override;
 
   /// The symbol for the identifier
-  Symbol const symbol;
+  const Symbol symbol;
 };
 
 }  // namespace ast
diff --git a/src/ast/if_statement.cc b/src/ast/if_statement.cc
index f4120d7..31708d0 100644
--- a/src/ast/if_statement.cc
+++ b/src/ast/if_statement.cc
@@ -23,8 +23,8 @@
 
 IfStatement::IfStatement(ProgramID pid,
                          const Source& src,
-                         Expression* cond,
-                         BlockStatement* b,
+                         const Expression* cond,
+                         const BlockStatement* b,
                          ElseStatementList else_stmts)
     : Base(pid, src),
       condition(cond),
@@ -44,7 +44,7 @@
 
 IfStatement::~IfStatement() = default;
 
-IfStatement* IfStatement::Clone(CloneContext* ctx) const {
+const IfStatement* IfStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* cond = ctx->Clone(condition);
diff --git a/src/ast/if_statement.h b/src/ast/if_statement.h
index cd37ba0..d4c54e1 100644
--- a/src/ast/if_statement.h
+++ b/src/ast/if_statement.h
@@ -33,8 +33,8 @@
   /// @param else_stmts the else statements
   IfStatement(ProgramID pid,
               const Source& src,
-              Expression* condition,
-              BlockStatement* body,
+              const Expression* condition,
+              const BlockStatement* body,
               ElseStatementList else_stmts);
   /// Move constructor
   IfStatement(IfStatement&&);
@@ -44,16 +44,16 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  IfStatement* Clone(CloneContext* ctx) const override;
+  const IfStatement* Clone(CloneContext* ctx) const override;
 
   /// The if condition or nullptr if none set
-  Expression* const condition;
+  const Expression* const condition;
 
   /// The if body
-  BlockStatement* const body;
+  const BlockStatement* const body;
 
   /// The else statements
-  ElseStatementList const else_statements;
+  const ElseStatementList else_statements;
 };
 
 }  // namespace ast
diff --git a/src/ast/int_literal.cc b/src/ast/int_literal.cc
index f9659d5..ff164c0 100644
--- a/src/ast/int_literal.cc
+++ b/src/ast/int_literal.cc
@@ -19,8 +19,7 @@
 namespace tint {
 namespace ast {
 
-IntLiteral::IntLiteral(ProgramID pid, const Source& src)
-    : Base(pid, src) {}
+IntLiteral::IntLiteral(ProgramID pid, const Source& src) : Base(pid, src) {}
 
 IntLiteral::~IntLiteral() = default;
 
diff --git a/src/ast/interpolate_decoration.cc b/src/ast/interpolate_decoration.cc
index e3f7fae..44a679d 100644
--- a/src/ast/interpolate_decoration.cc
+++ b/src/ast/interpolate_decoration.cc
@@ -35,7 +35,8 @@
   return "interpolate";
 }
 
-InterpolateDecoration* InterpolateDecoration::Clone(CloneContext* ctx) const {
+const InterpolateDecoration* InterpolateDecoration::Clone(
+    CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<InterpolateDecoration>(src, type, sampling);
diff --git a/src/ast/interpolate_decoration.h b/src/ast/interpolate_decoration.h
index 54883fa..30d173b 100644
--- a/src/ast/interpolate_decoration.h
+++ b/src/ast/interpolate_decoration.h
@@ -51,13 +51,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  InterpolateDecoration* Clone(CloneContext* ctx) const override;
+  const InterpolateDecoration* Clone(CloneContext* ctx) const override;
 
   /// The interpolation type
-  InterpolationType const type;
+  const InterpolationType type;
 
   /// The interpolation sampling
-  InterpolationSampling const sampling;
+  const InterpolationSampling sampling;
 };
 
 /// @param out the std::ostream to write to
diff --git a/src/ast/intrinsic_texture_helper_test.cc b/src/ast/intrinsic_texture_helper_test.cc
index c926f2d..04c3ee0 100644
--- a/src/ast/intrinsic_texture_helper_test.cc
+++ b/src/ast/intrinsic_texture_helper_test.cc
@@ -134,7 +134,7 @@
   return out;
 }
 
-ast::Type* TextureOverloadCase::BuildResultVectorComponentType(
+const ast::Type* TextureOverloadCase::BuildResultVectorComponentType(
     ProgramBuilder* b) const {
   switch (texture_data_type) {
     case ast::intrinsic::test::TextureDataType::kF32:
@@ -149,7 +149,7 @@
   return {};
 }
 
-ast::Variable* TextureOverloadCase::BuildTextureVariable(
+const ast::Variable* TextureOverloadCase::BuildTextureVariable(
     ProgramBuilder* b) const {
   DecorationList decos = {
       b->create<ast::GroupDecoration>(0),
@@ -188,7 +188,7 @@
   return nullptr;
 }
 
-ast::Variable* TextureOverloadCase::BuildSamplerVariable(
+const ast::Variable* TextureOverloadCase::BuildSamplerVariable(
     ProgramBuilder* b) const {
   DecorationList decos = {
       b->create<ast::GroupDecoration>(0),
diff --git a/src/ast/intrinsic_texture_helper_test.h b/src/ast/intrinsic_texture_helper_test.h
index 5f842f1..ee517a1 100644
--- a/src/ast/intrinsic_texture_helper_test.h
+++ b/src/ast/intrinsic_texture_helper_test.h
@@ -205,22 +205,23 @@
 
   /// @param builder the AST builder used for the test
   /// @returns the vector component type of the texture function return value
-  ast::Type* BuildResultVectorComponentType(ProgramBuilder* builder) const;
+  const ast::Type* BuildResultVectorComponentType(
+      ProgramBuilder* builder) const;
   /// @param builder the AST builder used for the test
   /// @returns a variable holding the test texture, automatically registered as
   /// a global variable.
-  ast::Variable* BuildTextureVariable(ProgramBuilder* builder) const;
+  const ast::Variable* BuildTextureVariable(ProgramBuilder* builder) const;
   /// @param builder the AST builder used for the test
   /// @returns a Variable holding the test sampler, automatically registered as
   /// a global variable.
-  ast::Variable* BuildSamplerVariable(ProgramBuilder* builder) const;
+  const ast::Variable* BuildSamplerVariable(ProgramBuilder* builder) const;
 
   /// The enumerator for this overload
-  ValidTextureOverload const overload;
+  const ValidTextureOverload overload;
   /// A human readable description of the overload
   const char* const description;
   /// The texture kind for the texture parameter
-  TextureKind const texture_kind;
+  const TextureKind texture_kind;
   /// The sampler kind for the sampler parameter
   /// Used only when texture_kind is not kStorage
   ast::SamplerKind const sampler_kind = ast::SamplerKind::kSampler;
@@ -233,7 +234,7 @@
   /// The dimensions of the texture parameter
   ast::TextureDimension const texture_dimension;
   /// The data type of the texture parameter
-  TextureDataType const texture_data_type;
+  const TextureDataType texture_data_type;
   /// Name of the function. e.g. `textureSample`, `textureSampleGrad`, etc
   const char* const function;
   /// A function that builds the AST arguments for the overload
diff --git a/src/ast/invariant_decoration.cc b/src/ast/invariant_decoration.cc
index cf00a5b..6ef6dbd 100644
--- a/src/ast/invariant_decoration.cc
+++ b/src/ast/invariant_decoration.cc
@@ -30,7 +30,7 @@
   return "invariant";
 }
 
-InvariantDecoration* InvariantDecoration::Clone(CloneContext* ctx) const {
+const InvariantDecoration* InvariantDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<InvariantDecoration>(src);
diff --git a/src/ast/invariant_decoration.h b/src/ast/invariant_decoration.h
index 5f3216d..77154d1 100644
--- a/src/ast/invariant_decoration.h
+++ b/src/ast/invariant_decoration.h
@@ -38,7 +38,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  InvariantDecoration* Clone(CloneContext* ctx) const override;
+  const InvariantDecoration* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/location_decoration.cc b/src/ast/location_decoration.cc
index 5db7e36..3c386fa 100644
--- a/src/ast/location_decoration.cc
+++ b/src/ast/location_decoration.cc
@@ -34,7 +34,7 @@
   return "location";
 }
 
-LocationDecoration* LocationDecoration::Clone(CloneContext* ctx) const {
+const LocationDecoration* LocationDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<LocationDecoration>(src, value);
diff --git a/src/ast/location_decoration.h b/src/ast/location_decoration.h
index d547874..e498ca5 100644
--- a/src/ast/location_decoration.h
+++ b/src/ast/location_decoration.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  LocationDecoration* Clone(CloneContext* ctx) const override;
+  const LocationDecoration* Clone(CloneContext* ctx) const override;
 
   /// The location value
-  uint32_t const value;
+  const uint32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/loop_statement.cc b/src/ast/loop_statement.cc
index e01392e..84f1249 100644
--- a/src/ast/loop_statement.cc
+++ b/src/ast/loop_statement.cc
@@ -23,8 +23,8 @@
 
 LoopStatement::LoopStatement(ProgramID pid,
                              const Source& src,
-                             BlockStatement* b,
-                             BlockStatement* cont)
+                             const BlockStatement* b,
+                             const BlockStatement* cont)
     : Base(pid, src), body(b), continuing(cont) {
   TINT_ASSERT(AST, body);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
@@ -35,7 +35,7 @@
 
 LoopStatement::~LoopStatement() = default;
 
-LoopStatement* LoopStatement::Clone(CloneContext* ctx) const {
+const LoopStatement* LoopStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* b = ctx->Clone(body);
diff --git a/src/ast/loop_statement.h b/src/ast/loop_statement.h
index 5f0ba50..90a6ddf 100644
--- a/src/ast/loop_statement.h
+++ b/src/ast/loop_statement.h
@@ -30,8 +30,8 @@
   /// @param continuing the continuing statements
   LoopStatement(ProgramID program_id,
                 const Source& source,
-                BlockStatement* body,
-                BlockStatement* continuing);
+                const BlockStatement* body,
+                const BlockStatement* continuing);
   /// Move constructor
   LoopStatement(LoopStatement&&);
   ~LoopStatement() override;
@@ -40,13 +40,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  LoopStatement* Clone(CloneContext* ctx) const override;
+  const LoopStatement* Clone(CloneContext* ctx) const override;
 
   /// The loop body
-  BlockStatement* const body;
+  const BlockStatement* const body;
 
   /// The continuing statements
-  BlockStatement* const continuing;
+  const BlockStatement* const continuing;
 };
 
 }  // namespace ast
diff --git a/src/ast/matrix.cc b/src/ast/matrix.cc
index 2a5801a..2530e80 100644
--- a/src/ast/matrix.cc
+++ b/src/ast/matrix.cc
@@ -23,7 +23,7 @@
 
 Matrix::Matrix(ProgramID pid,
                const Source& src,
-               Type* subtype,
+               const Type* subtype,
                uint32_t r,
                uint32_t c)
     : Base(pid, src), type(subtype), rows(r), columns(c) {
@@ -45,7 +45,7 @@
   return out.str();
 }
 
-Matrix* Matrix::Clone(CloneContext* ctx) const {
+const Matrix* Matrix::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/matrix.h b/src/ast/matrix.h
index 434ee3f..023adc6 100644
--- a/src/ast/matrix.h
+++ b/src/ast/matrix.h
@@ -33,7 +33,7 @@
   /// @param columns the number of columns in the matrix
   Matrix(ProgramID pid,
          const Source& src,
-         Type* subtype,
+         const Type* subtype,
          uint32_t rows,
          uint32_t columns);
   /// Move constructor
@@ -48,16 +48,16 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Matrix* Clone(CloneContext* ctx) const override;
+  const Matrix* Clone(CloneContext* ctx) const override;
 
   /// The type of the matrix
-  Type* const type;
+  const Type* const type;
 
   /// The number of rows in the matrix
-  uint32_t const rows;
+  const uint32_t rows;
 
   /// The number of columns in the matrix
-  uint32_t const columns;
+  const uint32_t columns;
 };
 
 }  // namespace ast
diff --git a/src/ast/member_accessor_expression.cc b/src/ast/member_accessor_expression.cc
index 4dc3bf8..12b2a3d 100644
--- a/src/ast/member_accessor_expression.cc
+++ b/src/ast/member_accessor_expression.cc
@@ -21,10 +21,11 @@
 namespace tint {
 namespace ast {
 
-MemberAccessorExpression::MemberAccessorExpression(ProgramID pid,
-                                                   const Source& src,
-                                                   Expression* str,
-                                                   IdentifierExpression* mem)
+MemberAccessorExpression::MemberAccessorExpression(
+    ProgramID pid,
+    const Source& src,
+    const Expression* str,
+    const IdentifierExpression* mem)
     : Base(pid, src), structure(str), member(mem) {
   TINT_ASSERT(AST, structure);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, structure, program_id);
@@ -37,7 +38,7 @@
 
 MemberAccessorExpression::~MemberAccessorExpression() = default;
 
-MemberAccessorExpression* MemberAccessorExpression::Clone(
+const MemberAccessorExpression* MemberAccessorExpression::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/member_accessor_expression.h b/src/ast/member_accessor_expression.h
index 9716e2e..58f93d0 100644
--- a/src/ast/member_accessor_expression.h
+++ b/src/ast/member_accessor_expression.h
@@ -31,8 +31,8 @@
   /// @param member the member
   MemberAccessorExpression(ProgramID program_id,
                            const Source& source,
-                           Expression* structure,
-                           IdentifierExpression* member);
+                           const Expression* structure,
+                           const IdentifierExpression* member);
   /// Move constructor
   MemberAccessorExpression(MemberAccessorExpression&&);
   ~MemberAccessorExpression() override;
@@ -41,13 +41,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  MemberAccessorExpression* Clone(CloneContext* ctx) const override;
+  const MemberAccessorExpression* Clone(CloneContext* ctx) const override;
 
   /// The structure
-  Expression* const structure;
+  const Expression* const structure;
 
   /// The member expression
-  IdentifierExpression* const member;
+  const IdentifierExpression* const member;
 };
 
 }  // namespace ast
diff --git a/src/ast/module.cc b/src/ast/module.cc
index 33ccd05..24999d2 100644
--- a/src/ast/module.cc
+++ b/src/ast/module.cc
@@ -28,7 +28,7 @@
 
 Module::Module(ProgramID pid,
                const Source& src,
-               std::vector<ast::Node*> global_decls)
+               std::vector<const ast::Node*> global_decls)
     : Base(pid, src), global_declarations_(std::move(global_decls)) {
   for (auto* decl : global_declarations_) {
     if (decl == nullptr) {
@@ -59,28 +59,28 @@
   return nullptr;
 }
 
-void Module::AddGlobalVariable(ast::Variable* var) {
+void Module::AddGlobalVariable(const ast::Variable* var) {
   TINT_ASSERT(AST, var);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, var, program_id);
   global_variables_.push_back(var);
   global_declarations_.push_back(var);
 }
 
-void Module::AddTypeDecl(ast::TypeDecl* type) {
+void Module::AddTypeDecl(const ast::TypeDecl* type) {
   TINT_ASSERT(AST, type);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, type, program_id);
   type_decls_.push_back(type);
   global_declarations_.push_back(type);
 }
 
-void Module::AddFunction(ast::Function* func) {
+void Module::AddFunction(const ast::Function* func) {
   TINT_ASSERT(AST, func);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, func, program_id);
   functions_.push_back(func);
   global_declarations_.push_back(func);
 }
 
-Module* Module::Clone(CloneContext* ctx) const {
+const Module* Module::Clone(CloneContext* ctx) const {
   auto* out = ctx->dst->create<Module>();
   out->Copy(ctx, this);
   return out;
diff --git a/src/ast/module.h b/src/ast/module.h
index a4e392c..012a73d 100644
--- a/src/ast/module.h
+++ b/src/ast/module.h
@@ -42,23 +42,23 @@
   /// the order they were declared in the source program
   Module(ProgramID pid,
          const Source& src,
-         std::vector<ast::Node*> global_decls);
+         std::vector<const Node*> global_decls);
 
   /// Destructor
   ~Module() override;
 
   /// @returns the ordered global declarations for the translation unit
-  const std::vector<ast::Node*>& GlobalDeclarations() const {
+  const std::vector<const Node*>& GlobalDeclarations() const {
     return global_declarations_;
   }
 
   /// Add a global variable to the Builder
   /// @param var the variable to add
-  void AddGlobalVariable(ast::Variable* var);
+  void AddGlobalVariable(const Variable* var);
 
   /// @returns true if the module has the global declaration `decl`
   /// @param decl the declaration to check
-  bool HasGlobalDeclaration(ast::Node* decl) const {
+  bool HasGlobalDeclaration(Node* decl) const {
     for (auto* d : global_declarations_) {
       if (d == decl) {
         return true;
@@ -75,18 +75,18 @@
 
   /// Adds a type declaration to the Builder.
   /// @param decl the type declaration to add
-  void AddTypeDecl(ast::TypeDecl* decl);
+  void AddTypeDecl(const TypeDecl* decl);
 
   /// @returns the TypeDecl registered as a TypeDecl()
   /// @param name the name of the type to search for
-  const ast::TypeDecl* LookupType(Symbol name) const;
+  const TypeDecl* LookupType(Symbol name) const;
 
   /// @returns the declared types in the translation unit
-  const std::vector<ast::TypeDecl*>& TypeDecls() const { return type_decls_; }
+  const std::vector<const TypeDecl*>& TypeDecls() const { return type_decls_; }
 
   /// Add a function to the Builder
   /// @param func the function to add
-  void AddFunction(ast::Function* func);
+  void AddFunction(const Function* func);
 
   /// @returns the functions declared in the translation unit
   const FunctionList& Functions() const { return functions_; }
@@ -95,7 +95,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  Module* Clone(CloneContext* ctx) const override;
+  const Module* Clone(CloneContext* ctx) const override;
 
   /// Copy copies the content of the Module src into this module.
   /// @param ctx the clone context
@@ -103,8 +103,8 @@
   void Copy(CloneContext* ctx, const Module* src);
 
  private:
-  std::vector<ast::Node*> global_declarations_;
-  std::vector<ast::TypeDecl*> type_decls_;
+  std::vector<const Node*> global_declarations_;
+  std::vector<const TypeDecl*> type_decls_;
   FunctionList functions_;
   VariableList global_variables_;
 };
diff --git a/src/ast/module_test.cc b/src/ast/module_test.cc
index e36a2b8..795988f 100644
--- a/src/ast/module_test.cc
+++ b/src/ast/module_test.cc
@@ -106,15 +106,15 @@
   // declaration that triggered the ReplaceAll().
   ProgramBuilder cloned;
   CloneContext ctx(&cloned, &p);
-  ctx.ReplaceAll([&](ast::Function*) -> ast::Function* {
+  ctx.ReplaceAll([&](const ast::Function*) -> const ast::Function* {
     ctx.dst->Alias("inserted_before_F", cloned.ty.u32());
     return nullptr;
   });
-  ctx.ReplaceAll([&](ast::Alias*) -> ast::Alias* {
+  ctx.ReplaceAll([&](const ast::Alias*) -> const ast::Alias* {
     ctx.dst->Alias("inserted_before_A", cloned.ty.u32());
     return nullptr;
   });
-  ctx.ReplaceAll([&](ast::Variable*) -> ast::Variable* {
+  ctx.ReplaceAll([&](const ast::Variable*) -> const ast::Variable* {
     ctx.dst->Alias("inserted_before_V", cloned.ty.u32());
     return nullptr;
   });
diff --git a/src/ast/multisampled_texture.cc b/src/ast/multisampled_texture.cc
index bd8127e..b1aa36c 100644
--- a/src/ast/multisampled_texture.cc
+++ b/src/ast/multisampled_texture.cc
@@ -24,7 +24,7 @@
 MultisampledTexture::MultisampledTexture(ProgramID pid,
                                          const Source& src,
                                          TextureDimension d,
-                                         Type* ty)
+                                         const Type* ty)
     : Base(pid, src, d), type(ty) {
   TINT_ASSERT(AST, type);
 }
@@ -41,7 +41,7 @@
   return out.str();
 }
 
-MultisampledTexture* MultisampledTexture::Clone(CloneContext* ctx) const {
+const MultisampledTexture* MultisampledTexture::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/multisampled_texture.h b/src/ast/multisampled_texture.h
index c1cef57..8cf65ab 100644
--- a/src/ast/multisampled_texture.h
+++ b/src/ast/multisampled_texture.h
@@ -33,7 +33,7 @@
   MultisampledTexture(ProgramID pid,
                       const Source& src,
                       TextureDimension dim,
-                      Type* type);
+                      const Type* type);
   /// Move constructor
   MultisampledTexture(MultisampledTexture&&);
   ~MultisampledTexture() override;
@@ -46,10 +46,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  MultisampledTexture* Clone(CloneContext* ctx) const override;
+  const MultisampledTexture* Clone(CloneContext* ctx) const override;
 
   /// The subtype of the multisampled texture
-  Type* const type;
+  const Type* const type;
 };
 
 }  // namespace ast
diff --git a/src/ast/node.h b/src/ast/node.h
index 242f7a3..fc0bc7f 100644
--- a/src/ast/node.h
+++ b/src/ast/node.h
@@ -38,10 +38,10 @@
   ~Node() override;
 
   /// The identifier of the program that owns this node
-  ProgramID const program_id;
+  const ProgramID program_id;
 
   /// The node source data
-  Source const source;
+  const Source source;
 
  protected:
   /// Create a new node
diff --git a/src/ast/override_decoration.cc b/src/ast/override_decoration.cc
index 6227d9c..6df009f 100644
--- a/src/ast/override_decoration.cc
+++ b/src/ast/override_decoration.cc
@@ -37,7 +37,7 @@
   return "override";
 }
 
-OverrideDecoration* OverrideDecoration::Clone(CloneContext* ctx) const {
+const OverrideDecoration* OverrideDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   if (has_value) {
diff --git a/src/ast/override_decoration.h b/src/ast/override_decoration.h
index 714498a..be17198 100644
--- a/src/ast/override_decoration.h
+++ b/src/ast/override_decoration.h
@@ -43,13 +43,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  OverrideDecoration* Clone(CloneContext* ctx) const override;
+  const OverrideDecoration* Clone(CloneContext* ctx) const override;
 
   /// True if an override id was specified
-  bool const has_value;
+  const bool has_value;
 
   /// The override id value
-  uint32_t const value;
+  const uint32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/pointer.cc b/src/ast/pointer.cc
index 4ae782d..2ba6d1b 100644
--- a/src/ast/pointer.cc
+++ b/src/ast/pointer.cc
@@ -23,7 +23,7 @@
 
 Pointer::Pointer(ProgramID pid,
                  const Source& src,
-                 Type* const subtype,
+                 const Type* const subtype,
                  ast::StorageClass sc,
                  ast::Access ac)
     : Base(pid, src), type(subtype), storage_class(sc), access(ac) {}
@@ -46,7 +46,7 @@
 
 Pointer::~Pointer() = default;
 
-Pointer* Pointer::Clone(CloneContext* ctx) const {
+const Pointer* Pointer::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/pointer.h b/src/ast/pointer.h
index b621c95..1cba57b 100644
--- a/src/ast/pointer.h
+++ b/src/ast/pointer.h
@@ -35,7 +35,7 @@
   /// @param access the access control of the pointer
   Pointer(ProgramID pid,
           const Source& src,
-          Type* const subtype,
+          const Type* const subtype,
           ast::StorageClass storage_class,
           ast::Access access);
   /// Move constructor
@@ -50,10 +50,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Pointer* Clone(CloneContext* ctx) const override;
+  const Pointer* Clone(CloneContext* ctx) const override;
 
   /// The pointee type
-  Type* const type;
+  const Type* const type;
 
   /// The storage class of the pointer
   ast::StorageClass const storage_class;
diff --git a/src/ast/return_statement.cc b/src/ast/return_statement.cc
index e48be37..fe0c946 100644
--- a/src/ast/return_statement.cc
+++ b/src/ast/return_statement.cc
@@ -26,7 +26,7 @@
 
 ReturnStatement::ReturnStatement(ProgramID pid,
                                  const Source& src,
-                                 Expression* val)
+                                 const Expression* val)
     : Base(pid, src), value(val) {
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, value, program_id);
 }
@@ -35,7 +35,7 @@
 
 ReturnStatement::~ReturnStatement() = default;
 
-ReturnStatement* ReturnStatement::Clone(CloneContext* ctx) const {
+const ReturnStatement* ReturnStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ret = ctx->Clone(value);
diff --git a/src/ast/return_statement.h b/src/ast/return_statement.h
index 79e1b35..e00f8ba 100644
--- a/src/ast/return_statement.h
+++ b/src/ast/return_statement.h
@@ -33,7 +33,7 @@
   /// @param pid the identifier of the program that owns this node
   /// @param src the source of this node
   /// @param value the return value
-  ReturnStatement(ProgramID pid, const Source& src, Expression* value);
+  ReturnStatement(ProgramID pid, const Source& src, const Expression* value);
   /// Move constructor
   ReturnStatement(ReturnStatement&&);
   ~ReturnStatement() override;
@@ -42,10 +42,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ReturnStatement* Clone(CloneContext* ctx) const override;
+  const ReturnStatement* Clone(CloneContext* ctx) const override;
 
   /// The value returned. May be null.
-  Expression* const value;
+  const Expression* const value;
 };
 
 }  // namespace ast
diff --git a/src/ast/sampled_texture.cc b/src/ast/sampled_texture.cc
index 11e0d17..da010a0 100644
--- a/src/ast/sampled_texture.cc
+++ b/src/ast/sampled_texture.cc
@@ -24,8 +24,8 @@
 SampledTexture::SampledTexture(ProgramID pid,
                                const Source& src,
                                TextureDimension d,
-                               Type const* ty)
-    : Base(pid, src, d), type(const_cast<Type*>(ty)) {
+                               const Type* ty)
+    : Base(pid, src, d), type(ty) {
   TINT_ASSERT(AST, type);
 }
 
@@ -39,7 +39,7 @@
   return out.str();
 }
 
-SampledTexture* SampledTexture::Clone(CloneContext* ctx) const {
+const SampledTexture* SampledTexture::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/sampled_texture.h b/src/ast/sampled_texture.h
index 0530767..4cae225 100644
--- a/src/ast/sampled_texture.h
+++ b/src/ast/sampled_texture.h
@@ -33,7 +33,7 @@
   SampledTexture(ProgramID pid,
                  const Source& src,
                  TextureDimension dim,
-                 Type const* type);
+                 const Type* type);
   /// Move constructor
   SampledTexture(SampledTexture&&);
   ~SampledTexture() override;
@@ -46,10 +46,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  SampledTexture* Clone(CloneContext* ctx) const override;
+  const SampledTexture* Clone(CloneContext* ctx) const override;
 
   /// The subtype of the sampled texture
-  Type* const type;
+  const Type* const type;
 };
 
 }  // namespace ast
diff --git a/src/ast/sampler.cc b/src/ast/sampler.cc
index e2387bb..a0b9261 100644
--- a/src/ast/sampler.cc
+++ b/src/ast/sampler.cc
@@ -44,7 +44,7 @@
   return kind == SamplerKind::kSampler ? "sampler" : "sampler_comparison";
 }
 
-Sampler* Sampler::Clone(CloneContext* ctx) const {
+const Sampler* Sampler::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<Sampler>(src, kind);
 }
diff --git a/src/ast/sampler.h b/src/ast/sampler.h
index 64bf4ea..a2eacc4 100644
--- a/src/ast/sampler.h
+++ b/src/ast/sampler.h
@@ -58,10 +58,10 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Sampler* Clone(CloneContext* ctx) const override;
+  const Sampler* Clone(CloneContext* ctx) const override;
 
   /// The sampler type
-  SamplerKind const kind;
+  const SamplerKind kind;
 };
 
 }  // namespace ast
diff --git a/src/ast/scalar_constructor_expression.cc b/src/ast/scalar_constructor_expression.cc
index 74e6e78..8085188 100644
--- a/src/ast/scalar_constructor_expression.cc
+++ b/src/ast/scalar_constructor_expression.cc
@@ -23,7 +23,7 @@
 
 ScalarConstructorExpression::ScalarConstructorExpression(ProgramID pid,
                                                          const Source& src,
-                                                         Literal* lit)
+                                                         const Literal* lit)
     : Base(pid, src), literal(lit) {
   TINT_ASSERT(AST, literal);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, literal, program_id);
@@ -34,7 +34,7 @@
 
 ScalarConstructorExpression::~ScalarConstructorExpression() = default;
 
-ScalarConstructorExpression* ScalarConstructorExpression::Clone(
+const ScalarConstructorExpression* ScalarConstructorExpression::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/scalar_constructor_expression.h b/src/ast/scalar_constructor_expression.h
index 47412ac..b80650e 100644
--- a/src/ast/scalar_constructor_expression.h
+++ b/src/ast/scalar_constructor_expression.h
@@ -31,7 +31,7 @@
   /// @param literal the const literal
   ScalarConstructorExpression(ProgramID pid,
                               const Source& src,
-                              Literal* literal);
+                              const Literal* literal);
   /// Move constructor
   ScalarConstructorExpression(ScalarConstructorExpression&&);
   ~ScalarConstructorExpression() override;
@@ -40,10 +40,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  ScalarConstructorExpression* Clone(CloneContext* ctx) const override;
+  const ScalarConstructorExpression* Clone(CloneContext* ctx) const override;
 
   /// The literal value
-  Literal* const literal;
+  const Literal* const literal;
 };
 
 }  // namespace ast
diff --git a/src/ast/sint_literal.cc b/src/ast/sint_literal.cc
index 2e6f232..99f291d 100644
--- a/src/ast/sint_literal.cc
+++ b/src/ast/sint_literal.cc
@@ -30,7 +30,7 @@
   return static_cast<uint32_t>(value);
 }
 
-SintLiteral* SintLiteral::Clone(CloneContext* ctx) const {
+const SintLiteral* SintLiteral::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<SintLiteral>(src, value);
diff --git a/src/ast/sint_literal.h b/src/ast/sint_literal.h
index 75f9aa3..ec807d6 100644
--- a/src/ast/sint_literal.h
+++ b/src/ast/sint_literal.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  SintLiteral* Clone(CloneContext* ctx) const override;
+  const SintLiteral* Clone(CloneContext* ctx) const override;
 
   /// The int literal value
-  int32_t const value;
+  const int32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/stage_decoration.cc b/src/ast/stage_decoration.cc
index 3fd66ee..eafc1bc 100644
--- a/src/ast/stage_decoration.cc
+++ b/src/ast/stage_decoration.cc
@@ -34,7 +34,7 @@
   return "stage";
 }
 
-StageDecoration* StageDecoration::Clone(CloneContext* ctx) const {
+const StageDecoration* StageDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<StageDecoration>(src, stage);
diff --git a/src/ast/stage_decoration.h b/src/ast/stage_decoration.h
index acd0359..ac90690 100644
--- a/src/ast/stage_decoration.h
+++ b/src/ast/stage_decoration.h
@@ -42,10 +42,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StageDecoration* Clone(CloneContext* ctx) const override;
+  const StageDecoration* Clone(CloneContext* ctx) const override;
 
   /// The pipeline stage
-  PipelineStage const stage;
+  const PipelineStage stage;
 };
 
 }  // namespace ast
diff --git a/src/ast/statement.h b/src/ast/statement.h
index bc31e37..567545b 100644
--- a/src/ast/statement.h
+++ b/src/ast/statement.h
@@ -40,7 +40,7 @@
 };
 
 /// A list of statements
-using StatementList = std::vector<Statement*>;
+using StatementList = std::vector<const Statement*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/storage_texture.cc b/src/ast/storage_texture.cc
index 6c8783f..5b8fdcd 100644
--- a/src/ast/storage_texture.cc
+++ b/src/ast/storage_texture.cc
@@ -144,7 +144,7 @@
                                const Source& src,
                                TextureDimension d,
                                ImageFormat fmt,
-                               Type* subtype,
+                               const Type* subtype,
                                Access ac)
     : Base(pid, src, d), format(fmt), type(subtype), access(ac) {}
 
@@ -158,7 +158,7 @@
   return out.str();
 }
 
-StorageTexture* StorageTexture::Clone(CloneContext* ctx) const {
+const StorageTexture* StorageTexture::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/storage_texture.h b/src/ast/storage_texture.h
index d09bb1a..5e56aba 100644
--- a/src/ast/storage_texture.h
+++ b/src/ast/storage_texture.h
@@ -84,7 +84,7 @@
                  const Source& src,
                  TextureDimension dim,
                  ImageFormat format,
-                 Type* subtype,
+                 const Type* subtype,
                  Access access_control);
 
   /// Move constructor
@@ -99,7 +99,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  StorageTexture* Clone(CloneContext* ctx) const override;
+  const StorageTexture* Clone(CloneContext* ctx) const override;
 
   /// @param format the storage texture image format
   /// @param builder the ProgramBuilder used to build the returned type
@@ -107,13 +107,13 @@
   static Type* SubtypeFor(ImageFormat format, ProgramBuilder& builder);
 
   /// The image format
-  ImageFormat const format;
+  const ImageFormat format;
 
   /// The storage subtype
-  Type* const type;
+  const Type* const type;
 
   /// The access control
-  Access const access;
+  const Access access;
 };
 
 }  // namespace ast
diff --git a/src/ast/stride_decoration.cc b/src/ast/stride_decoration.cc
index 08d7ee9..9aa8bb1 100644
--- a/src/ast/stride_decoration.cc
+++ b/src/ast/stride_decoration.cc
@@ -32,7 +32,7 @@
   return "stride";
 }
 
-StrideDecoration* StrideDecoration::Clone(CloneContext* ctx) const {
+const StrideDecoration* StrideDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<StrideDecoration>(src, stride);
diff --git a/src/ast/stride_decoration.h b/src/ast/stride_decoration.h
index c9cf3e3..5053c3a 100644
--- a/src/ast/stride_decoration.h
+++ b/src/ast/stride_decoration.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StrideDecoration* Clone(CloneContext* ctx) const override;
+  const StrideDecoration* Clone(CloneContext* ctx) const override;
 
   /// The stride value
-  uint32_t const stride;
+  const uint32_t stride;
 };
 
 }  // namespace ast
diff --git a/src/ast/struct.cc b/src/ast/struct.cc
index cb91067..7868caa 100644
--- a/src/ast/struct.cc
+++ b/src/ast/struct.cc
@@ -48,7 +48,7 @@
   return HasDecoration<StructBlockDecoration>(decorations);
 }
 
-Struct* Struct::Clone(CloneContext* ctx) const {
+const Struct* Struct::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto n = ctx->Clone(name);
diff --git a/src/ast/struct.h b/src/ast/struct.h
index 6235b2e..f3a08c9 100644
--- a/src/ast/struct.h
+++ b/src/ast/struct.h
@@ -51,13 +51,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  Struct* Clone(CloneContext* ctx) const override;
+  const Struct* Clone(CloneContext* ctx) const override;
 
   /// The members
-  StructMemberList const members;
+  const StructMemberList members;
 
   /// The struct decorations
-  DecorationList const decorations;
+  const DecorationList decorations;
 };
 
 }  // namespace ast
diff --git a/src/ast/struct_block_decoration.cc b/src/ast/struct_block_decoration.cc
index e41b34e..73f2530 100644
--- a/src/ast/struct_block_decoration.cc
+++ b/src/ast/struct_block_decoration.cc
@@ -32,7 +32,8 @@
   return "block";
 }
 
-StructBlockDecoration* StructBlockDecoration::Clone(CloneContext* ctx) const {
+const StructBlockDecoration* StructBlockDecoration::Clone(
+    CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<StructBlockDecoration>(src);
diff --git a/src/ast/struct_block_decoration.h b/src/ast/struct_block_decoration.h
index 8a3cbff..481591f 100644
--- a/src/ast/struct_block_decoration.h
+++ b/src/ast/struct_block_decoration.h
@@ -40,7 +40,7 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StructBlockDecoration* Clone(CloneContext* ctx) const override;
+  const StructBlockDecoration* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/struct_member.cc b/src/ast/struct_member.cc
index 73a3460..c33240b 100644
--- a/src/ast/struct_member.cc
+++ b/src/ast/struct_member.cc
@@ -24,7 +24,7 @@
 StructMember::StructMember(ProgramID pid,
                            const Source& src,
                            const Symbol& sym,
-                           ast::Type* ty,
+                           const ast::Type* ty,
                            DecorationList decos)
     : Base(pid, src), symbol(sym), type(ty), decorations(std::move(decos)) {
   TINT_ASSERT(AST, type);
@@ -40,7 +40,7 @@
 
 StructMember::~StructMember() = default;
 
-StructMember* StructMember::Clone(CloneContext* ctx) const {
+const StructMember* StructMember::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto sym = ctx->Clone(symbol);
diff --git a/src/ast/struct_member.h b/src/ast/struct_member.h
index ac3725b..eaad548 100644
--- a/src/ast/struct_member.h
+++ b/src/ast/struct_member.h
@@ -38,7 +38,7 @@
   StructMember(ProgramID pid,
                const Source& src,
                const Symbol& sym,
-               ast::Type* type,
+               const ast::Type* type,
                DecorationList decorations);
   /// Move constructor
   StructMember(StructMember&&);
@@ -49,20 +49,20 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StructMember* Clone(CloneContext* ctx) const override;
+  const StructMember* Clone(CloneContext* ctx) const override;
 
   /// The symbol
-  Symbol const symbol;
+  const Symbol symbol;
 
   /// The type
-  ast::Type* const type;
+  const ast::Type* const type;
 
   /// The decorations
-  DecorationList const decorations;
+  const DecorationList decorations;
 };
 
 /// A list of struct members
-using StructMemberList = std::vector<StructMember*>;
+using StructMemberList = std::vector<const StructMember*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/struct_member_align_decoration.cc b/src/ast/struct_member_align_decoration.cc
index e2fb028..fc84a45 100644
--- a/src/ast/struct_member_align_decoration.cc
+++ b/src/ast/struct_member_align_decoration.cc
@@ -35,7 +35,7 @@
   return "align";
 }
 
-StructMemberAlignDecoration* StructMemberAlignDecoration::Clone(
+const StructMemberAlignDecoration* StructMemberAlignDecoration::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/struct_member_align_decoration.h b/src/ast/struct_member_align_decoration.h
index 64c4dfa..13febe9 100644
--- a/src/ast/struct_member_align_decoration.h
+++ b/src/ast/struct_member_align_decoration.h
@@ -41,10 +41,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StructMemberAlignDecoration* Clone(CloneContext* ctx) const override;
+  const StructMemberAlignDecoration* Clone(CloneContext* ctx) const override;
 
   /// The align value
-  uint32_t const align;
+  const uint32_t align;
 };
 
 }  // namespace ast
diff --git a/src/ast/struct_member_offset_decoration.cc b/src/ast/struct_member_offset_decoration.cc
index a482fbd..ac73b24 100644
--- a/src/ast/struct_member_offset_decoration.cc
+++ b/src/ast/struct_member_offset_decoration.cc
@@ -34,7 +34,7 @@
   return "offset";
 }
 
-StructMemberOffsetDecoration* StructMemberOffsetDecoration::Clone(
+const StructMemberOffsetDecoration* StructMemberOffsetDecoration::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/struct_member_offset_decoration.h b/src/ast/struct_member_offset_decoration.h
index 753c647..8c6f3ac 100644
--- a/src/ast/struct_member_offset_decoration.h
+++ b/src/ast/struct_member_offset_decoration.h
@@ -51,10 +51,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StructMemberOffsetDecoration* Clone(CloneContext* ctx) const override;
+  const StructMemberOffsetDecoration* Clone(CloneContext* ctx) const override;
 
   /// The offset value
-  uint32_t const offset;
+  const uint32_t offset;
 };
 
 }  // namespace ast
diff --git a/src/ast/struct_member_size_decoration.cc b/src/ast/struct_member_size_decoration.cc
index 0155c41..b54c8de 100644
--- a/src/ast/struct_member_size_decoration.cc
+++ b/src/ast/struct_member_size_decoration.cc
@@ -35,7 +35,7 @@
   return "size";
 }
 
-StructMemberSizeDecoration* StructMemberSizeDecoration::Clone(
+const StructMemberSizeDecoration* StructMemberSizeDecoration::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/struct_member_size_decoration.h b/src/ast/struct_member_size_decoration.h
index 50bbed0..1ca7d77 100644
--- a/src/ast/struct_member_size_decoration.h
+++ b/src/ast/struct_member_size_decoration.h
@@ -41,10 +41,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  StructMemberSizeDecoration* Clone(CloneContext* ctx) const override;
+  const StructMemberSizeDecoration* Clone(CloneContext* ctx) const override;
 
   /// The size value
-  uint32_t const size;
+  const uint32_t size;
 };
 
 }  // namespace ast
diff --git a/src/ast/switch_statement.cc b/src/ast/switch_statement.cc
index 2013fcc..eeb74bf 100644
--- a/src/ast/switch_statement.cc
+++ b/src/ast/switch_statement.cc
@@ -23,7 +23,7 @@
 
 SwitchStatement::SwitchStatement(ProgramID pid,
                                  const Source& src,
-                                 Expression* cond,
+                                 const Expression* cond,
                                  CaseStatementList b)
     : Base(pid, src), condition(cond), body(b) {
   TINT_ASSERT(AST, condition);
@@ -38,7 +38,7 @@
 
 SwitchStatement::~SwitchStatement() = default;
 
-SwitchStatement* SwitchStatement::Clone(CloneContext* ctx) const {
+const SwitchStatement* SwitchStatement::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* cond = ctx->Clone(condition);
diff --git a/src/ast/switch_statement.h b/src/ast/switch_statement.h
index 8f9a8e7..711e553 100644
--- a/src/ast/switch_statement.h
+++ b/src/ast/switch_statement.h
@@ -31,7 +31,7 @@
   /// @param body the switch body
   SwitchStatement(ProgramID pid,
                   const Source& src,
-                  Expression* condition,
+                  const Expression* condition,
                   CaseStatementList body);
   /// Move constructor
   SwitchStatement(SwitchStatement&&);
@@ -44,13 +44,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  SwitchStatement* Clone(CloneContext* ctx) const override;
+  const SwitchStatement* Clone(CloneContext* ctx) const override;
 
   /// The switch condition or nullptr if none set
-  Expression* const condition;
+  const Expression* const condition;
 
   /// The Switch body
-  CaseStatementList const body;
+  const CaseStatementList body;
   SwitchStatement(const SwitchStatement&) = delete;
 };
 
diff --git a/src/ast/texture.h b/src/ast/texture.h
index 769d1eb..e3383a7 100644
--- a/src/ast/texture.h
+++ b/src/ast/texture.h
@@ -74,7 +74,7 @@
   ~Texture() override;
 
   /// The texture dimension
-  TextureDimension const dim;
+  const TextureDimension dim;
 };
 
 }  // namespace ast
diff --git a/src/ast/type_constructor_expression.cc b/src/ast/type_constructor_expression.cc
index bfae88b..2745f2e 100644
--- a/src/ast/type_constructor_expression.cc
+++ b/src/ast/type_constructor_expression.cc
@@ -23,7 +23,7 @@
 
 TypeConstructorExpression::TypeConstructorExpression(ProgramID pid,
                                                      const Source& src,
-                                                     ast::Type* ty,
+                                                     const ast::Type* ty,
                                                      ExpressionList vals)
     : Base(pid, src), type(ty), values(std::move(vals)) {
   TINT_ASSERT(AST, type);
@@ -38,7 +38,7 @@
 
 TypeConstructorExpression::~TypeConstructorExpression() = default;
 
-TypeConstructorExpression* TypeConstructorExpression::Clone(
+const TypeConstructorExpression* TypeConstructorExpression::Clone(
     CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
diff --git a/src/ast/type_constructor_expression.h b/src/ast/type_constructor_expression.h
index b61d8dd..94095e1 100644
--- a/src/ast/type_constructor_expression.h
+++ b/src/ast/type_constructor_expression.h
@@ -36,7 +36,7 @@
   /// @param values the constructor values
   TypeConstructorExpression(ProgramID pid,
                             const Source& src,
-                            ast::Type* type,
+                            const ast::Type* type,
                             ExpressionList values);
   /// Move constructor
   TypeConstructorExpression(TypeConstructorExpression&&);
@@ -46,13 +46,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  TypeConstructorExpression* Clone(CloneContext* ctx) const override;
+  const TypeConstructorExpression* Clone(CloneContext* ctx) const override;
 
   /// The type
-  ast::Type* const type;
+  const ast::Type* const type;
 
   /// The values
-  ExpressionList const values;
+  const ExpressionList values;
 };
 
 }  // namespace ast
diff --git a/src/ast/type_decl.h b/src/ast/type_decl.h
index 2ad75c3..d0d49e4 100644
--- a/src/ast/type_decl.h
+++ b/src/ast/type_decl.h
@@ -36,7 +36,7 @@
   ~TypeDecl() override;
 
   /// The name of the type declaration
-  Symbol const name;
+  const Symbol name;
 };
 
 }  // namespace ast
diff --git a/src/ast/type_name.cc b/src/ast/type_name.cc
index 3ad3236..66da084 100644
--- a/src/ast/type_name.cc
+++ b/src/ast/type_name.cc
@@ -32,7 +32,7 @@
   return symbols.NameFor(name);
 }
 
-TypeName* TypeName::Clone(CloneContext* ctx) const {
+const TypeName* TypeName::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   auto n = ctx->Clone(name);
   return ctx->dst->create<TypeName>(src, n);
diff --git a/src/ast/type_name.h b/src/ast/type_name.h
index 867bc96..5e177ab 100644
--- a/src/ast/type_name.h
+++ b/src/ast/type_name.h
@@ -35,7 +35,6 @@
   /// Destructor
   ~TypeName() override;
 
-
   /// @param symbols the program's symbol table
   /// @returns the name for this type that closely resembles how it would be
   /// declared in WGSL.
@@ -44,7 +43,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  TypeName* Clone(CloneContext* ctx) const override;
+  const TypeName* Clone(CloneContext* ctx) const override;
 
   /// The type name
   Symbol name;
diff --git a/src/ast/u32.cc b/src/ast/u32.cc
index 9337861..efe3624 100644
--- a/src/ast/u32.cc
+++ b/src/ast/u32.cc
@@ -31,7 +31,7 @@
   return "u32";
 }
 
-U32* U32::Clone(CloneContext* ctx) const {
+const U32* U32::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<U32>(src);
 }
diff --git a/src/ast/u32.h b/src/ast/u32.h
index 6671429..5d7ba44 100644
--- a/src/ast/u32.h
+++ b/src/ast/u32.h
@@ -41,7 +41,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  U32* Clone(CloneContext* ctx) const override;
+  const U32* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/uint_literal.cc b/src/ast/uint_literal.cc
index b25db81..27ebda0 100644
--- a/src/ast/uint_literal.cc
+++ b/src/ast/uint_literal.cc
@@ -30,7 +30,7 @@
   return value;
 }
 
-UintLiteral* UintLiteral::Clone(CloneContext* ctx) const {
+const UintLiteral* UintLiteral::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   return ctx->dst->create<UintLiteral>(src, value);
diff --git a/src/ast/uint_literal.h b/src/ast/uint_literal.h
index 3a2df62..3429758 100644
--- a/src/ast/uint_literal.h
+++ b/src/ast/uint_literal.h
@@ -39,10 +39,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  UintLiteral* Clone(CloneContext* ctx) const override;
+  const UintLiteral* Clone(CloneContext* ctx) const override;
 
   /// The int literal value
-  uint32_t const value;
+  const uint32_t value;
 };
 
 }  // namespace ast
diff --git a/src/ast/unary_op_expression.cc b/src/ast/unary_op_expression.cc
index 21ebfec..c80241a 100644
--- a/src/ast/unary_op_expression.cc
+++ b/src/ast/unary_op_expression.cc
@@ -24,7 +24,7 @@
 UnaryOpExpression::UnaryOpExpression(ProgramID pid,
                                      const Source& src,
                                      UnaryOp o,
-                                     Expression* e)
+                                     const Expression* e)
     : Base(pid, src), op(o), expr(e) {
   TINT_ASSERT(AST, expr);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
@@ -34,7 +34,7 @@
 
 UnaryOpExpression::~UnaryOpExpression() = default;
 
-UnaryOpExpression* UnaryOpExpression::Clone(CloneContext* ctx) const {
+const UnaryOpExpression* UnaryOpExpression::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* e = ctx->Clone(expr);
diff --git a/src/ast/unary_op_expression.h b/src/ast/unary_op_expression.h
index baecf4a..1d3e25a 100644
--- a/src/ast/unary_op_expression.h
+++ b/src/ast/unary_op_expression.h
@@ -32,7 +32,7 @@
   UnaryOpExpression(ProgramID program_id,
                     const Source& source,
                     UnaryOp op,
-                    Expression* expr);
+                    const Expression* expr);
   /// Move constructor
   UnaryOpExpression(UnaryOpExpression&&);
   ~UnaryOpExpression() override;
@@ -41,13 +41,13 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  UnaryOpExpression* Clone(CloneContext* ctx) const override;
+  const UnaryOpExpression* Clone(CloneContext* ctx) const override;
 
   /// The op
-  UnaryOp const op;
+  const UnaryOp op;
 
   /// The expression
-  Expression* const expr;
+  const Expression* const expr;
 };
 
 }  // namespace ast
diff --git a/src/ast/variable.cc b/src/ast/variable.cc
index f5da167..5f9baf8 100644
--- a/src/ast/variable.cc
+++ b/src/ast/variable.cc
@@ -30,11 +30,11 @@
                    Access da,
                    const ast::Type* ty,
                    bool constant,
-                   Expression* ctor,
+                   const Expression* ctor,
                    DecorationList decos)
     : Base(pid, src),
       symbol(sym),
-      type(const_cast<Type*>(ty)),
+      type(ty),
       is_const(constant),
       constructor(ctor),
       decorations(std::move(decos)),
@@ -50,8 +50,8 @@
 Variable::~Variable() = default;
 
 VariableBindingPoint Variable::BindingPoint() const {
-  GroupDecoration* group = nullptr;
-  BindingDecoration* binding = nullptr;
+  const GroupDecoration* group = nullptr;
+  const BindingDecoration* binding = nullptr;
   for (auto* deco : decorations) {
     if (auto* g = deco->As<GroupDecoration>()) {
       group = g;
@@ -62,7 +62,7 @@
   return VariableBindingPoint{group, binding};
 }
 
-Variable* Variable::Clone(CloneContext* ctx) const {
+const Variable* Variable::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   auto sym = ctx->Clone(symbol);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/variable.h b/src/ast/variable.h
index 66d4811..3ef3b8a 100644
--- a/src/ast/variable.h
+++ b/src/ast/variable.h
@@ -35,9 +35,9 @@
 /// VariableBindingPoint holds a group and binding decoration.
 struct VariableBindingPoint {
   /// The `[[group]]` part of the binding point
-  GroupDecoration* group = nullptr;
+  const GroupDecoration* group = nullptr;
   /// The `[[binding]]` part of the binding point
-  BindingDecoration* binding = nullptr;
+  const BindingDecoration* binding = nullptr;
 
   /// @returns true if the BindingPoint has a valid group and binding
   /// decoration.
@@ -116,7 +116,7 @@
            Access declared_access,
            const ast::Type* type,
            bool is_const,
-           Expression* constructor,
+           const Expression* constructor,
            DecorationList decorations);
   /// Move constructor
   Variable(Variable&&);
@@ -130,32 +130,32 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  Variable* Clone(CloneContext* ctx) const override;
+  const Variable* Clone(CloneContext* ctx) const override;
 
   /// The variable symbol
-  Symbol const symbol;
+  const Symbol symbol;
 
   /// The variable type
-  ast::Type* const type;
+  const ast::Type* const type;
 
   /// True if this is a constant, false otherwise
-  bool const is_const;
+  const bool is_const;
 
   /// The constructor expression or nullptr if none set
-  Expression* const constructor;
+  const Expression* const constructor;
 
   /// The decorations attached to this variable
-  DecorationList const decorations;
+  const DecorationList decorations;
 
   /// The declared storage class
-  StorageClass const declared_storage_class;
+  const StorageClass declared_storage_class;
 
   /// The declared access control
-  Access const declared_access;
+  const Access declared_access;
 };
 
 /// A list of variables
-using VariableList = std::vector<Variable*>;
+using VariableList = std::vector<const Variable*>;
 
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/variable_decl_statement.cc b/src/ast/variable_decl_statement.cc
index d4ffaca..8e65919 100644
--- a/src/ast/variable_decl_statement.cc
+++ b/src/ast/variable_decl_statement.cc
@@ -23,7 +23,7 @@
 
 VariableDeclStatement::VariableDeclStatement(ProgramID pid,
                                              const Source& src,
-                                             Variable* var)
+                                             const Variable* var)
     : Base(pid, src), variable(var) {
   TINT_ASSERT(AST, variable);
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, variable, program_id);
@@ -33,7 +33,8 @@
 
 VariableDeclStatement::~VariableDeclStatement() = default;
 
-VariableDeclStatement* VariableDeclStatement::Clone(CloneContext* ctx) const {
+const VariableDeclStatement* VariableDeclStatement::Clone(
+    CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* var = ctx->Clone(variable);
diff --git a/src/ast/variable_decl_statement.h b/src/ast/variable_decl_statement.h
index 3335179..8a148b2 100644
--- a/src/ast/variable_decl_statement.h
+++ b/src/ast/variable_decl_statement.h
@@ -31,7 +31,7 @@
   /// @param variable the variable
   VariableDeclStatement(ProgramID program_id,
                         const Source& source,
-                        Variable* variable);
+                        const Variable* variable);
   /// Move constructor
   VariableDeclStatement(VariableDeclStatement&&);
   ~VariableDeclStatement() override;
@@ -40,10 +40,10 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  VariableDeclStatement* Clone(CloneContext* ctx) const override;
+  const VariableDeclStatement* Clone(CloneContext* ctx) const override;
 
   /// The variable
-  Variable* const variable;
+  const Variable* const variable;
 };
 
 }  // namespace ast
diff --git a/src/ast/vector.cc b/src/ast/vector.cc
index 4fdd60e..bea0dc2 100644
--- a/src/ast/vector.cc
+++ b/src/ast/vector.cc
@@ -22,10 +22,10 @@
 namespace ast {
 
 Vector::Vector(ProgramID pid,
-               const Source& src,
-               Type const* subtype,
+               Source const& src,
+               const Type* subtype,
                uint32_t w)
-    : Base(pid, src), type(const_cast<Type*>(subtype)), width(w) {
+    : Base(pid, src), type(subtype), width(w) {
   TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, subtype, program_id);
   TINT_ASSERT(AST, width > 1);
   TINT_ASSERT(AST, width < 5);
@@ -41,7 +41,7 @@
   return out.str();
 }
 
-Vector* Vector::Clone(CloneContext* ctx) const {
+const Vector* Vector::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* ty = ctx->Clone(type);
diff --git a/src/ast/vector.h b/src/ast/vector.h
index 91a53c5..956594d 100644
--- a/src/ast/vector.h
+++ b/src/ast/vector.h
@@ -30,7 +30,7 @@
   /// @param src the source of this node
   /// @param subtype the vector element type
   /// @param width the number of elements in the vector
-  Vector(ProgramID pid, const Source& src, Type const* subtype, uint32_t width);
+  Vector(ProgramID pid, Source const& src, const Type* subtype, uint32_t width);
   /// Move constructor
   Vector(Vector&&);
   ~Vector() override;
@@ -43,13 +43,13 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Vector* Clone(CloneContext* ctx) const override;
+  const Vector* Clone(CloneContext* ctx) const override;
 
   /// The type of the vector elements
-  Type* const type;
+  const Type* const type;
 
   /// The number of elements in the vector
-  uint32_t const width;
+  const uint32_t width;
 };
 
 }  // namespace ast
diff --git a/src/ast/void.cc b/src/ast/void.cc
index 427d827..59c4670 100644
--- a/src/ast/void.cc
+++ b/src/ast/void.cc
@@ -31,7 +31,7 @@
   return "void";
 }
 
-Void* Void::Clone(CloneContext* ctx) const {
+const Void* Void::Clone(CloneContext* ctx) const {
   auto src = ctx->Clone(source);
   return ctx->dst->create<Void>(src);
 }
diff --git a/src/ast/void.h b/src/ast/void.h
index 46cb3a2..a2b7af4 100644
--- a/src/ast/void.h
+++ b/src/ast/void.h
@@ -41,7 +41,7 @@
   /// Clones this type and all transitive types using the `CloneContext` `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned type
-  Void* Clone(CloneContext* ctx) const override;
+  const Void* Clone(CloneContext* ctx) const override;
 };
 
 }  // namespace ast
diff --git a/src/ast/workgroup_decoration.cc b/src/ast/workgroup_decoration.cc
index 271bb85..cca599d 100644
--- a/src/ast/workgroup_decoration.cc
+++ b/src/ast/workgroup_decoration.cc
@@ -25,9 +25,9 @@
 
 WorkgroupDecoration::WorkgroupDecoration(ProgramID pid,
                                          const Source& src,
-                                         ast::Expression* x_,
-                                         ast::Expression* y_,
-                                         ast::Expression* z_)
+                                         const ast::Expression* x_,
+                                         const ast::Expression* y_,
+                                         const ast::Expression* z_)
     : Base(pid, src), x(x_), y(y_), z(z_) {}
 
 WorkgroupDecoration::~WorkgroupDecoration() = default;
@@ -36,7 +36,7 @@
   return "workgroup_size";
 }
 
-WorkgroupDecoration* WorkgroupDecoration::Clone(CloneContext* ctx) const {
+const WorkgroupDecoration* WorkgroupDecoration::Clone(CloneContext* ctx) const {
   // Clone arguments outside of create() call to have deterministic ordering
   auto src = ctx->Clone(source);
   auto* x_ = ctx->Clone(x);
diff --git a/src/ast/workgroup_decoration.h b/src/ast/workgroup_decoration.h
index daae3f0..119e3c4 100644
--- a/src/ast/workgroup_decoration.h
+++ b/src/ast/workgroup_decoration.h
@@ -37,14 +37,14 @@
   /// @param z the optional workgroup z dimension expression
   WorkgroupDecoration(ProgramID pid,
                       const Source& src,
-                      ast::Expression* x,
-                      ast::Expression* y = nullptr,
-                      ast::Expression* z = nullptr);
+                      const ast::Expression* x,
+                      const ast::Expression* y = nullptr,
+                      const ast::Expression* z = nullptr);
 
   ~WorkgroupDecoration() override;
 
   /// @returns the workgroup dimensions
-  std::array<ast::Expression*, 3> Values() const { return {x, y, z}; }
+  std::array<const ast::Expression*, 3> Values() const { return {x, y, z}; }
 
   /// @returns the WGSL name for the decoration
   std::string Name() const override;
@@ -53,14 +53,14 @@
   /// `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned node
-  WorkgroupDecoration* Clone(CloneContext* ctx) const override;
+  const WorkgroupDecoration* Clone(CloneContext* ctx) const override;
 
   /// The workgroup x dimension.
-  ast::Expression* const x;
+  const ast::Expression* const x;
   /// The optional workgroup y dimension. May be null.
-  ast::Expression* const y = nullptr;
+  const ast::Expression* const y = nullptr;
   /// The optional workgroup z dimension. May be null.
-  ast::Expression* const z = nullptr;
+  const ast::Expression* const z = nullptr;
 };
 
 }  // namespace ast
diff --git a/src/castable.h b/src/castable.h
index ed075b0..67f1dc2 100644
--- a/src/castable.h
+++ b/src/castable.h
@@ -74,7 +74,8 @@
   /// @returns the static TypeInfo for the type T
   template <typename T>
   static const TypeInfo& Of() {
-    return detail::TypeInfoOf<T>::info;
+    using NO_CV = typename std::remove_cv<T>::type;
+    return detail::TypeInfoOf<NO_CV>::info;
   }
 };
 
diff --git a/src/clone_context.cc b/src/clone_context.cc
index ed85d49..e56f754 100644
--- a/src/clone_context.cc
+++ b/src/clone_context.cc
@@ -63,13 +63,13 @@
 ast::FunctionList CloneContext::Clone(const ast::FunctionList& v) {
   ast::FunctionList out;
   out.reserve(v.size());
-  for (ast::Function* el : v) {
+  for (const ast::Function* el : v) {
     out.Add(Clone(el));
   }
   return out;
 }
 
-tint::Cloneable* CloneContext::CloneCloneable(Cloneable* object) {
+const tint::Cloneable* CloneContext::CloneCloneable(const Cloneable* object) {
   // If the input is nullptr, there's nothing to clone - just return nullptr.
   if (object == nullptr) {
     return nullptr;
@@ -97,7 +97,7 @@
   return object->Clone(this);
 }
 
-void CloneContext::CheckedCastFailure(Cloneable* got,
+void CloneContext::CheckedCastFailure(const Cloneable* got,
                                       const TypeInfo& expected) {
   TINT_ICE(Clone, Diagnostics())
       << "Cloned object was not of the expected type\n"
diff --git a/src/clone_context.h b/src/clone_context.h
index 04f5dac..9d765a0 100644
--- a/src/clone_context.h
+++ b/src/clone_context.h
@@ -48,7 +48,7 @@
   /// Performs a deep clone of this object using the CloneContext `ctx`.
   /// @param ctx the clone context
   /// @return the newly cloned object
-  virtual Cloneable* Clone(CloneContext* ctx) const = 0;
+  virtual const Cloneable* Clone(CloneContext* ctx) const = 0;
 };
 
 /// @returns an invalid ProgramID
@@ -97,7 +97,7 @@
   /// @param object the type deriving from Cloneable to clone
   /// @return the cloned node
   template <typename T>
-  T* Clone(T* object) {
+  const T* Clone(const T* object) {
     if (src) {
       TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, object);
     }
@@ -121,7 +121,7 @@
   /// @param a the type deriving from Cloneable to clone
   /// @return the cloned node
   template <typename T>
-  T* CloneWithoutTransform(T* a) {
+  const T* CloneWithoutTransform(const T* a) {
     // If the input is nullptr, there's nothing to clone - just return nullptr.
     if (a == nullptr) {
       return nullptr;
@@ -289,7 +289,9 @@
     }
     CloneableTransform transform;
     transform.typeinfo = &TypeInfo::Of<T>();
-    transform.function = [=](Cloneable* in) { return replacer(in->As<T>()); };
+    transform.function = [=](const Cloneable* in) {
+      return replacer(in->As<T>());
+    };
     transforms_.emplace_back(std::move(transform));
     return *this;
   }
@@ -329,10 +331,10 @@
   template <typename WHAT,
             typename WITH,
             typename = traits::EnableIfIsType<WITH, Cloneable>>
-  CloneContext& Replace(WHAT* what, WITH* with) {
+  CloneContext& Replace(const WHAT* what, const WITH* with) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, with);
-    replacements_[what] = [with]() -> Cloneable* { return with; };
+    replacements_[what] = [with]() -> const Cloneable* { return with; };
     return *this;
   }
 
@@ -350,7 +352,7 @@
   /// assertion in debug builds, and undefined behavior in release builds.
   /// @returns this CloneContext so calls can be chained
   template <typename WHAT, typename WITH, typename = std::result_of_t<WITH()>>
-  CloneContext& Replace(WHAT* what, WITH&& with) {
+  CloneContext& Replace(const WHAT* what, WITH&& with) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
     replacements_[what] = with;
     return *this;
@@ -411,7 +413,7 @@
   template <typename T, typename BEFORE, typename OBJECT>
   CloneContext& InsertBefore(const std::vector<T>& vector,
                              const BEFORE* before,
-                             OBJECT* object) {
+                             const OBJECT* object) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, before);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
     if (std::find(vector.begin(), vector.end(), before) == vector.end()) {
@@ -435,7 +437,7 @@
   template <typename T, typename AFTER, typename OBJECT>
   CloneContext& InsertAfter(const std::vector<T>& vector,
                             const AFTER* after,
-                            OBJECT* object) {
+                            const OBJECT* object) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, after);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
     if (std::find(vector.begin(), vector.end(), after) == vector.end()) {
@@ -473,7 +475,7 @@
 
     // TypeInfo of the Cloneable that the transform operates on
     const TypeInfo* typeinfo;
-    std::function<Cloneable*(Cloneable*)> function;
+    std::function<const Cloneable*(const Cloneable*)> function;
   };
 
   CloneContext(const CloneContext&) = delete;
@@ -482,11 +484,11 @@
   /// Cast `obj` from type `FROM` to type `TO`, returning the cast object.
   /// Reports an internal compiler error if the cast failed.
   template <typename TO, typename FROM>
-  TO* CheckedCast(FROM* obj) {
+  const TO* CheckedCast(const FROM* obj) {
     if (obj == nullptr) {
       return nullptr;
     }
-    if (TO* cast = obj->template As<TO>()) {
+    if (const TO* cast = obj->template As<TO>()) {
       return cast;
     }
     CheckedCastFailure(obj, TypeInfo::Of<TO>());
@@ -495,17 +497,17 @@
 
   /// Clones a Cloneable object, using any replacements or transforms that have
   /// been configured.
-  tint::Cloneable* CloneCloneable(Cloneable* object);
+  const Cloneable* CloneCloneable(const Cloneable* object);
 
   /// Adds an error diagnostic to Diagnostics() that the cloned object was not
   /// of the expected type.
-  void CheckedCastFailure(Cloneable* got, const TypeInfo& expected);
+  void CheckedCastFailure(const Cloneable* got, const TypeInfo& expected);
 
   /// @returns the diagnostic list of #dst
   diag::List& Diagnostics() const;
 
-  /// A vector of Cloneable*
-  using CloneableList = std::vector<Cloneable*>;
+  /// A vector of const Cloneable*
+  using CloneableList = std::vector<const Cloneable*>;
 
   /// Transformations to be applied to a list (vector)
   struct ListTransforms {
@@ -538,7 +540,7 @@
 
   /// A map of object in #src to functions that create their replacement in
   /// #dst
-  std::unordered_map<const Cloneable*, std::function<Cloneable*()>>
+  std::unordered_map<const Cloneable*, std::function<const Cloneable*()>>
       replacements_;
 
   /// A map of symbol in #src to their cloned equivalent in #dst
diff --git a/src/clone_context_test.cc b/src/clone_context_test.cc
index e3720cb..d51d4ae 100644
--- a/src/clone_context_test.cc
+++ b/src/clone_context_test.cc
@@ -31,13 +31,18 @@
 };
 
 struct Node : public Castable<Node, Cloneable> {
-  Node(Allocator* alloc, Symbol n) : allocator(alloc), name(n) {}
+  Node(Allocator* alloc,
+       Symbol n,
+       const Node* node_a = nullptr,
+       const Node* node_b = nullptr,
+       const Node* node_c = nullptr)
+      : allocator(alloc), name(n), a(node_a), b(node_b), c(node_c) {}
   Allocator* const allocator;
   Symbol name;
-  Node* a = nullptr;
-  Node* b = nullptr;
-  Node* c = nullptr;
-  std::vector<Node*> vec;
+  const Node* a = nullptr;
+  const Node* b = nullptr;
+  const Node* c = nullptr;
+  std::vector<const Node*> vec;
 
   Node* Clone(CloneContext* ctx) const override {
     auto* out = allocator->Create<Node>(ctx->Clone(name));
@@ -50,7 +55,12 @@
 };
 
 struct Replaceable : public Castable<Replaceable, Node> {
-  Replaceable(Allocator* alloc, Symbol n) : Base(alloc, n) {}
+  Replaceable(Allocator* alloc,
+              Symbol n,
+              const Node* node_a = nullptr,
+              const Node* node_b = nullptr,
+              const Node* node_c = nullptr)
+      : Base(alloc, n, node_a, node_b, node_c) {}
 };
 
 struct Replacement : public Castable<Replacement, Replaceable> {
@@ -71,8 +81,8 @@
       : allocator(alloc), program_id(id), cloned_program_id(cloned_id) {}
 
   Allocator* const allocator;
-  ProgramID const program_id;
-  ProgramID const cloned_program_id;
+  const ProgramID program_id;
+  const ProgramID cloned_program_id;
 
   ProgramNode* Clone(CloneContext*) const override {
     return allocator->Create<ProgramNode>(cloned_program_id, cloned_program_id);
@@ -86,16 +96,19 @@
 using CloneContextNodeTest = ::testing::Test;
 
 TEST_F(CloneContextNodeTest, Clone) {
-  Allocator a;
+  Allocator alloc;
 
   ProgramBuilder builder;
-  auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-  original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-  original_root->a->b = a.Create<Node>(builder.Symbols().New("a->b"));
-  original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-  original_root->b->a = original_root->a;  // Aliased
-  original_root->b->b = a.Create<Node>(builder.Symbols().New("b->b"));
-  original_root->c = original_root->b;  // Aliased
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+    auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
   Program original(std::move(builder));
 
   //                          root
@@ -142,15 +155,19 @@
 }
 
 TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Cloneable) {
-  Allocator a;
+  Allocator alloc;
 
   ProgramBuilder builder;
-  auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-  original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-  original_root->a->b = a.Create<Replaceable>(builder.Symbols().New("a->b"));
-  original_root->b = a.Create<Replaceable>(builder.Symbols().New("b"));
-  original_root->b->a = original_root->a;  // Aliased
-  original_root->c = original_root->b;     // Aliased
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Replaceable>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b =
+        alloc.Create<Replaceable>(builder.Symbols().New("b"), b_a, nullptr);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
   Program original(std::move(builder));
 
   //                          root
@@ -167,13 +184,13 @@
   ProgramBuilder cloned;
 
   CloneContext ctx(&cloned, &original);
-  ctx.ReplaceAll([&](Replaceable* in) {
+  ctx.ReplaceAll([&](const Replaceable* in) {
     auto out_name = cloned.Symbols().Register(
         "replacement:" + original.Symbols().NameFor(in->name));
     auto b_name = cloned.Symbols().Register(
         "replacement-child:" + original.Symbols().NameFor(in->name));
-    auto* out = a.Create<Replacement>(out_name);
-    out->b = a.Create<Node>(b_name);
+    auto* out = alloc.Create<Replacement>(out_name);
+    out->b = alloc.Create<Node>(b_name);
     out->c = ctx.Clone(in->a);
     return out;
   });
@@ -235,16 +252,19 @@
 }
 
 TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Symbols) {
-  Allocator a;
+  Allocator alloc;
 
   ProgramBuilder builder;
-  auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-  original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-  original_root->a->b = a.Create<Node>(builder.Symbols().New("a->b"));
-  original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-  original_root->b->a = original_root->a;  // Aliased
-  original_root->b->b = a.Create<Node>(builder.Symbols().New("b->b"));
-  original_root->c = original_root->b;  // Aliased
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+    auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
   Program original(std::move(builder));
 
   //                          root
@@ -282,7 +302,7 @@
 
   ProgramBuilder cloned;
   CloneContext ctx(&cloned, &original);
-  ctx.ReplaceAll([&](Node*) {
+  ctx.ReplaceAll([&](const Node*) {
     return a.Create<Replacement>(builder.Symbols().New("<unexpected-node>"));
   });
 
@@ -629,8 +649,8 @@
         ProgramBuilder cloned;
         Program original;
         CloneContext ctx(&cloned, &original);
-        ctx.ReplaceAll([](Node*) { return nullptr; });
-        ctx.ReplaceAll([](Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
       },
       "internal compiler error: ReplaceAll() called with a handler for type " +
           node_name + " that is already handled by a handler for type " +
@@ -646,8 +666,8 @@
         ProgramBuilder cloned;
         Program original;
         CloneContext ctx(&cloned, &original);
-        ctx.ReplaceAll([](Node*) { return nullptr; });
-        ctx.ReplaceAll([](Replaceable*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
       },
       "internal compiler error: ReplaceAll() called with a handler for type " +
           replaceable_name + " that is already handled by a handler for type " +
@@ -663,8 +683,8 @@
         ProgramBuilder cloned;
         Program original;
         CloneContext ctx(&cloned, &original);
-        ctx.ReplaceAll([](Replaceable*) { return nullptr; });
-        ctx.ReplaceAll([](Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
       },
       "internal compiler error: ReplaceAll() called with a handler for type " +
           node_name + " that is already handled by a handler for type " +
@@ -735,8 +755,8 @@
         ProgramBuilder cloned;
         Program original;
         CloneContext ctx(&cloned, &original);
-        ctx.ReplaceAll([](Symbol s) { return s; });
-        ctx.ReplaceAll([](Symbol s) { return s; });
+        ctx.ReplaceAll([](const Symbol s) { return s; });
+        ctx.ReplaceAll([](const Symbol s) { return s; });
       },
       "internal compiler error: ReplaceAll(const SymbolTransform&) called "
       "multiple times on the same CloneContext");
diff --git a/src/debug.h b/src/debug.h
index d6086d7..f17afaf 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -67,7 +67,7 @@
 
  private:
   char const* const file_;
-  size_t const line_;
+  const size_t line_;
   diag::System system_;
   diag::List& diagnostics_;
   std::stringstream msg_;
diff --git a/src/diagnostic/formatter.h b/src/diagnostic/formatter.h
index dbb2856..91afd81 100644
--- a/src/diagnostic/formatter.h
+++ b/src/diagnostic/formatter.h
@@ -63,7 +63,7 @@
 
   void format(const Diagnostic& diag, State& state) const;
 
-  Style const style_;
+  const Style style_;
 };
 
 }  // namespace diag
diff --git a/src/diagnostic/printer_linux.cc b/src/diagnostic/printer_linux.cc
index 99b0da4..42c5264 100644
--- a/src/diagnostic/printer_linux.cc
+++ b/src/diagnostic/printer_linux.cc
@@ -87,7 +87,7 @@
   }
 
   FILE* const file;
-  bool const use_colors;
+  const bool use_colors;
 };
 
 }  // namespace
diff --git a/src/diagnostic/printer_windows.cc b/src/diagnostic/printer_windows.cc
index 504ca9b..eba1dda 100644
--- a/src/diagnostic/printer_windows.cc
+++ b/src/diagnostic/printer_windows.cc
@@ -100,7 +100,7 @@
   }
 
   FILE* const file;
-  ConsoleInfo const console;
+  const ConsoleInfo console;
 };
 
 }  // namespace
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index fe64df1..ca2a968 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -579,7 +579,7 @@
   return total_size;
 }
 
-ast::Function* Inspector::FindEntryPointByName(const std::string& name) {
+const ast::Function* Inspector::FindEntryPointByName(const std::string& name) {
   auto* func = program_->AST().Functions().Find(program_->Symbols().Get(name));
   if (!func) {
     diagnostics_.add_error(diag::System::Inspector, name + " was not found!");
@@ -597,7 +597,7 @@
 
 void Inspector::AddEntryPointInOutVariables(
     std::string name,
-    sem::Type* type,
+    const sem::Type* type,
     const ast::DecorationList& decorations,
     std::vector<StageVariable>& variables) const {
   // Skip builtins.
@@ -638,7 +638,7 @@
 }
 
 bool Inspector::ContainsBuiltin(ast::Builtin builtin,
-                                sem::Type* type,
+                                const sem::Type* type,
                                 const ast::DecorationList& decorations) const {
   auto* unwrapped_type = type->UnwrapRef();
 
diff --git a/src/inspector/inspector.h b/src/inspector/inspector.h
index 4a9938d..8a8f582 100644
--- a/src/inspector/inspector.h
+++ b/src/inspector/inspector.h
@@ -147,7 +147,7 @@
   /// @param name name of the entry point to find
   /// @returns a pointer to the entry point if it exists, otherwise returns
   ///          nullptr and sets the error string.
-  ast::Function* FindEntryPointByName(const std::string& name);
+  const ast::Function* FindEntryPointByName(const std::string& name);
 
   /// Recursively add entry point IO variables.
   /// If `type` is a struct, recurse into members, appending the member name.
@@ -157,7 +157,7 @@
   /// @param decorations the variable decorations
   /// @param variables the list to add the variables to
   void AddEntryPointInOutVariables(std::string name,
-                                   sem::Type* type,
+                                   const sem::Type* type,
                                    const ast::DecorationList& decorations,
                                    std::vector<StageVariable>& variables) const;
 
@@ -165,7 +165,7 @@
   /// If `type` is a struct, recurse into members to check for the decoration.
   /// Otherwise, check `decorations` for the decoration.
   bool ContainsBuiltin(ast::Builtin builtin,
-                       sem::Type* type,
+                       const sem::Type* type,
                        const ast::DecorationList& decorations) const;
 
   /// Gathers all the texture resource bindings of the given type for the given
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index bda54c0..743449b 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -300,7 +300,7 @@
   ComponentType component;
   CompositionType composition;
   std::tie(component, composition) = GetParam();
-  std::function<ast::Type*()> tint_type =
+  std::function<const ast::Type*()> tint_type =
       GetTypeFunction(component, composition);
 
   auto* in_var = Param("in_var", tint_type(), {Location(0u)});
@@ -683,7 +683,7 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, NonOverridableConstantSkipped) {
-  ast::Struct* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
   AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
   MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
   MakeCallerBodyFunction("ep_func", {"ub_func"},
@@ -1193,8 +1193,7 @@
 }
 
 TEST_F(InspectorGetStorageSizeTest, Simple) {
-  ast::Struct* ub_struct_type =
-      MakeUniformBufferType("ub_type", {ty.i32(), ty.i32()});
+  auto* ub_struct_type = MakeUniformBufferType("ub_type", {ty.i32(), ty.i32()});
   AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
   MakeStructVariableReferenceBodyFunction("ub_func", "ub_var", {{0, ty.i32()}});
 
@@ -1232,7 +1231,7 @@
 }
 
 TEST_F(InspectorGetResourceBindingsTest, Simple) {
-  ast::Struct* ub_struct_type = MakeUniformBufferType("ub_type", {ty.i32()});
+  auto* ub_struct_type = MakeUniformBufferType("ub_type", {ty.i32()});
   AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
   MakeStructVariableReferenceBodyFunction("ub_func", "ub_var", {{0, ty.i32()}});
 
@@ -1339,7 +1338,7 @@
 }
 
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, NonEntryPointFunc) {
-  ast::Struct* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
   AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
 
   MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
@@ -1357,7 +1356,7 @@
 }
 
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, Simple) {
-  ast::Struct* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
   AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
 
   MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
@@ -1382,7 +1381,7 @@
 }
 
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, MultipleMembers) {
-  ast::Struct* foo_struct_type =
+  auto* foo_struct_type =
       MakeUniformBufferType("foo_type", {ty.i32(), ty.u32(), ty.f32()});
   AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
 
@@ -1409,8 +1408,7 @@
 }
 
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, ContainingPadding) {
-  ast::Struct* foo_struct_type =
-      MakeUniformBufferType("foo_type", {ty.vec3<f32>()});
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.vec3<f32>()});
   AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
 
   MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub",
@@ -1436,7 +1434,7 @@
 }
 
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, MultipleUniformBuffers) {
-  ast::Struct* ub_struct_type =
+  auto* ub_struct_type =
       MakeUniformBufferType("ub_type", {ty.i32(), ty.u32(), ty.f32()});
   AddUniformBuffer("ub_foo", ty.Of(ub_struct_type), 0, 0);
   AddUniformBuffer("ub_bar", ty.Of(ub_struct_type), 0, 1);
@@ -1493,7 +1491,7 @@
 TEST_F(InspectorGetUniformBufferResourceBindingsTest, ContainingArray) {
   // Manually create uniform buffer to make sure it had a valid layout (array
   // with elem stride of 16, and that is 16-byte aligned within the struct)
-  ast::Struct* foo_struct_type = Structure(
+  auto* foo_struct_type = Structure(
       "foo_type",
       {Member("0i32", ty.i32()),
        Member("b", ty.array(ty.u32(), 4, /*stride*/ 16), {MemberAlign(16)})},
@@ -2370,7 +2368,7 @@
   auto* st_type = MakeStorageTextureTypes(dim, format);
   AddStorageTexture("st_var", st_type, 0, 0);
 
-  ast::Type* dim_type = nullptr;
+  const ast::Type* dim_type = nullptr;
   switch (dim) {
     case ast::TextureDimension::k1d:
       dim_type = ty.i32();
@@ -2849,7 +2847,7 @@
 TEST_F(InspectorGetWorkgroupStorageSizeTest, CompoundTypes) {
   // This struct should occupy 68 bytes. 4 from the i32 field, and another 64
   // from the 4-element array with 16-byte stride.
-  ast::Struct* wg_struct_type = MakeStructType(
+  auto* wg_struct_type = MakeStructType(
       "WgStruct", {ty.i32(), ty.array(ty.i32(), 4, /*stride=*/16)},
       /*is_block=*/false);
   AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
@@ -2891,7 +2889,7 @@
   // Per WGSL spec, a struct's size is the offset its last member plus the size
   // of its last member, rounded up to the alignment of its largest member. So
   // here the struct is expected to occupy 1024 bytes of workgroup storage.
-  ast::Struct* wg_struct_type = MakeStructTypeFromMembers(
+  const auto* wg_struct_type = MakeStructTypeFromMembers(
       "WgStruct",
       {MakeStructMember(0, ty.f32(),
                         {create<ast::StructMemberAlignDecoration>(1024)})},
diff --git a/src/inspector/resource_binding.cc b/src/inspector/resource_binding.cc
index 8519140..e9f6e8a 100644
--- a/src/inspector/resource_binding.cc
+++ b/src/inspector/resource_binding.cc
@@ -53,7 +53,7 @@
   }
 
   if (auto* at = base_type->As<sem::Array>()) {
-    base_type = const_cast<sem::Type*>(at->ElemType());
+    base_type = at->ElemType();
   } else if (auto* mt = base_type->As<sem::Matrix>()) {
     base_type = mt->type();
   } else if (auto* vt = base_type->As<sem::Vector>()) {
diff --git a/src/inspector/test_inspector_builder.cc b/src/inspector/test_inspector_builder.cc
index 1d42d58..649004a 100644
--- a/src/inspector/test_inspector_builder.cc
+++ b/src/inspector/test_inspector_builder.cc
@@ -47,7 +47,7 @@
   Func(caller, ast::VariableList(), ty.void_(), body, decorations);
 }
 
-ast::Struct* InspectorBuilder::MakeInOutStruct(
+const ast::Struct* InspectorBuilder::MakeInOutStruct(
     std::string name,
     std::vector<std::tuple<std::string, uint32_t>> inout_vars) {
   ast::StructMemberList members;
@@ -60,10 +60,10 @@
   return Structure(name, members);
 }
 
-ast::Function* InspectorBuilder::MakePlainGlobalReferenceBodyFunction(
+const ast::Function* InspectorBuilder::MakePlainGlobalReferenceBodyFunction(
     std::string func,
     std::string var,
-    ast::Type* type,
+    const ast::Type* type,
     ast::DecorationList decorations) {
   ast::StatementList stmts;
   stmts.emplace_back(Decl(Var("local_" + var, type)));
@@ -83,13 +83,14 @@
   return false;
 }
 
-std::string InspectorBuilder::StructMemberName(size_t idx, ast::Type* type) {
+std::string InspectorBuilder::StructMemberName(size_t idx,
+                                               const ast::Type* type) {
   return std::to_string(idx) + type->FriendlyName(Symbols());
 }
 
-ast::Struct* InspectorBuilder::MakeStructType(
+const ast::Struct* InspectorBuilder::MakeStructType(
     const std::string& name,
-    std::vector<ast::Type*> member_types,
+    std::vector<const ast::Type*> member_types,
     bool is_block) {
   ast::StructMemberList members;
   for (auto* type : member_types) {
@@ -98,7 +99,7 @@
   return MakeStructTypeFromMembers(name, std::move(members), is_block);
 }
 
-ast::Struct* InspectorBuilder::MakeStructTypeFromMembers(
+const ast::Struct* InspectorBuilder::MakeStructTypeFromMembers(
     const std::string& name,
     ast::StructMemberList members,
     bool is_block) {
@@ -109,28 +110,28 @@
   return Structure(name, std::move(members), decos);
 }
 
-ast::StructMember* InspectorBuilder::MakeStructMember(
+const ast::StructMember* InspectorBuilder::MakeStructMember(
     size_t index,
-    ast::Type* type,
+    const ast::Type* type,
     ast::DecorationList decorations) {
   return Member(StructMemberName(index, type), type, std::move(decorations));
 }
 
-ast::Struct* InspectorBuilder::MakeUniformBufferType(
+const ast::Struct* InspectorBuilder::MakeUniformBufferType(
     const std::string& name,
-    std::vector<ast::Type*> member_types) {
+    std::vector<const ast::Type*> member_types) {
   return MakeStructType(name, member_types, true);
 }
 
-std::function<ast::TypeName*()> InspectorBuilder::MakeStorageBufferTypes(
+std::function<const ast::TypeName*()> InspectorBuilder::MakeStorageBufferTypes(
     const std::string& name,
-    std::vector<ast::Type*> member_types) {
+    std::vector<const ast::Type*> member_types) {
   MakeStructType(name, member_types, true);
   return [this, name] { return ty.type_name(name); };
 }
 
 void InspectorBuilder::AddUniformBuffer(const std::string& name,
-                                        ast::Type* type,
+                                        const ast::Type* type,
                                         uint32_t group,
                                         uint32_t binding) {
   Global(name, type, ast::StorageClass::kUniform,
@@ -141,12 +142,12 @@
 }
 
 void InspectorBuilder::AddWorkgroupStorage(const std::string& name,
-                                           ast::Type* type) {
+                                           const ast::Type* type) {
   Global(name, type, ast::StorageClass::kWorkgroup);
 }
 
 void InspectorBuilder::AddStorageBuffer(const std::string& name,
-                                        ast::Type* type,
+                                        const ast::Type* type,
                                         ast::Access access,
                                         uint32_t group,
                                         uint32_t binding) {
@@ -160,11 +161,11 @@
 void InspectorBuilder::MakeStructVariableReferenceBodyFunction(
     std::string func_name,
     std::string struct_name,
-    std::vector<std::tuple<size_t, ast::Type*>> members) {
+    std::vector<std::tuple<size_t, const ast::Type*>> members) {
   ast::StatementList stmts;
   for (auto member : members) {
     size_t member_idx;
-    ast::Type* member_type;
+    const ast::Type* member_type;
     std::tie(member_idx, member_type) = member;
     std::string member_name = StructMemberName(member_idx, member_type);
 
@@ -173,7 +174,7 @@
 
   for (auto member : members) {
     size_t member_idx;
-    ast::Type* member_type;
+    const ast::Type* member_type;
     std::tie(member_idx, member_type) = member;
     std::string member_name = StructMemberName(member_idx, member_type);
 
@@ -208,7 +209,7 @@
 }
 
 void InspectorBuilder::AddResource(const std::string& name,
-                                   ast::Type* type,
+                                   const ast::Type* type,
                                    uint32_t group,
                                    uint32_t binding) {
   Global(name, type,
@@ -219,16 +220,16 @@
 }
 
 void InspectorBuilder::AddGlobalVariable(const std::string& name,
-                                         ast::Type* type) {
+                                         const ast::Type* type) {
   Global(name, type, ast::StorageClass::kPrivate);
 }
 
-ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
     const std::string& func_name,
     const std::string& texture_name,
     const std::string& sampler_name,
     const std::string& coords_name,
-    ast::Type* base_type,
+    const ast::Type* base_type,
     ast::DecorationList decorations) {
   std::string result_name = "sampler_result";
 
@@ -242,13 +243,13 @@
   return Func(func_name, ast::VariableList(), ty.void_(), stmts, decorations);
 }
 
-ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
     const std::string& func_name,
     const std::string& texture_name,
     const std::string& sampler_name,
     const std::string& coords_name,
     const std::string& array_index,
-    ast::Type* base_type,
+    const ast::Type* base_type,
     ast::DecorationList decorations) {
   std::string result_name = "sampler_result";
 
@@ -264,13 +265,14 @@
   return Func(func_name, ast::VariableList(), ty.void_(), stmts, decorations);
 }
 
-ast::Function* InspectorBuilder::MakeComparisonSamplerReferenceBodyFunction(
+const ast::Function*
+InspectorBuilder::MakeComparisonSamplerReferenceBodyFunction(
     const std::string& func_name,
     const std::string& texture_name,
     const std::string& sampler_name,
     const std::string& coords_name,
     const std::string& depth_name,
-    ast::Type* base_type,
+    const ast::Type* base_type,
     ast::DecorationList decorations) {
   std::string result_name = "sampler_result";
 
@@ -285,7 +287,7 @@
   return Func(func_name, ast::VariableList(), ty.void_(), stmts, decorations);
 }
 
-ast::Type* InspectorBuilder::GetBaseType(
+const ast::Type* InspectorBuilder::GetBaseType(
     ResourceBinding::SampledKind sampled_kind) {
   switch (sampled_kind) {
     case ResourceBinding::SampledKind::kFloat:
@@ -299,8 +301,8 @@
   }
 }
 
-ast::Type* InspectorBuilder::GetCoordsType(ast::TextureDimension dim,
-                                           ast::Type* scalar) {
+const ast::Type* InspectorBuilder::GetCoordsType(ast::TextureDimension dim,
+                                                 const ast::Type* scalar) {
   switch (dim) {
     case ast::TextureDimension::k1d:
       return scalar;
@@ -317,13 +319,14 @@
   return nullptr;
 }
 
-ast::Type* InspectorBuilder::MakeStorageTextureTypes(ast::TextureDimension dim,
-                                                     ast::ImageFormat format) {
+const ast::Type* InspectorBuilder::MakeStorageTextureTypes(
+    ast::TextureDimension dim,
+    ast::ImageFormat format) {
   return ty.storage_texture(dim, format, ast::Access::kWrite);
 }
 
 void InspectorBuilder::AddStorageTexture(const std::string& name,
-                                         ast::Type* type,
+                                         const ast::Type* type,
                                          uint32_t group,
                                          uint32_t binding) {
   Global(name, type,
@@ -333,10 +336,10 @@
          });
 }
 
-ast::Function* InspectorBuilder::MakeStorageTextureBodyFunction(
+const ast::Function* InspectorBuilder::MakeStorageTextureBodyFunction(
     const std::string& func_name,
     const std::string& st_name,
-    ast::Type* dim_type,
+    const ast::Type* dim_type,
     ast::DecorationList decorations) {
   ast::StatementList stmts;
 
@@ -347,22 +350,22 @@
   return Func(func_name, ast::VariableList(), ty.void_(), stmts, decorations);
 }
 
-std::function<ast::Type*()> InspectorBuilder::GetTypeFunction(
+std::function<const ast::Type*()> InspectorBuilder::GetTypeFunction(
     ComponentType component,
     CompositionType composition) {
-  std::function<ast::Type*()> func;
+  std::function<const ast::Type*()> func;
   switch (component) {
     case ComponentType::kFloat:
-      func = [this]() -> ast::Type* { return ty.f32(); };
+      func = [this]() -> const ast::Type* { return ty.f32(); };
       break;
     case ComponentType::kSInt:
-      func = [this]() -> ast::Type* { return ty.i32(); };
+      func = [this]() -> const ast::Type* { return ty.i32(); };
       break;
     case ComponentType::kUInt:
-      func = [this]() -> ast::Type* { return ty.u32(); };
+      func = [this]() -> const ast::Type* { return ty.u32(); };
       break;
     case ComponentType::kUnknown:
-      return []() -> ast::Type* { return nullptr; };
+      return []() -> const ast::Type* { return nullptr; };
   }
 
   uint32_t n;
@@ -382,7 +385,7 @@
       return []() -> ast::Type* { return nullptr; };
   }
 
-  return [this, func, n]() -> ast::Type* { return ty.vec(func(), n); };
+  return [this, func, n]() -> const ast::Type* { return ty.vec(func(), n); };
 }
 
 Inspector& InspectorBuilder::Build() {
diff --git a/src/inspector/test_inspector_builder.h b/src/inspector/test_inspector_builder.h
index faa69a4..e3783ab 100644
--- a/src/inspector/test_inspector_builder.h
+++ b/src/inspector/test_inspector_builder.h
@@ -60,7 +60,7 @@
   /// @param name the name of the generated struct
   /// @param inout_vars tuples of {name, loc} that will be the struct members
   /// @returns a structure object
-  ast::Struct* MakeInOutStruct(
+  const ast::Struct* MakeInOutStruct(
       std::string name,
       std::vector<std::tuple<std::string, uint32_t>> inout_vars);
 
@@ -91,7 +91,7 @@
   ///                   calls in the function body
   /// @param decorations the function decorations
   /// @returns a function object
-  ast::Function* MakeInOutVariableCallerBodyFunction(
+  const ast::Function* MakeInOutVariableCallerBodyFunction(
       std::string caller,
       std::string callee,
       std::vector<std::tuple<std::string, std::string>> inout_vars,
@@ -104,10 +104,11 @@
   /// @param constructor val to initialize the constant with, if NULL no
   ///             constructor will be added.
   /// @returns the constant that was created
-  ast::Variable* AddOverridableConstantWithID(std::string name,
-                                              uint32_t id,
-                                              ast::Type* type,
-                                              ast::Expression* constructor) {
+  const ast::Variable* AddOverridableConstantWithID(
+      std::string name,
+      uint32_t id,
+      const ast::Type* type,
+      const ast::Expression* constructor) {
     return GlobalConst(name, type, constructor,
                        ast::DecorationList{
                            Override(id),
@@ -120,9 +121,10 @@
   /// @param constructor val to initialize the constant with, if NULL no
   ///             constructor will be added.
   /// @returns the constant that was created
-  ast::Variable* AddOverridableConstantWithoutID(std::string name,
-                                                 ast::Type* type,
-                                                 ast::Expression* constructor) {
+  const ast::Variable* AddOverridableConstantWithoutID(
+      std::string name,
+      const ast::Type* type,
+      const ast::Expression* constructor) {
     return GlobalConst(name, type, constructor,
                        ast::DecorationList{
                            Override(),
@@ -136,10 +138,10 @@
   /// @param type type of the const being referenced
   /// @param decorations the function decorations
   /// @returns a function object
-  ast::Function* MakePlainGlobalReferenceBodyFunction(
+  const ast::Function* MakePlainGlobalReferenceBodyFunction(
       std::string func,
       std::string var,
-      ast::Type* type,
+      const ast::Type* type,
       ast::DecorationList decorations);
 
   /// @param vec Vector of StageVariable to be searched
@@ -152,49 +154,50 @@
   /// @param idx index of member
   /// @param type type of member
   /// @returns a string for the member
-  std::string StructMemberName(size_t idx, ast::Type* type);
+  std::string StructMemberName(size_t idx, const ast::Type* type);
 
   /// Generates a struct type
   /// @param name name for the type
   /// @param member_types a vector of member types
   /// @param is_block whether or not to decorate as a Block
   /// @returns a struct type
-  ast::Struct* MakeStructType(const std::string& name,
-                              std::vector<ast::Type*> member_types,
-                              bool is_block);
+  const ast::Struct* MakeStructType(const std::string& name,
+                                    std::vector<const ast::Type*> member_types,
+                                    bool is_block);
 
   /// Generates a struct type from a list of member nodes.
   /// @param name name for the struct type
   /// @param members a vector of members
   /// @param is_block whether or not to decorate as a Block
   /// @returns a struct type
-  ast::Struct* MakeStructTypeFromMembers(const std::string& name,
-                                         ast::StructMemberList members,
-                                         bool is_block);
+  const ast::Struct* MakeStructTypeFromMembers(const std::string& name,
+                                               ast::StructMemberList members,
+                                               bool is_block);
 
   /// Generates a struct member with a specified index and type.
   /// @param index index of the field within the struct
   /// @param type the type of the member field
   /// @param decorations a list of decorations to apply to the member field
   /// @returns a struct member
-  ast::StructMember* MakeStructMember(size_t index,
-                                      ast::Type* type,
-                                      ast::DecorationList decorations);
+  const ast::StructMember* MakeStructMember(size_t index,
+                                            const ast::Type* type,
+                                            ast::DecorationList decorations);
 
   /// Generates types appropriate for using in an uniform buffer
   /// @param name name for the type
   /// @param member_types a vector of member types
   /// @returns a struct type that has the layout for an uniform buffer.
-  ast::Struct* MakeUniformBufferType(const std::string& name,
-                                     std::vector<ast::Type*> member_types);
+  const ast::Struct* MakeUniformBufferType(
+      const std::string& name,
+      std::vector<const ast::Type*> member_types);
 
   /// Generates types appropriate for using in a storage buffer
   /// @param name name for the type
   /// @param member_types a vector of member types
   /// @returns a function that returns the created structure.
-  std::function<ast::TypeName*()> MakeStorageBufferTypes(
+  std::function<const ast::TypeName*()> MakeStorageBufferTypes(
       const std::string& name,
-      std::vector<ast::Type*> member_types);
+      std::vector<const ast::Type*> member_types);
 
   /// Adds an uniform buffer variable to the program
   /// @param name the name of the variable
@@ -202,14 +205,14 @@
   /// @param group the binding/group/ to use for the uniform buffer
   /// @param binding the binding number to use for the uniform buffer
   void AddUniformBuffer(const std::string& name,
-                        ast::Type* type,
+                        const ast::Type* type,
                         uint32_t group,
                         uint32_t binding);
 
   /// Adds a workgroup storage variable to the program
   /// @param name the name of the variable
   /// @param type the type of the variable
-  void AddWorkgroupStorage(const std::string& name, ast::Type* type);
+  void AddWorkgroupStorage(const std::string& name, const ast::Type* type);
 
   /// Adds a storage buffer variable to the program
   /// @param name the name of the variable
@@ -218,7 +221,7 @@
   /// @param group the binding/group to use for the storage buffer
   /// @param binding the binding number to use for the storage buffer
   void AddStorageBuffer(const std::string& name,
-                        ast::Type* type,
+                        const ast::Type* type,
                         ast::Access access,
                         uint32_t group,
                         uint32_t binding);
@@ -230,7 +233,7 @@
   void MakeStructVariableReferenceBodyFunction(
       std::string func_name,
       std::string struct_name,
-      std::vector<std::tuple<size_t, ast::Type*>> members);
+      std::vector<std::tuple<size_t, const ast::Type*>> members);
 
   /// Adds a regular sampler variable to the program
   /// @param name the name of the variable
@@ -252,14 +255,14 @@
   /// @param group the binding/group to use for the resource
   /// @param binding the binding number to use for the resource
   void AddResource(const std::string& name,
-                   ast::Type* type,
+                   const ast::Type* type,
                    uint32_t group,
                    uint32_t binding);
 
   /// Add a module scope private variable to the progames
   /// @param name the name of the variable
   /// @param type the type to use
-  void AddGlobalVariable(const std::string& name, ast::Type* type);
+  void AddGlobalVariable(const std::string& name, const ast::Type* type);
 
   /// Generates a function that references a specific sampler variable
   /// @param func_name name of the function created
@@ -269,12 +272,12 @@
   /// @param base_type sampler base type
   /// @param decorations the function decorations
   /// @returns a function that references all of the values specified
-  ast::Function* MakeSamplerReferenceBodyFunction(
+  const ast::Function* MakeSamplerReferenceBodyFunction(
       const std::string& func_name,
       const std::string& texture_name,
       const std::string& sampler_name,
       const std::string& coords_name,
-      ast::Type* base_type,
+      const ast::Type* base_type,
       ast::DecorationList decorations);
 
   /// Generates a function that references a specific sampler variable
@@ -286,13 +289,13 @@
   /// @param base_type sampler base type
   /// @param decorations the function decorations
   /// @returns a function that references all of the values specified
-  ast::Function* MakeSamplerReferenceBodyFunction(
+  const ast::Function* MakeSamplerReferenceBodyFunction(
       const std::string& func_name,
       const std::string& texture_name,
       const std::string& sampler_name,
       const std::string& coords_name,
       const std::string& array_index,
-      ast::Type* base_type,
+      const ast::Type* base_type,
       ast::DecorationList decorations);
 
   /// Generates a function that references a specific comparison sampler
@@ -305,33 +308,34 @@
   /// @param base_type sampler base type
   /// @param decorations the function decorations
   /// @returns a function that references all of the values specified
-  ast::Function* MakeComparisonSamplerReferenceBodyFunction(
+  const ast::Function* MakeComparisonSamplerReferenceBodyFunction(
       const std::string& func_name,
       const std::string& texture_name,
       const std::string& sampler_name,
       const std::string& coords_name,
       const std::string& depth_name,
-      ast::Type* base_type,
+      const ast::Type* base_type,
       ast::DecorationList decorations);
 
   /// Gets an appropriate type for the data in a given texture type.
   /// @param sampled_kind type of in the texture
   /// @returns a pointer to a type appropriate for the coord param
-  ast::Type* GetBaseType(ResourceBinding::SampledKind sampled_kind);
+  const ast::Type* GetBaseType(ResourceBinding::SampledKind sampled_kind);
 
   /// Gets an appropriate type for the coords parameter depending the the
   /// dimensionality of the texture being sampled.
   /// @param dim dimensionality of the texture being sampled
   /// @param scalar the scalar type
   /// @returns a pointer to a type appropriate for the coord param
-  ast::Type* GetCoordsType(ast::TextureDimension dim, ast::Type* scalar);
+  const ast::Type* GetCoordsType(ast::TextureDimension dim,
+                                 const ast::Type* scalar);
 
   /// Generates appropriate types for a Read-Only StorageTexture
   /// @param dim the texture dimension of the storage texture
   /// @param format the image format of the storage texture
   /// @returns the storage texture type
-  ast::Type* MakeStorageTextureTypes(ast::TextureDimension dim,
-                                     ast::ImageFormat format);
+  const ast::Type* MakeStorageTextureTypes(ast::TextureDimension dim,
+                                           ast::ImageFormat format);
 
   /// Adds a storage texture variable to the program
   /// @param name the name of the variable
@@ -339,7 +343,7 @@
   /// @param group the binding/group to use for the sampled texture
   /// @param binding the binding57 number to use for the sampled texture
   void AddStorageTexture(const std::string& name,
-                         ast::Type* type,
+                         const ast::Type* type,
                          uint32_t group,
                          uint32_t binding);
 
@@ -349,10 +353,10 @@
   /// @param dim_type type expected by textureDimensons to return
   /// @param decorations the function decorations
   /// @returns a function that references all of the values specified
-  ast::Function* MakeStorageTextureBodyFunction(
+  const ast::Function* MakeStorageTextureBodyFunction(
       const std::string& func_name,
       const std::string& st_name,
-      ast::Type* dim_type,
+      const ast::Type* dim_type,
       ast::DecorationList decorations);
 
   /// Get a generator function that returns a type appropriate for a stage
@@ -360,8 +364,9 @@
   /// @param component component type of the stage variable
   /// @param composition composition type of the stage variable
   /// @returns a generator function for the stage variable's type.
-  std::function<ast::Type*()> GetTypeFunction(ComponentType component,
-                                              CompositionType composition);
+  std::function<const ast::Type*()> GetTypeFunction(
+      ComponentType component,
+      CompositionType composition);
 
   /// Build the Program given all of the previous methods called and return an
   /// Inspector for it.
@@ -370,12 +375,12 @@
   Inspector& Build();
 
   /// @returns the type for a SamplerKind::kSampler
-  ast::Sampler* sampler_type() {
+  const ast::Sampler* sampler_type() {
     return ty.sampler(ast::SamplerKind::kSampler);
   }
 
   /// @returns the type for a SamplerKind::kComparison
-  ast::Sampler* comparison_sampler_type() {
+  const ast::Sampler* comparison_sampler_type() {
     return ty.sampler(ast::SamplerKind::kComparisonSampler);
   }
 
diff --git a/src/intrinsic_table.cc b/src/intrinsic_table.cc
index 0f497ef..d74bb8d 100644
--- a/src/intrinsic_table.cc
+++ b/src/intrinsic_table.cc
@@ -742,7 +742,7 @@
 /// ParameterInfo describes a parameter
 struct ParameterInfo {
   /// The parameter usage (parameter name in definition file)
-  ParameterUsage const usage;
+  const ParameterUsage usage;
 
   /// Pointer to a list of indices that are used to match the parameter type.
   /// The matcher indices index on Matchers::type and / or Matchers::number.
@@ -757,7 +757,7 @@
   const char* name;
   /// Optional type matcher constraint.
   /// Either an index in Matchers::type, or kNoMatcher
-  MatcherIndex const matcher_index;
+  const MatcherIndex matcher_index;
 };
 
 /// OpenNumberInfo describes an open number
@@ -766,17 +766,17 @@
   const char* name;
   /// Optional number matcher constraint.
   /// Either an index in Matchers::number, or kNoMatcher
-  MatcherIndex const matcher_index;
+  const MatcherIndex matcher_index;
 };
 
 /// OverloadInfo describes a single function overload
 struct OverloadInfo {
   /// Total number of parameters for the overload
-  uint8_t const num_parameters;
+  const uint8_t num_parameters;
   /// Total number of open types for the overload
-  uint8_t const num_open_types;
+  const uint8_t num_open_types;
   /// Total number of open numbers for the overload
-  uint8_t const num_open_numbers;
+  const uint8_t num_open_numbers;
   /// Pointer to the first open type
   OpenTypeInfo const* const open_types;
   /// Pointer to the first open number
@@ -796,7 +796,7 @@
 /// IntrinsicInfo describes an intrinsic function
 struct IntrinsicInfo {
   /// Number of overloads of the intrinsic function
-  uint8_t const num_overloads;
+  const uint8_t num_overloads;
   /// Pointer to the start of the overloads for the function
   OverloadInfo const* const overloads;
 };
@@ -809,7 +809,7 @@
   /// Parameter describes a single parameter
   struct Parameter {
     /// Parameter type
-    sem::Type* const type;
+    const sem::Type* const type;
     /// Parameter usage
     ParameterUsage const usage = ParameterUsage::kNone;
   };
@@ -995,8 +995,8 @@
     auto* indices = parameter.matcher_indices;
     auto* type = Match(closed, overload, indices).Type(args[p]->UnwrapRef());
     if (type) {
-      parameters.emplace_back(IntrinsicPrototype::Parameter{
-          const_cast<sem::Type*>(type), parameter.usage});
+      parameters.emplace_back(
+          IntrinsicPrototype::Parameter{type, parameter.usage});
       match_score += kScorePerMatchedParam;
     } else {
       overload_matched = false;
@@ -1070,7 +1070,7 @@
           ast::StorageClass::kNone, ast::Access::kUndefined, p.usage));
     }
     return builder.create<sem::Intrinsic>(
-        intrinsic.type, const_cast<sem::Type*>(intrinsic.return_type),
+        intrinsic.type, intrinsic.return_type,
         std::move(params), intrinsic.supported_stages, intrinsic.is_deprecated);
   });
 }
diff --git a/src/program.cc b/src/program.cc
index c3cd9c1..b838c7d 100644
--- a/src/program.cc
+++ b/src/program.cc
@@ -114,7 +114,7 @@
   return is_valid_;
 }
 
-sem::Type* Program::TypeOf(const ast::Expression* expr) const {
+const sem::Type* Program::TypeOf(const ast::Expression* expr) const {
   auto* sem = Sem().Get(expr);
   return sem ? sem->Type() : nullptr;
 }
diff --git a/src/program.h b/src/program.h
index ad42704..f5ceca7 100644
--- a/src/program.h
+++ b/src/program.h
@@ -149,7 +149,7 @@
   /// @param expr the AST expression
   /// @return the resolved semantic type for the expression, or nullptr if the
   /// expression has no resolved type.
-  sem::Type* TypeOf(const ast::Expression* expr) const;
+  const sem::Type* TypeOf(const ast::Expression* expr) const;
 
   /// Helper for returning the resolved semantic type of the AST type `type`.
   /// @param type the AST type
diff --git a/src/program_builder.cc b/src/program_builder.cc
index f9b6bc6..abcd6f2 100644
--- a/src/program_builder.cc
+++ b/src/program_builder.cc
@@ -89,12 +89,12 @@
   }
 }
 
-sem::Type* ProgramBuilder::TypeOf(const ast::Expression* expr) const {
+const sem::Type* ProgramBuilder::TypeOf(const ast::Expression* expr) const {
   auto* sem = Sem().Get(expr);
   return sem ? sem->Type() : nullptr;
 }
 
-sem::Type* ProgramBuilder::TypeOf(const ast::Variable* var) const {
+const sem::Type* ProgramBuilder::TypeOf(const ast::Variable* var) const {
   auto* sem = Sem().Get(var);
   return sem ? sem->Type() : nullptr;
 }
@@ -107,10 +107,6 @@
   return Sem().Get(type_decl);
 }
 
-ast::TypeName* ProgramBuilder::TypesBuilder::Of(ast::TypeDecl* decl) const {
-  return type_name(decl->name);
-}
-
 const ast::TypeName* ProgramBuilder::TypesBuilder::Of(
     const ast::TypeDecl* decl) const {
   return type_name(decl->name);
@@ -118,11 +114,12 @@
 
 ProgramBuilder::TypesBuilder::TypesBuilder(ProgramBuilder* pb) : builder(pb) {}
 
-ast::Statement* ProgramBuilder::WrapInStatement(ast::Literal* lit) {
+const ast::Statement* ProgramBuilder::WrapInStatement(const ast::Literal* lit) {
   return WrapInStatement(create<ast::ScalarConstructorExpression>(lit));
 }
 
-ast::Statement* ProgramBuilder::WrapInStatement(ast::Expression* expr) {
+const ast::Statement* ProgramBuilder::WrapInStatement(
+    const ast::Expression* expr) {
   if (auto* ce = expr->As<ast::CallExpression>()) {
     return Ignore(ce);
   }
@@ -130,15 +127,18 @@
   return Decl(Const(symbols_.New(), nullptr, expr));
 }
 
-ast::VariableDeclStatement* ProgramBuilder::WrapInStatement(ast::Variable* v) {
+const ast::VariableDeclStatement* ProgramBuilder::WrapInStatement(
+    const ast::Variable* v) {
   return create<ast::VariableDeclStatement>(v);
 }
 
-ast::Statement* ProgramBuilder::WrapInStatement(ast::Statement* stmt) {
+const ast::Statement* ProgramBuilder::WrapInStatement(
+    const ast::Statement* stmt) {
   return stmt;
 }
 
-ast::Function* ProgramBuilder::WrapInFunction(ast::StatementList stmts) {
+const ast::Function* ProgramBuilder::WrapInFunction(
+    const ast::StatementList stmts) {
   return Func("test_function", {}, ty.void_(), std::move(stmts),
               {create<ast::StageDecoration>(ast::PipelineStage::kCompute),
                WorkgroupSize(1, 1, 1)});
diff --git a/src/program_builder.h b/src/program_builder.h
index 39a1fa3..0928b8e 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -127,13 +127,13 @@
 
     ast::StorageClass storage = ast::StorageClass::kNone;
     ast::Access access = ast::Access::kUndefined;
-    ast::Expression* constructor = nullptr;
+    const ast::Expression* constructor = nullptr;
     ast::DecorationList decorations = {};
 
    private:
     void Set(ast::StorageClass sc) { storage = sc; }
     void Set(ast::Access ac) { access = ac; }
-    void Set(ast::Expression* c) { constructor = c; }
+    void Set(const ast::Expression* c) { constructor = c; }
     void Set(const ast::DecorationList& l) { decorations = l; }
 
     template <typename FIRST, typename... ARGS>
@@ -388,59 +388,59 @@
 
     /// @return the tint AST type for the C type `T`.
     template <typename T>
-    ast::Type* Of() const {
+    const ast::Type* Of() const {
       return CToAST<T>::get(this);
     }
 
     /// @returns a boolean type
-    ast::Bool* bool_() const { return builder->create<ast::Bool>(); }
+    const ast::Bool* bool_() const { return builder->create<ast::Bool>(); }
 
     /// @param source the Source of the node
     /// @returns a boolean type
-    ast::Bool* bool_(const Source& source) const {
+    const ast::Bool* bool_(const Source& source) const {
       return builder->create<ast::Bool>(source);
     }
 
     /// @returns a f32 type
-    ast::F32* f32() const { return builder->create<ast::F32>(); }
+    const ast::F32* f32() const { return builder->create<ast::F32>(); }
 
     /// @param source the Source of the node
     /// @returns a f32 type
-    ast::F32* f32(const Source& source) const {
+    const ast::F32* f32(const Source& source) const {
       return builder->create<ast::F32>(source);
     }
 
     /// @returns a i32 type
-    ast::I32* i32() const { return builder->create<ast::I32>(); }
+    const ast::I32* i32() const { return builder->create<ast::I32>(); }
 
     /// @param source the Source of the node
     /// @returns a i32 type
-    ast::I32* i32(const Source& source) const {
+    const ast::I32* i32(const Source& source) const {
       return builder->create<ast::I32>(source);
     }
 
     /// @returns a u32 type
-    ast::U32* u32() const { return builder->create<ast::U32>(); }
+    const ast::U32* u32() const { return builder->create<ast::U32>(); }
 
     /// @param source the Source of the node
     /// @returns a u32 type
-    ast::U32* u32(const Source& source) const {
+    const ast::U32* u32(const Source& source) const {
       return builder->create<ast::U32>(source);
     }
 
     /// @returns a void type
-    ast::Void* void_() const { return builder->create<ast::Void>(); }
+    const ast::Void* void_() const { return builder->create<ast::Void>(); }
 
     /// @param source the Source of the node
     /// @returns a void type
-    ast::Void* void_(const Source& source) const {
+    const ast::Void* void_(const Source& source) const {
       return builder->create<ast::Void>(source);
     }
 
     /// @param type vector subtype
     /// @param n vector width in elements
     /// @return the tint AST type for a `n`-element vector of `type`.
-    ast::Vector* vec(ast::Type* type, uint32_t n) const {
+    const ast::Vector* vec(const ast::Type* type, uint32_t n) const {
       return builder->create<ast::Vector>(type, n);
     }
 
@@ -448,44 +448,52 @@
     /// @param type vector subtype
     /// @param n vector width in elements
     /// @return the tint AST type for a `n`-element vector of `type`.
-    ast::Vector* vec(const Source& source, ast::Type* type, uint32_t n) const {
+    const ast::Vector* vec(const Source& source,
+                           const ast::Type* type,
+                           uint32_t n) const {
       return builder->create<ast::Vector>(source, type, n);
     }
 
     /// @param type vector subtype
     /// @return the tint AST type for a 2-element vector of `type`.
-    ast::Vector* vec2(ast::Type* type) const { return vec(type, 2u); }
+    const ast::Vector* vec2(const ast::Type* type) const {
+      return vec(type, 2u);
+    }
 
     /// @param type vector subtype
     /// @return the tint AST type for a 3-element vector of `type`.
-    ast::Vector* vec3(ast::Type* type) const { return vec(type, 3u); }
+    const ast::Vector* vec3(const ast::Type* type) const {
+      return vec(type, 3u);
+    }
 
     /// @param type vector subtype
     /// @return the tint AST type for a 4-element vector of `type`.
-    ast::Vector* vec4(ast::Type* type) const { return vec(type, 4u); }
+    const ast::Vector* vec4(const ast::Type* type) const {
+      return vec(type, 4u);
+    }
 
     /// @param n vector width in elements
     /// @return the tint AST type for a `n`-element vector of `type`.
     template <typename T>
-    ast::Vector* vec(uint32_t n) const {
+    const ast::Vector* vec(uint32_t n) const {
       return vec(Of<T>(), n);
     }
 
     /// @return the tint AST type for a 2-element vector of the C type `T`.
     template <typename T>
-    ast::Vector* vec2() const {
+    const ast::Vector* vec2() const {
       return vec2(Of<T>());
     }
 
     /// @return the tint AST type for a 3-element vector of the C type `T`.
     template <typename T>
-    ast::Vector* vec3() const {
+    const ast::Vector* vec3() const {
       return vec3(Of<T>());
     }
 
     /// @return the tint AST type for a 4-element vector of the C type `T`.
     template <typename T>
-    ast::Vector* vec4() const {
+    const ast::Vector* vec4() const {
       return vec4(Of<T>());
     }
 
@@ -493,7 +501,9 @@
     /// @param columns number of columns for the matrix
     /// @param rows number of rows for the matrix
     /// @return the tint AST type for a matrix of `type`
-    ast::Matrix* mat(ast::Type* type, uint32_t columns, uint32_t rows) const {
+    const ast::Matrix* mat(const ast::Type* type,
+                           uint32_t columns,
+                           uint32_t rows) const {
       return builder->create<ast::Matrix>(type, rows, columns);
     }
 
@@ -502,108 +512,126 @@
     /// @param columns number of columns for the matrix
     /// @param rows number of rows for the matrix
     /// @return the tint AST type for a matrix of `type`
-    ast::Matrix* mat(const Source& source,
-                     ast::Type* type,
-                     uint32_t columns,
-                     uint32_t rows) const {
+    const ast::Matrix* mat(const Source& source,
+                           const ast::Type* type,
+                           uint32_t columns,
+                           uint32_t rows) const {
       return builder->create<ast::Matrix>(source, type, rows, columns);
     }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 2x3 matrix of `type`.
-    ast::Matrix* mat2x2(ast::Type* type) const { return mat(type, 2u, 2u); }
+    const ast::Matrix* mat2x2(const ast::Type* type) const {
+      return mat(type, 2u, 2u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 2x3 matrix of `type`.
-    ast::Matrix* mat2x3(ast::Type* type) const { return mat(type, 2u, 3u); }
+    const ast::Matrix* mat2x3(const ast::Type* type) const {
+      return mat(type, 2u, 3u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 2x4 matrix of `type`.
-    ast::Matrix* mat2x4(ast::Type* type) const { return mat(type, 2u, 4u); }
+    const ast::Matrix* mat2x4(const ast::Type* type) const {
+      return mat(type, 2u, 4u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 3x2 matrix of `type`.
-    ast::Matrix* mat3x2(ast::Type* type) const { return mat(type, 3u, 2u); }
+    const ast::Matrix* mat3x2(const ast::Type* type) const {
+      return mat(type, 3u, 2u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 3x3 matrix of `type`.
-    ast::Matrix* mat3x3(ast::Type* type) const { return mat(type, 3u, 3u); }
+    const ast::Matrix* mat3x3(const ast::Type* type) const {
+      return mat(type, 3u, 3u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 3x4 matrix of `type`.
-    ast::Matrix* mat3x4(ast::Type* type) const { return mat(type, 3u, 4u); }
+    const ast::Matrix* mat3x4(const ast::Type* type) const {
+      return mat(type, 3u, 4u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 4x2 matrix of `type`.
-    ast::Matrix* mat4x2(ast::Type* type) const { return mat(type, 4u, 2u); }
+    const ast::Matrix* mat4x2(const ast::Type* type) const {
+      return mat(type, 4u, 2u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 4x3 matrix of `type`.
-    ast::Matrix* mat4x3(ast::Type* type) const { return mat(type, 4u, 3u); }
+    const ast::Matrix* mat4x3(const ast::Type* type) const {
+      return mat(type, 4u, 3u);
+    }
 
     /// @param type matrix subtype
     /// @return the tint AST type for a 4x4 matrix of `type`.
-    ast::Matrix* mat4x4(ast::Type* type) const { return mat(type, 4u, 4u); }
+    const ast::Matrix* mat4x4(const ast::Type* type) const {
+      return mat(type, 4u, 4u);
+    }
 
     /// @param columns number of columns for the matrix
     /// @param rows number of rows for the matrix
     /// @return the tint AST type for a matrix of `type`
     template <typename T>
-    ast::Matrix* mat(uint32_t columns, uint32_t rows) const {
+    const ast::Matrix* mat(uint32_t columns, uint32_t rows) const {
       return mat(Of<T>(), columns, rows);
     }
 
     /// @return the tint AST type for a 2x3 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat2x2() const {
+    const ast::Matrix* mat2x2() const {
       return mat2x2(Of<T>());
     }
 
     /// @return the tint AST type for a 2x3 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat2x3() const {
+    const ast::Matrix* mat2x3() const {
       return mat2x3(Of<T>());
     }
 
     /// @return the tint AST type for a 2x4 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat2x4() const {
+    const ast::Matrix* mat2x4() const {
       return mat2x4(Of<T>());
     }
 
     /// @return the tint AST type for a 3x2 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat3x2() const {
+    const ast::Matrix* mat3x2() const {
       return mat3x2(Of<T>());
     }
 
     /// @return the tint AST type for a 3x3 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat3x3() const {
+    const ast::Matrix* mat3x3() const {
       return mat3x3(Of<T>());
     }
 
     /// @return the tint AST type for a 3x4 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat3x4() const {
+    const ast::Matrix* mat3x4() const {
       return mat3x4(Of<T>());
     }
 
     /// @return the tint AST type for a 4x2 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat4x2() const {
+    const ast::Matrix* mat4x2() const {
       return mat4x2(Of<T>());
     }
 
     /// @return the tint AST type for a 4x3 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat4x3() const {
+    const ast::Matrix* mat4x3() const {
       return mat4x3(Of<T>());
     }
 
     /// @return the tint AST type for a 4x4 matrix of the C type `T`.
     template <typename T>
-    ast::Matrix* mat4x4() const {
+    const ast::Matrix* mat4x4() const {
       return mat4x4(Of<T>());
     }
 
@@ -612,9 +640,9 @@
     /// @param decos the optional decorations for the array
     /// @return the tint AST type for a array of size `n` of type `T`
     template <typename EXPR = ast::Expression*>
-    ast::Array* array(ast::Type* subtype,
-                      EXPR&& n = nullptr,
-                      ast::DecorationList decos = {}) const {
+    const ast::Array* array(const ast::Type* subtype,
+                            EXPR&& n = nullptr,
+                            ast::DecorationList decos = {}) const {
       return builder->create<ast::Array>(
           subtype, builder->Expr(std::forward<EXPR>(n)), decos);
     }
@@ -625,10 +653,10 @@
     /// @param decos the optional decorations for the array
     /// @return the tint AST type for a array of size `n` of type `T`
     template <typename EXPR = ast::Expression*>
-    ast::Array* array(const Source& source,
-                      ast::Type* subtype,
-                      EXPR&& n = nullptr,
-                      ast::DecorationList decos = {}) const {
+    const ast::Array* array(const Source& source,
+                            const ast::Type* subtype,
+                            EXPR&& n = nullptr,
+                            ast::DecorationList decos = {}) const {
       return builder->create<ast::Array>(
           source, subtype, builder->Expr(std::forward<EXPR>(n)), decos);
     }
@@ -638,7 +666,9 @@
     /// @param stride the array stride. 0 represents implicit stride
     /// @return the tint AST type for a array of size `n` of type `T`
     template <typename EXPR>
-    ast::Array* array(ast::Type* subtype, EXPR&& n, uint32_t stride) const {
+    const ast::Array* array(const ast::Type* subtype,
+                            EXPR&& n,
+                            uint32_t stride) const {
       ast::DecorationList decos;
       if (stride) {
         decos.emplace_back(builder->create<ast::StrideDecoration>(stride));
@@ -652,10 +682,10 @@
     /// @param stride the array stride. 0 represents implicit stride
     /// @return the tint AST type for a array of size `n` of type `T`
     template <typename EXPR>
-    ast::Array* array(const Source& source,
-                      ast::Type* subtype,
-                      EXPR&& n,
-                      uint32_t stride) const {
+    const ast::Array* array(const Source& source,
+                            const ast::Type* subtype,
+                            EXPR&& n,
+                            uint32_t stride) const {
       ast::DecorationList decos;
       if (stride) {
         decos.emplace_back(builder->create<ast::StrideDecoration>(stride));
@@ -665,27 +695,27 @@
 
     /// @return the tint AST type for a runtime-sized array of type `T`
     template <typename T>
-    ast::Array* array() const {
+    const ast::Array* array() const {
       return array(Of<T>(), nullptr);
     }
 
     /// @return the tint AST type for an array of size `N` of type `T`
     template <typename T, int N>
-    ast::Array* array() const {
+    const ast::Array* array() const {
       return array(Of<T>(), builder->Expr(N));
     }
 
     /// @param stride the array stride
     /// @return the tint AST type for a runtime-sized array of type `T`
     template <typename T>
-    ast::Array* array(uint32_t stride) const {
+    const ast::Array* array(uint32_t stride) const {
       return array(Of<T>(), nullptr, stride);
     }
 
     /// @param stride the array stride
     /// @return the tint AST type for an array of size `N` of type `T`
     template <typename T, int N>
-    ast::Array* array(uint32_t stride) const {
+    const ast::Array* array(uint32_t stride) const {
       return array(Of<T>(), builder->Expr(N), stride);
     }
 
@@ -693,7 +723,7 @@
     /// @param name the name
     /// @returns the type name
     template <typename NAME>
-    ast::TypeName* type_name(NAME&& name) const {
+    const ast::TypeName* type_name(NAME&& name) const {
       return builder->create<ast::TypeName>(
           builder->Sym(std::forward<NAME>(name)));
     }
@@ -703,7 +733,7 @@
     /// @param name the name
     /// @returns the type name
     template <typename NAME>
-    ast::TypeName* type_name(const Source& source, NAME&& name) const {
+    const ast::TypeName* type_name(const Source& source, NAME&& name) const {
       return builder->create<ast::TypeName>(
           source, builder->Sym(std::forward<NAME>(name)));
     }
@@ -713,7 +743,7 @@
     /// @param type the alias type
     /// @returns the alias pointer
     template <typename NAME>
-    ast::Alias* alias(NAME&& name, ast::Type* type) const {
+    const ast::Alias* alias(NAME&& name, const ast::Type* type) const {
       auto sym = builder->Sym(std::forward<NAME>(name));
       return builder->create<ast::Alias>(sym, type);
     }
@@ -724,9 +754,9 @@
     /// @param type the alias type
     /// @returns the alias pointer
     template <typename NAME>
-    ast::Alias* alias(const Source& source,
-                      NAME&& name,
-                      ast::Type* type) const {
+    const ast::Alias* alias(const Source& source,
+                            NAME&& name,
+                            const ast::Type* type) const {
       auto sym = builder->Sym(std::forward<NAME>(name));
       return builder->create<ast::Alias>(source, sym, type);
     }
@@ -735,9 +765,10 @@
     /// @param storage_class the storage class of the pointer
     /// @param access the optional access control of the pointer
     /// @return the pointer to `type` with the given ast::StorageClass
-    ast::Pointer* pointer(ast::Type* type,
-                          ast::StorageClass storage_class,
-                          ast::Access access = ast::Access::kUndefined) const {
+    const ast::Pointer* pointer(
+        const ast::Type* type,
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
       return builder->create<ast::Pointer>(type, storage_class, access);
     }
 
@@ -746,10 +777,11 @@
     /// @param storage_class the storage class of the pointer
     /// @param access the optional access control of the pointer
     /// @return the pointer to `type` with the given ast::StorageClass
-    ast::Pointer* pointer(const Source& source,
-                          ast::Type* type,
-                          ast::StorageClass storage_class,
-                          ast::Access access = ast::Access::kUndefined) const {
+    const ast::Pointer* pointer(
+        const Source& source,
+        const ast::Type* type,
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
       return builder->create<ast::Pointer>(source, type, storage_class, access);
     }
 
@@ -757,60 +789,63 @@
     /// @param access the optional access control of the pointer
     /// @return the pointer to type `T` with the given ast::StorageClass.
     template <typename T>
-    ast::Pointer* pointer(ast::StorageClass storage_class,
-                          ast::Access access = ast::Access::kUndefined) const {
+    const ast::Pointer* pointer(
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
       return pointer(Of<T>(), storage_class, access);
     }
 
     /// @param source the Source of the node
     /// @param type the type of the atomic
     /// @return the atomic to `type`
-    ast::Atomic* atomic(const Source& source, ast::Type* type) const {
+    const ast::Atomic* atomic(const Source& source,
+                              const ast::Type* type) const {
       return builder->create<ast::Atomic>(source, type);
     }
 
     /// @param type the type of the atomic
     /// @return the atomic to `type`
-    ast::Atomic* atomic(ast::Type* type) const {
+    const ast::Atomic* atomic(const ast::Type* type) const {
       return builder->create<ast::Atomic>(type);
     }
 
     /// @return the atomic to type `T`
     template <typename T>
-    ast::Atomic* atomic() const {
+    const ast::Atomic* atomic() const {
       return atomic(Of<T>());
     }
 
     /// @param kind the kind of sampler
     /// @returns the sampler
-    ast::Sampler* sampler(ast::SamplerKind kind) const {
+    const ast::Sampler* sampler(ast::SamplerKind kind) const {
       return builder->create<ast::Sampler>(kind);
     }
 
     /// @param source the Source of the node
     /// @param kind the kind of sampler
     /// @returns the sampler
-    ast::Sampler* sampler(const Source& source, ast::SamplerKind kind) const {
+    const ast::Sampler* sampler(const Source& source,
+                                ast::SamplerKind kind) const {
       return builder->create<ast::Sampler>(source, kind);
     }
 
     /// @param dims the dimensionality of the texture
     /// @returns the depth texture
-    ast::DepthTexture* depth_texture(ast::TextureDimension dims) const {
+    const ast::DepthTexture* depth_texture(ast::TextureDimension dims) const {
       return builder->create<ast::DepthTexture>(dims);
     }
 
     /// @param source the Source of the node
     /// @param dims the dimensionality of the texture
     /// @returns the depth texture
-    ast::DepthTexture* depth_texture(const Source& source,
-                                     ast::TextureDimension dims) const {
+    const ast::DepthTexture* depth_texture(const Source& source,
+                                           ast::TextureDimension dims) const {
       return builder->create<ast::DepthTexture>(source, dims);
     }
 
     /// @param dims the dimensionality of the texture
     /// @returns the multisampled depth texture
-    ast::DepthMultisampledTexture* depth_multisampled_texture(
+    const ast::DepthMultisampledTexture* depth_multisampled_texture(
         ast::TextureDimension dims) const {
       return builder->create<ast::DepthMultisampledTexture>(dims);
     }
@@ -818,7 +853,7 @@
     /// @param source the Source of the node
     /// @param dims the dimensionality of the texture
     /// @returns the multisampled depth texture
-    ast::DepthMultisampledTexture* depth_multisampled_texture(
+    const ast::DepthMultisampledTexture* depth_multisampled_texture(
         const Source& source,
         ast::TextureDimension dims) const {
       return builder->create<ast::DepthMultisampledTexture>(source, dims);
@@ -827,8 +862,8 @@
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the sampled texture
-    ast::SampledTexture* sampled_texture(ast::TextureDimension dims,
-                                         ast::Type* subtype) const {
+    const ast::SampledTexture* sampled_texture(ast::TextureDimension dims,
+                                               const ast::Type* subtype) const {
       return builder->create<ast::SampledTexture>(dims, subtype);
     }
 
@@ -836,17 +871,18 @@
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the sampled texture
-    ast::SampledTexture* sampled_texture(const Source& source,
-                                         ast::TextureDimension dims,
-                                         ast::Type* subtype) const {
+    const ast::SampledTexture* sampled_texture(const Source& source,
+                                               ast::TextureDimension dims,
+                                               const ast::Type* subtype) const {
       return builder->create<ast::SampledTexture>(source, dims, subtype);
     }
 
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the multisampled texture
-    ast::MultisampledTexture* multisampled_texture(ast::TextureDimension dims,
-                                                   ast::Type* subtype) const {
+    const ast::MultisampledTexture* multisampled_texture(
+        ast::TextureDimension dims,
+        const ast::Type* subtype) const {
       return builder->create<ast::MultisampledTexture>(dims, subtype);
     }
 
@@ -854,9 +890,10 @@
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the multisampled texture
-    ast::MultisampledTexture* multisampled_texture(const Source& source,
-                                                   ast::TextureDimension dims,
-                                                   ast::Type* subtype) const {
+    const ast::MultisampledTexture* multisampled_texture(
+        const Source& source,
+        ast::TextureDimension dims,
+        const ast::Type* subtype) const {
       return builder->create<ast::MultisampledTexture>(source, dims, subtype);
     }
 
@@ -864,9 +901,9 @@
     /// @param format the image format of the texture
     /// @param access the access control of the texture
     /// @returns the storage texture
-    ast::StorageTexture* storage_texture(ast::TextureDimension dims,
-                                         ast::ImageFormat format,
-                                         ast::Access access) const {
+    const ast::StorageTexture* storage_texture(ast::TextureDimension dims,
+                                               ast::ImageFormat format,
+                                               ast::Access access) const {
       auto* subtype = ast::StorageTexture::SubtypeFor(format, *builder);
       return builder->create<ast::StorageTexture>(dims, format, subtype,
                                                   access);
@@ -877,32 +914,26 @@
     /// @param format the image format of the texture
     /// @param access the access control of the texture
     /// @returns the storage texture
-    ast::StorageTexture* storage_texture(const Source& source,
-                                         ast::TextureDimension dims,
-                                         ast::ImageFormat format,
-                                         ast::Access access) const {
+    const ast::StorageTexture* storage_texture(const Source& source,
+                                               ast::TextureDimension dims,
+                                               ast::ImageFormat format,
+                                               ast::Access access) const {
       auto* subtype = ast::StorageTexture::SubtypeFor(format, *builder);
       return builder->create<ast::StorageTexture>(source, dims, format, subtype,
                                                   access);
     }
 
     /// @returns the external texture
-    ast::ExternalTexture* external_texture() const {
+    const ast::ExternalTexture* external_texture() const {
       return builder->create<ast::ExternalTexture>();
     }
 
     /// @param source the Source of the node
     /// @returns the external texture
-    ast::ExternalTexture* external_texture(const Source& source) const {
+    const ast::ExternalTexture* external_texture(const Source& source) const {
       return builder->create<ast::ExternalTexture>(source);
     }
 
-    /// [DEPRECATED]: TODO(crbug.com/tint/745): Migrate to const AST pointers.
-    /// Constructs a TypeName for the type declaration.
-    /// @param type the type
-    /// @return either type or a pointer to a new ast::TypeName
-    ast::TypeName* Of(ast::TypeDecl* type) const;
-
     /// Constructs a TypeName for the type declaration.
     /// @param type the type
     /// @return either type or a pointer to a new ast::TypeName
@@ -916,7 +947,7 @@
     /// contains a single static `get()` method for obtaining the corresponding
     /// AST type for the C type `T`.
     /// `get()` has the signature:
-    ///    `static ast::Type* get(Types* t)`
+    ///    `static const ast::Type* get(Types* t)`
     template <typename T>
     struct CToAST {};
   };
@@ -945,111 +976,116 @@
 
   /// Passthrough for nullptr
   /// @return nullptr
-  ast::IdentifierExpression* Expr(std::nullptr_t) { return nullptr; }
+  const ast::IdentifierExpression* Expr(std::nullptr_t) { return nullptr; }
 
   /// @param source the source information
   /// @param symbol the identifier symbol
   /// @return an ast::IdentifierExpression with the given symbol
-  ast::IdentifierExpression* Expr(const Source& source, Symbol symbol) {
+  const ast::IdentifierExpression* Expr(const Source& source, Symbol symbol) {
     return create<ast::IdentifierExpression>(source, symbol);
   }
 
   /// @param symbol the identifier symbol
   /// @return an ast::IdentifierExpression with the given symbol
-  ast::IdentifierExpression* Expr(Symbol symbol) {
+  const ast::IdentifierExpression* Expr(Symbol symbol) {
     return create<ast::IdentifierExpression>(symbol);
   }
 
   /// @param source the source information
   /// @param variable the AST variable
   /// @return an ast::IdentifierExpression with the variable's symbol
-  ast::IdentifierExpression* Expr(const Source& source,
-                                  ast::Variable* variable) {
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const ast::Variable* variable) {
     return create<ast::IdentifierExpression>(source, variable->symbol);
   }
 
   /// @param variable the AST variable
   /// @return an ast::IdentifierExpression with the variable's symbol
-  ast::IdentifierExpression* Expr(ast::Variable* variable) {
+  const ast::IdentifierExpression* Expr(const ast::Variable* variable) {
     return create<ast::IdentifierExpression>(variable->symbol);
   }
 
   /// @param source the source information
   /// @param name the identifier name
   /// @return an ast::IdentifierExpression with the given name
-  ast::IdentifierExpression* Expr(const Source& source, const char* name) {
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const char* name) {
     return create<ast::IdentifierExpression>(source, Symbols().Register(name));
   }
 
   /// @param name the identifier name
   /// @return an ast::IdentifierExpression with the given name
-  ast::IdentifierExpression* Expr(const char* name) {
+  const ast::IdentifierExpression* Expr(const char* name) {
     return create<ast::IdentifierExpression>(Symbols().Register(name));
   }
 
   /// @param source the source information
   /// @param name the identifier name
   /// @return an ast::IdentifierExpression with the given name
-  ast::IdentifierExpression* Expr(const Source& source,
-                                  const std::string& name) {
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const std::string& name) {
     return create<ast::IdentifierExpression>(source, Symbols().Register(name));
   }
 
   /// @param name the identifier name
   /// @return an ast::IdentifierExpression with the given name
-  ast::IdentifierExpression* Expr(const std::string& name) {
+  const ast::IdentifierExpression* Expr(const std::string& name) {
     return create<ast::IdentifierExpression>(Symbols().Register(name));
   }
 
   /// @param source the source information
   /// @param value the boolean value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(const Source& source, bool value) {
+  const ast::ScalarConstructorExpression* Expr(const Source& source,
+                                               bool value) {
     return create<ast::ScalarConstructorExpression>(source, Literal(value));
   }
 
   /// @param value the boolean value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(bool value) {
+  const ast::ScalarConstructorExpression* Expr(bool value) {
     return create<ast::ScalarConstructorExpression>(Literal(value));
   }
 
   /// @param source the source information
   /// @param value the float value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(const Source& source, f32 value) {
+  const ast::ScalarConstructorExpression* Expr(const Source& source,
+                                               f32 value) {
     return create<ast::ScalarConstructorExpression>(source, Literal(value));
   }
 
   /// @param value the float value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(f32 value) {
+  const ast::ScalarConstructorExpression* Expr(f32 value) {
     return create<ast::ScalarConstructorExpression>(Literal(value));
   }
 
   /// @param source the source information
   /// @param value the integer value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(const Source& source, i32 value) {
+  const ast::ScalarConstructorExpression* Expr(const Source& source,
+                                               i32 value) {
     return create<ast::ScalarConstructorExpression>(source, Literal(value));
   }
 
   /// @param value the integer value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(i32 value) {
+  const ast::ScalarConstructorExpression* Expr(i32 value) {
     return create<ast::ScalarConstructorExpression>(Literal(value));
   }
 
   /// @param source the source information
   /// @param value the unsigned int value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(const Source& source, u32 value) {
+  const ast::ScalarConstructorExpression* Expr(const Source& source,
+                                               u32 value) {
     return create<ast::ScalarConstructorExpression>(source, Literal(value));
   }
 
   /// @param value the unsigned int value
   /// @return a Scalar constructor for the given value
-  ast::ScalarConstructorExpression* Expr(u32 value) {
+  const ast::ScalarConstructorExpression* Expr(u32 value) {
     return create<ast::ScalarConstructorExpression>(Literal(value));
   }
 
@@ -1094,52 +1130,60 @@
   /// @param source the source location for the literal
   /// @param val the boolan value
   /// @return a boolean literal with the given value
-  ast::BoolLiteral* Literal(const Source& source, bool val) {
+  const ast::BoolLiteral* Literal(const Source& source, bool val) {
     return create<ast::BoolLiteral>(source, val);
   }
 
   /// @param val the boolan value
   /// @return a boolean literal with the given value
-  ast::BoolLiteral* Literal(bool val) { return create<ast::BoolLiteral>(val); }
+  const ast::BoolLiteral* Literal(bool val) {
+    return create<ast::BoolLiteral>(val);
+  }
 
   /// @param source the source location for the literal
   /// @param val the float value
   /// @return a float literal with the given value
-  ast::FloatLiteral* Literal(const Source& source, f32 val) {
+  const ast::FloatLiteral* Literal(const Source& source, f32 val) {
     return create<ast::FloatLiteral>(source, val);
   }
 
   /// @param val the float value
   /// @return a float literal with the given value
-  ast::FloatLiteral* Literal(f32 val) { return create<ast::FloatLiteral>(val); }
+  const ast::FloatLiteral* Literal(f32 val) {
+    return create<ast::FloatLiteral>(val);
+  }
 
   /// @param source the source location for the literal
   /// @param val the unsigned int value
   /// @return a ast::UintLiteral with the given value
-  ast::UintLiteral* Literal(const Source& source, u32 val) {
+  const ast::UintLiteral* Literal(const Source& source, u32 val) {
     return create<ast::UintLiteral>(source, val);
   }
 
   /// @param val the unsigned int value
   /// @return a ast::UintLiteral with the given value
-  ast::UintLiteral* Literal(u32 val) { return create<ast::UintLiteral>(val); }
+  const ast::UintLiteral* Literal(u32 val) {
+    return create<ast::UintLiteral>(val);
+  }
 
   /// @param source the source location for the literal
   /// @param val the integer value
   /// @return the ast::SintLiteral with the given value
-  ast::SintLiteral* Literal(const Source& source, i32 val) {
+  const ast::SintLiteral* Literal(const Source& source, i32 val) {
     return create<ast::SintLiteral>(source, val);
   }
 
   /// @param val the integer value
   /// @return the ast::SintLiteral with the given value
-  ast::SintLiteral* Literal(i32 val) { return create<ast::SintLiteral>(val); }
+  const ast::SintLiteral* Literal(i32 val) {
+    return create<ast::SintLiteral>(val);
+  }
 
   /// @param args the arguments for the type constructor
   /// @return an `ast::TypeConstructorExpression` of type `ty`, with the values
   /// of `args` converted to `ast::Expression`s using `Expr()`
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* Construct(ARGS&&... args) {
+  const ast::TypeConstructorExpression* Construct(ARGS&&... args) {
     return Construct(ty.Of<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1148,7 +1192,8 @@
   /// @return an `ast::TypeConstructorExpression` of `type` constructed with the
   /// values `args`.
   template <typename... ARGS>
-  ast::TypeConstructorExpression* Construct(ast::Type* type, ARGS&&... args) {
+  const ast::TypeConstructorExpression* Construct(const ast::Type* type,
+                                                  ARGS&&... args) {
     return create<ast::TypeConstructorExpression>(
         type, ExprList(std::forward<ARGS>(args)...));
   }
@@ -1159,9 +1204,9 @@
   /// @return an `ast::TypeConstructorExpression` of `type` constructed with the
   /// values `args`.
   template <typename... ARGS>
-  ast::TypeConstructorExpression* Construct(const Source& source,
-                                            ast::Type* type,
-                                            ARGS&&... args) {
+  const ast::TypeConstructorExpression* Construct(const Source& source,
+                                                  const ast::Type* type,
+                                                  ARGS&&... args) {
     return create<ast::TypeConstructorExpression>(
         source, type, ExprList(std::forward<ARGS>(args)...));
   }
@@ -1170,7 +1215,7 @@
   /// @return an `ast::BitcastExpression` of type `ty`, with the values of
   /// `expr` converted to `ast::Expression`s using `Expr()`
   template <typename T, typename EXPR>
-  ast::BitcastExpression* Bitcast(EXPR&& expr) {
+  const ast::BitcastExpression* Bitcast(EXPR&& expr) {
     return Bitcast(ty.Of<T>(), std::forward<EXPR>(expr));
   }
 
@@ -1179,7 +1224,7 @@
   /// @return an `ast::BitcastExpression` of `type` constructed with the values
   /// `expr`.
   template <typename EXPR>
-  ast::BitcastExpression* Bitcast(ast::Type* type, EXPR&& expr) {
+  const ast::BitcastExpression* Bitcast(const ast::Type* type, EXPR&& expr) {
     return create<ast::BitcastExpression>(type, Expr(std::forward<EXPR>(expr)));
   }
 
@@ -1189,9 +1234,9 @@
   /// @return an `ast::BitcastExpression` of `type` constructed with the values
   /// `expr`.
   template <typename EXPR>
-  ast::BitcastExpression* Bitcast(const Source& source,
-                                  ast::Type* type,
-                                  EXPR&& expr) {
+  const ast::BitcastExpression* Bitcast(const Source& source,
+                                        const ast::Type* type,
+                                        EXPR&& expr) {
     return create<ast::BitcastExpression>(source, type,
                                           Expr(std::forward<EXPR>(expr)));
   }
@@ -1202,9 +1247,9 @@
   /// @return an `ast::TypeConstructorExpression` of a `size`-element vector of
   /// type `type`, constructed with the values `args`.
   template <typename... ARGS>
-  ast::TypeConstructorExpression* vec(ast::Type* type,
-                                      uint32_t size,
-                                      ARGS&&... args) {
+  const ast::TypeConstructorExpression* vec(const ast::Type* type,
+                                            uint32_t size,
+                                            ARGS&&... args) {
     return Construct(ty.vec(type, size), std::forward<ARGS>(args)...);
   }
 
@@ -1212,7 +1257,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 2-element vector of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* vec2(ARGS&&... args) {
+  const ast::TypeConstructorExpression* vec2(ARGS&&... args) {
     return Construct(ty.vec2<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1220,7 +1265,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 3-element vector of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* vec3(ARGS&&... args) {
+  const ast::TypeConstructorExpression* vec3(ARGS&&... args) {
     return Construct(ty.vec3<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1228,7 +1273,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 4-element vector of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* vec4(ARGS&&... args) {
+  const ast::TypeConstructorExpression* vec4(ARGS&&... args) {
     return Construct(ty.vec4<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1236,7 +1281,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 2x2 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat2x2(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat2x2(ARGS&&... args) {
     return Construct(ty.mat2x2<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1244,7 +1289,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 2x3 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat2x3(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat2x3(ARGS&&... args) {
     return Construct(ty.mat2x3<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1252,7 +1297,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 2x4 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat2x4(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat2x4(ARGS&&... args) {
     return Construct(ty.mat2x4<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1260,7 +1305,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 3x2 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat3x2(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat3x2(ARGS&&... args) {
     return Construct(ty.mat3x2<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1268,7 +1313,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 3x3 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat3x3(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat3x3(ARGS&&... args) {
     return Construct(ty.mat3x3<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1276,7 +1321,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 3x4 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat3x4(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat3x4(ARGS&&... args) {
     return Construct(ty.mat3x4<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1284,7 +1329,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 4x2 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat4x2(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat4x2(ARGS&&... args) {
     return Construct(ty.mat4x2<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1292,7 +1337,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 4x3 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat4x3(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat4x3(ARGS&&... args) {
     return Construct(ty.mat4x3<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1300,7 +1345,7 @@
   /// @return an `ast::TypeConstructorExpression` of a 4x4 matrix of type
   /// `T`, constructed with the values `args`.
   template <typename T, typename... ARGS>
-  ast::TypeConstructorExpression* mat4x4(ARGS&&... args) {
+  const ast::TypeConstructorExpression* mat4x4(ARGS&&... args) {
     return Construct(ty.mat4x4<T>(), std::forward<ARGS>(args)...);
   }
 
@@ -1308,7 +1353,7 @@
   /// @return an `ast::TypeConstructorExpression` of an array with element type
   /// `T` and size `N`, constructed with the values `args`.
   template <typename T, int N, typename... ARGS>
-  ast::TypeConstructorExpression* array(ARGS&&... args) {
+  const ast::TypeConstructorExpression* array(ARGS&&... args) {
     return Construct(ty.array<T, N>(), std::forward<ARGS>(args)...);
   }
 
@@ -1318,9 +1363,9 @@
   /// @return an `ast::TypeConstructorExpression` of an array with element type
   /// `subtype`, constructed with the values `args`.
   template <typename EXPR, typename... ARGS>
-  ast::TypeConstructorExpression* array(ast::Type* subtype,
-                                        EXPR&& n,
-                                        ARGS&&... args) {
+  const ast::TypeConstructorExpression* array(const ast::Type* subtype,
+                                              EXPR&& n,
+                                              ARGS&&... args) {
     return Construct(ty.array(subtype, std::forward<EXPR>(n)),
                      std::forward<ARGS>(args)...);
   }
@@ -1338,9 +1383,9 @@
   /// @returns a `ast::Variable` with the given name, type and additional
   /// options
   template <typename NAME, typename... OPTIONAL>
-  ast::Variable* Var(NAME&& name,
-                     const ast::Type* type,
-                     OPTIONAL&&... optional) {
+  const ast::Variable* Var(NAME&& name,
+                           const ast::Type* type,
+                           OPTIONAL&&... optional) {
     VarOptionals opts(std::forward<OPTIONAL>(optional)...);
     return create<ast::Variable>(Sym(std::forward<NAME>(name)), opts.storage,
                                  opts.access, type, false, opts.constructor,
@@ -1360,10 +1405,10 @@
   /// value.
   /// @returns a `ast::Variable` with the given name, storage and type
   template <typename NAME, typename... OPTIONAL>
-  ast::Variable* Var(const Source& source,
-                     NAME&& name,
-                     const ast::Type* type,
-                     OPTIONAL&&... optional) {
+  const ast::Variable* Var(const Source& source,
+                           NAME&& name,
+                           const ast::Type* type,
+                           OPTIONAL&&... optional) {
     VarOptionals opts(std::forward<OPTIONAL>(optional)...);
     return create<ast::Variable>(source, Sym(std::forward<NAME>(name)),
                                  opts.storage, opts.access, type, false,
@@ -1376,10 +1421,10 @@
   /// @param decorations optional variable decorations
   /// @returns a constant `ast::Variable` with the given name and type
   template <typename NAME>
-  ast::Variable* Const(NAME&& name,
-                       ast::Type* type,
-                       ast::Expression* constructor,
-                       ast::DecorationList decorations = {}) {
+  const ast::Variable* Const(NAME&& name,
+                             const ast::Type* type,
+                             const ast::Expression* constructor,
+                             ast::DecorationList decorations = {}) {
     return create<ast::Variable>(
         Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
         ast::Access::kUndefined, type, true, constructor, decorations);
@@ -1392,11 +1437,11 @@
   /// @param decorations optional variable decorations
   /// @returns a constant `ast::Variable` with the given name and type
   template <typename NAME>
-  ast::Variable* Const(const Source& source,
-                       NAME&& name,
-                       ast::Type* type,
-                       ast::Expression* constructor,
-                       ast::DecorationList decorations = {}) {
+  const ast::Variable* Const(const Source& source,
+                             NAME&& name,
+                             const ast::Type* type,
+                             const ast::Expression* constructor,
+                             ast::DecorationList decorations = {}) {
     return create<ast::Variable>(
         source, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
         ast::Access::kUndefined, type, true, constructor, decorations);
@@ -1407,9 +1452,9 @@
   /// @param decorations optional parameter decorations
   /// @returns a constant `ast::Variable` with the given name and type
   template <typename NAME>
-  ast::Variable* Param(NAME&& name,
-                       ast::Type* type,
-                       ast::DecorationList decorations = {}) {
+  const ast::Variable* Param(NAME&& name,
+                             const ast::Type* type,
+                             ast::DecorationList decorations = {}) {
     return create<ast::Variable>(
         Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
         ast::Access::kUndefined, type, true, nullptr, decorations);
@@ -1421,10 +1466,10 @@
   /// @param decorations optional parameter decorations
   /// @returns a constant `ast::Variable` with the given name and type
   template <typename NAME>
-  ast::Variable* Param(const Source& source,
-                       NAME&& name,
-                       ast::Type* type,
-                       ast::DecorationList decorations = {}) {
+  const ast::Variable* Param(const Source& source,
+                             NAME&& name,
+                             const ast::Type* type,
+                             ast::DecorationList decorations = {}) {
     return create<ast::Variable>(
         source, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
         ast::Access::kUndefined, type, true, nullptr, decorations);
@@ -1445,9 +1490,9 @@
   template <typename NAME,
             typename... OPTIONAL,
             typename = DisableIfSource<NAME>>
-  ast::Variable* Global(NAME&& name,
-                        const ast::Type* type,
-                        OPTIONAL&&... optional) {
+  const ast::Variable* Global(NAME&& name,
+                              const ast::Type* type,
+                              OPTIONAL&&... optional) {
     auto* var = Var(std::forward<NAME>(name), type,
                     std::forward<OPTIONAL>(optional)...);
     AST().AddGlobalVariable(var);
@@ -1468,10 +1513,10 @@
   /// @returns a new `ast::Variable`, which is automatically registered as a
   /// global variable with the ast::Module.
   template <typename NAME, typename... OPTIONAL>
-  ast::Variable* Global(const Source& source,
-                        NAME&& name,
-                        ast::Type* type,
-                        OPTIONAL&&... optional) {
+  const ast::Variable* Global(const Source& source,
+                              NAME&& name,
+                              const ast::Type* type,
+                              OPTIONAL&&... optional) {
     auto* var = Var(source, std::forward<NAME>(name), type,
                     std::forward<OPTIONAL>(optional)...);
     AST().AddGlobalVariable(var);
@@ -1486,10 +1531,10 @@
   /// arguments of `args`, which is automatically registered as a global
   /// variable with the ast::Module.
   template <typename NAME>
-  ast::Variable* GlobalConst(NAME&& name,
-                             ast::Type* type,
-                             ast::Expression* constructor,
-                             ast::DecorationList decorations = {}) {
+  const ast::Variable* GlobalConst(NAME&& name,
+                                   const ast::Type* type,
+                                   const ast::Expression* constructor,
+                                   ast::DecorationList decorations = {}) {
     auto* var = Const(std::forward<NAME>(name), type, constructor,
                       std::move(decorations));
     AST().AddGlobalVariable(var);
@@ -1505,11 +1550,11 @@
   /// arguments of `args`, which is automatically registered as a global
   /// variable with the ast::Module.
   template <typename NAME>
-  ast::Variable* GlobalConst(const Source& source,
-                             NAME&& name,
-                             ast::Type* type,
-                             ast::Expression* constructor,
-                             ast::DecorationList decorations = {}) {
+  const ast::Variable* GlobalConst(const Source& source,
+                                   NAME&& name,
+                                   const ast::Type* type,
+                                   const ast::Expression* constructor,
+                                   ast::DecorationList decorations = {}) {
     auto* var = Const(source, std::forward<NAME>(name), type, constructor,
                       std::move(decorations));
     AST().AddGlobalVariable(var);
@@ -1520,7 +1565,7 @@
   /// @param expr the expression to take the address of
   /// @return an ast::UnaryOpExpression that takes the address of `expr`
   template <typename EXPR>
-  ast::UnaryOpExpression* AddressOf(const Source& source, EXPR&& expr) {
+  const ast::UnaryOpExpression* AddressOf(const Source& source, EXPR&& expr) {
     return create<ast::UnaryOpExpression>(source, ast::UnaryOp::kAddressOf,
                                           Expr(std::forward<EXPR>(expr)));
   }
@@ -1528,7 +1573,7 @@
   /// @param expr the expression to take the address of
   /// @return an ast::UnaryOpExpression that takes the address of `expr`
   template <typename EXPR>
-  ast::UnaryOpExpression* AddressOf(EXPR&& expr) {
+  const ast::UnaryOpExpression* AddressOf(EXPR&& expr) {
     return create<ast::UnaryOpExpression>(ast::UnaryOp::kAddressOf,
                                           Expr(std::forward<EXPR>(expr)));
   }
@@ -1537,7 +1582,7 @@
   /// @param expr the expression to perform an indirection on
   /// @return an ast::UnaryOpExpression that dereferences the pointer `expr`
   template <typename EXPR>
-  ast::UnaryOpExpression* Deref(const Source& source, EXPR&& expr) {
+  const ast::UnaryOpExpression* Deref(const Source& source, EXPR&& expr) {
     return create<ast::UnaryOpExpression>(source, ast::UnaryOp::kIndirection,
                                           Expr(std::forward<EXPR>(expr)));
   }
@@ -1545,7 +1590,7 @@
   /// @param expr the expression to perform an indirection on
   /// @return an ast::UnaryOpExpression that dereferences the pointer `expr`
   template <typename EXPR>
-  ast::UnaryOpExpression* Deref(EXPR&& expr) {
+  const ast::UnaryOpExpression* Deref(EXPR&& expr) {
     return create<ast::UnaryOpExpression>(ast::UnaryOp::kIndirection,
                                           Expr(std::forward<EXPR>(expr)));
   }
@@ -1556,7 +1601,9 @@
   /// @returns a `ast::CallExpression` to the function `func`, with the
   /// arguments of `args` converted to `ast::Expression`s using `Expr()`.
   template <typename NAME, typename... ARGS>
-  ast::CallExpression* Call(const Source& source, NAME&& func, ARGS&&... args) {
+  const ast::CallExpression* Call(const Source& source,
+                                  NAME&& func,
+                                  ARGS&&... args) {
     return create<ast::CallExpression>(source, Expr(func),
                                        ExprList(std::forward<ARGS>(args)...));
   }
@@ -1566,7 +1613,7 @@
   /// @returns a `ast::CallExpression` to the function `func`, with the
   /// arguments of `args` converted to `ast::Expression`s using `Expr()`.
   template <typename NAME, typename... ARGS, typename = DisableIfSource<NAME>>
-  ast::CallExpression* Call(NAME&& func, ARGS&&... args) {
+  const ast::CallExpression* Call(NAME&& func, ARGS&&... args) {
     return create<ast::CallExpression>(Expr(func),
                                        ExprList(std::forward<ARGS>(args)...));
   }
@@ -1575,7 +1622,7 @@
   /// @returns a `ast::CallStatement` that calls the `ignore` intrinsic which is
   /// passed the single `expr` argument
   template <typename EXPR>
-  ast::CallStatement* Ignore(EXPR&& expr) {
+  const ast::CallStatement* Ignore(EXPR&& expr) {
     return create<ast::CallStatement>(Call("ignore", Expr(expr)));
   }
 
@@ -1583,7 +1630,7 @@
   /// @param rhs the right hand argument to the addition operation
   /// @returns a `ast::BinaryExpression` summing the arguments `lhs` and `rhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Add(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Add(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kAdd,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1593,7 +1640,7 @@
   /// @param rhs the right hand argument to the and operation
   /// @returns a `ast::BinaryExpression` bitwise anding `lhs` and `rhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* And(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* And(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kAnd,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1603,7 +1650,7 @@
   /// @param rhs the right hand argument to the or operation
   /// @returns a `ast::BinaryExpression` bitwise or-ing `lhs` and `rhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Or(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Or(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kOr,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1613,7 +1660,7 @@
   /// @param rhs the right hand argument to the subtraction operation
   /// @returns a `ast::BinaryExpression` subtracting `rhs` from `lhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Sub(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Sub(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kSubtract,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1623,7 +1670,7 @@
   /// @param rhs the right hand argument to the multiplication operation
   /// @returns a `ast::BinaryExpression` multiplying `rhs` from `lhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Mul(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Mul(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kMultiply,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1634,7 +1681,7 @@
   /// @param rhs the right hand argument to the multiplication operation
   /// @returns a `ast::BinaryExpression` multiplying `rhs` from `lhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Mul(const Source& source, LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Mul(const Source& source, LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(source, ast::BinaryOp::kMultiply,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1644,7 +1691,7 @@
   /// @param rhs the right hand argument to the division operation
   /// @returns a `ast::BinaryExpression` dividing `lhs` by `rhs`
   template <typename LHS, typename RHS>
-  ast::Expression* Div(LHS&& lhs, RHS&& rhs) {
+  const ast::Expression* Div(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kDivide,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1654,7 +1701,7 @@
   /// @param rhs the right hand argument to the bit shift right operation
   /// @returns a `ast::BinaryExpression` bit shifting right `lhs` by `rhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Shr(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Shr(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kShiftRight,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1664,7 +1711,7 @@
   /// @param rhs the right hand argument to the bit shift left operation
   /// @returns a `ast::BinaryExpression` bit shifting left `lhs` by `rhs`
   template <typename LHS, typename RHS>
-  ast::BinaryExpression* Shl(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Shl(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kShiftLeft,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1675,9 +1722,9 @@
   /// @param idx the index argument for the array accessor expression
   /// @returns a `ast::ArrayAccessorExpression` that indexes `arr` with `idx`
   template <typename ARR, typename IDX>
-  ast::ArrayAccessorExpression* IndexAccessor(const Source& source,
-                                              ARR&& arr,
-                                              IDX&& idx) {
+  const ast::ArrayAccessorExpression* IndexAccessor(const Source& source,
+                                                    ARR&& arr,
+                                                    IDX&& idx) {
     return create<ast::ArrayAccessorExpression>(
         source, Expr(std::forward<ARR>(arr)), Expr(std::forward<IDX>(idx)));
   }
@@ -1686,7 +1733,7 @@
   /// @param idx the index argument for the array accessor expression
   /// @returns a `ast::ArrayAccessorExpression` that indexes `arr` with `idx`
   template <typename ARR, typename IDX>
-  ast::ArrayAccessorExpression* IndexAccessor(ARR&& arr, IDX&& idx) {
+  const ast::ArrayAccessorExpression* IndexAccessor(ARR&& arr, IDX&& idx) {
     return create<ast::ArrayAccessorExpression>(Expr(std::forward<ARR>(arr)),
                                                 Expr(std::forward<IDX>(idx)));
   }
@@ -1695,7 +1742,7 @@
   /// @param idx the index argument for the array accessor expression
   /// @returns a `ast::MemberAccessorExpression` that indexes `obj` with `idx`
   template <typename OBJ, typename IDX>
-  ast::MemberAccessorExpression* MemberAccessor(OBJ&& obj, IDX&& idx) {
+  const ast::MemberAccessorExpression* MemberAccessor(OBJ&& obj, IDX&& idx) {
     return create<ast::MemberAccessorExpression>(Expr(std::forward<OBJ>(obj)),
                                                  Expr(std::forward<IDX>(idx)));
   }
@@ -1703,7 +1750,7 @@
   /// Creates a ast::StructMemberOffsetDecoration
   /// @param val the offset value
   /// @returns the offset decoration pointer
-  ast::StructMemberOffsetDecoration* MemberOffset(uint32_t val) {
+  const ast::StructMemberOffsetDecoration* MemberOffset(uint32_t val) {
     return create<ast::StructMemberOffsetDecoration>(source_, val);
   }
 
@@ -1711,15 +1758,15 @@
   /// @param source the source information
   /// @param val the size value
   /// @returns the size decoration pointer
-  ast::StructMemberSizeDecoration* MemberSize(const Source& source,
-                                              uint32_t val) {
+  const ast::StructMemberSizeDecoration* MemberSize(const Source& source,
+                                                    uint32_t val) {
     return create<ast::StructMemberSizeDecoration>(source, val);
   }
 
   /// Creates a ast::StructMemberSizeDecoration
   /// @param val the size value
   /// @returns the size decoration pointer
-  ast::StructMemberSizeDecoration* MemberSize(uint32_t val) {
+  const ast::StructMemberSizeDecoration* MemberSize(uint32_t val) {
     return create<ast::StructMemberSizeDecoration>(source_, val);
   }
 
@@ -1727,35 +1774,35 @@
   /// @param source the source information
   /// @param val the align value
   /// @returns the align decoration pointer
-  ast::StructMemberAlignDecoration* MemberAlign(const Source& source,
-                                                uint32_t val) {
+  const ast::StructMemberAlignDecoration* MemberAlign(const Source& source,
+                                                      uint32_t val) {
     return create<ast::StructMemberAlignDecoration>(source, val);
   }
 
   /// Creates a ast::StructMemberAlignDecoration
   /// @param val the align value
   /// @returns the align decoration pointer
-  ast::StructMemberAlignDecoration* MemberAlign(uint32_t val) {
+  const ast::StructMemberAlignDecoration* MemberAlign(uint32_t val) {
     return create<ast::StructMemberAlignDecoration>(source_, val);
   }
 
   /// Creates a ast::StructBlockDecoration
   /// @returns the struct block decoration pointer
-  ast::StructBlockDecoration* StructBlock() {
+  const ast::StructBlockDecoration* StructBlock() {
     return create<ast::StructBlockDecoration>();
   }
 
   /// Creates the ast::GroupDecoration
   /// @param value group decoration index
   /// @returns the group decoration pointer
-  ast::GroupDecoration* Group(uint32_t value) {
+  const ast::GroupDecoration* Group(uint32_t value) {
     return create<ast::GroupDecoration>(value);
   }
 
   /// Creates the ast::BindingDecoration
   /// @param value the binding index
   /// @returns the binding deocration pointer
-  ast::BindingDecoration* Binding(uint32_t value) {
+  const ast::BindingDecoration* Binding(uint32_t value) {
     return create<ast::BindingDecoration>(value);
   }
 
@@ -1779,13 +1826,13 @@
   /// decorations
   /// @returns the function pointer
   template <typename NAME>
-  ast::Function* Func(const Source& source,
-                      NAME&& name,
-                      ast::VariableList params,
-                      ast::Type* type,
-                      ast::StatementList body,
-                      ast::DecorationList decorations = {},
-                      ast::DecorationList return_type_decorations = {}) {
+  const ast::Function* Func(const Source& source,
+                            NAME&& name,
+                            ast::VariableList params,
+                            const ast::Type* type,
+                            ast::StatementList body,
+                            ast::DecorationList decorations = {},
+                            ast::DecorationList return_type_decorations = {}) {
     auto* func =
         create<ast::Function>(source, Sym(std::forward<NAME>(name)), params,
                               type, create<ast::BlockStatement>(body),
@@ -1804,12 +1851,12 @@
   /// decorations
   /// @returns the function pointer
   template <typename NAME>
-  ast::Function* Func(NAME&& name,
-                      ast::VariableList params,
-                      ast::Type* type,
-                      ast::StatementList body,
-                      ast::DecorationList decorations = {},
-                      ast::DecorationList return_type_decorations = {}) {
+  const ast::Function* Func(NAME&& name,
+                            ast::VariableList params,
+                            const ast::Type* type,
+                            ast::StatementList body,
+                            ast::DecorationList decorations = {},
+                            ast::DecorationList return_type_decorations = {}) {
     auto* func = create<ast::Function>(Sym(std::forward<NAME>(name)), params,
                                        type, create<ast::BlockStatement>(body),
                                        decorations, return_type_decorations);
@@ -1820,31 +1867,33 @@
   /// Creates an ast::BreakStatement
   /// @param source the source information
   /// @returns the break statement pointer
-  ast::BreakStatement* Break(const Source& source) {
+  const ast::BreakStatement* Break(const Source& source) {
     return create<ast::BreakStatement>(source);
   }
 
   /// Creates an ast::BreakStatement
   /// @returns the break statement pointer
-  ast::BreakStatement* Break() { return create<ast::BreakStatement>(); }
+  const ast::BreakStatement* Break() { return create<ast::BreakStatement>(); }
 
   /// Creates an ast::ReturnStatement with no return value
   /// @param source the source information
   /// @returns the return statement pointer
-  ast::ReturnStatement* Return(const Source& source) {
+  const ast::ReturnStatement* Return(const Source& source) {
     return create<ast::ReturnStatement>(source);
   }
 
   /// Creates an ast::ReturnStatement with no return value
   /// @returns the return statement pointer
-  ast::ReturnStatement* Return() { return create<ast::ReturnStatement>(); }
+  const ast::ReturnStatement* Return() {
+    return create<ast::ReturnStatement>();
+  }
 
   /// Creates an ast::ReturnStatement with the given return value
   /// @param source the source information
   /// @param val the return value
   /// @returns the return statement pointer
   template <typename EXPR>
-  ast::ReturnStatement* Return(const Source& source, EXPR&& val) {
+  const ast::ReturnStatement* Return(const Source& source, EXPR&& val) {
     return create<ast::ReturnStatement>(source, Expr(std::forward<EXPR>(val)));
   }
 
@@ -1852,7 +1901,7 @@
   /// @param val the return value
   /// @returns the return statement pointer
   template <typename EXPR, typename = DisableIfSource<EXPR>>
-  ast::ReturnStatement* Return(EXPR&& val) {
+  const ast::ReturnStatement* Return(EXPR&& val) {
     return create<ast::ReturnStatement>(Expr(std::forward<EXPR>(val)));
   }
 
@@ -1862,7 +1911,9 @@
   /// @param type the alias target type
   /// @returns the alias type
   template <typename NAME>
-  ast::Alias* Alias(const Source& source, NAME&& name, ast::Type* type) {
+  const ast::Alias* Alias(const Source& source,
+                          NAME&& name,
+                          const ast::Type* type) {
     auto* out = ty.alias(source, std::forward<NAME>(name), type);
     AST().AddTypeDecl(out);
     return out;
@@ -1873,7 +1924,7 @@
   /// @param type the alias target type
   /// @returns the alias type
   template <typename NAME>
-  ast::Alias* Alias(NAME&& name, ast::Type* type) {
+  const ast::Alias* Alias(NAME&& name, const ast::Type* type) {
     auto* out = ty.alias(std::forward<NAME>(name), type);
     AST().AddTypeDecl(out);
     return out;
@@ -1886,10 +1937,10 @@
   /// @param decorations the optional struct decorations
   /// @returns the struct type
   template <typename NAME>
-  ast::Struct* Structure(const Source& source,
-                         NAME&& name,
-                         ast::StructMemberList members,
-                         ast::DecorationList decorations = {}) {
+  const ast::Struct* Structure(const Source& source,
+                               NAME&& name,
+                               ast::StructMemberList members,
+                               ast::DecorationList decorations = {}) {
     auto sym = Sym(std::forward<NAME>(name));
     auto* type = create<ast::Struct>(source, sym, std::move(members),
                                      std::move(decorations));
@@ -1903,9 +1954,9 @@
   /// @param decorations the optional struct decorations
   /// @returns the struct type
   template <typename NAME>
-  ast::Struct* Structure(NAME&& name,
-                         ast::StructMemberList members,
-                         ast::DecorationList decorations = {}) {
+  const ast::Struct* Structure(NAME&& name,
+                               ast::StructMemberList members,
+                               ast::DecorationList decorations = {}) {
     auto sym = Sym(std::forward<NAME>(name));
     auto* type =
         create<ast::Struct>(sym, std::move(members), std::move(decorations));
@@ -1920,10 +1971,10 @@
   /// @param decorations the optional struct member decorations
   /// @returns the struct member pointer
   template <typename NAME>
-  ast::StructMember* Member(const Source& source,
-                            NAME&& name,
-                            ast::Type* type,
-                            ast::DecorationList decorations = {}) {
+  const ast::StructMember* Member(const Source& source,
+                                  NAME&& name,
+                                  const ast::Type* type,
+                                  ast::DecorationList decorations = {}) {
     return create<ast::StructMember>(source, Sym(std::forward<NAME>(name)),
                                      type, std::move(decorations));
   }
@@ -1934,9 +1985,9 @@
   /// @param decorations the optional struct member decorations
   /// @returns the struct member pointer
   template <typename NAME>
-  ast::StructMember* Member(NAME&& name,
-                            ast::Type* type,
-                            ast::DecorationList decorations = {}) {
+  const ast::StructMember* Member(NAME&& name,
+                                  const ast::Type* type,
+                                  ast::DecorationList decorations = {}) {
     return create<ast::StructMember>(source_, Sym(std::forward<NAME>(name)),
                                      type, std::move(decorations));
   }
@@ -1947,7 +1998,9 @@
   /// @param type the struct member type
   /// @returns the struct member pointer
   template <typename NAME>
-  ast::StructMember* Member(uint32_t offset, NAME&& name, ast::Type* type) {
+  const ast::StructMember* Member(uint32_t offset,
+                                  NAME&& name,
+                                  const ast::Type* type) {
     return create<ast::StructMember>(
         source_, Sym(std::forward<NAME>(name)), type,
         ast::DecorationList{
@@ -1960,7 +2013,8 @@
   /// @param statements statements of block
   /// @returns the block statement pointer
   template <typename... Statements>
-  ast::BlockStatement* Block(const Source& source, Statements&&... statements) {
+  const ast::BlockStatement* Block(const Source& source,
+                                   Statements&&... statements) {
     return create<ast::BlockStatement>(
         source, ast::StatementList{std::forward<Statements>(statements)...});
   }
@@ -1969,7 +2023,7 @@
   /// @param statements statements of block
   /// @returns the block statement pointer
   template <typename... STATEMENTS, typename = DisableIfSource<STATEMENTS...>>
-  ast::BlockStatement* Block(STATEMENTS&&... statements) {
+  const ast::BlockStatement* Block(STATEMENTS&&... statements) {
     return create<ast::BlockStatement>(
         ast::StatementList{std::forward<STATEMENTS>(statements)...});
   }
@@ -1979,7 +2033,8 @@
   /// @param body the else body
   /// @returns the else statement pointer
   template <typename CONDITION>
-  ast::ElseStatement* Else(CONDITION&& condition, ast::BlockStatement* body) {
+  const ast::ElseStatement* Else(CONDITION&& condition,
+                                 const ast::BlockStatement* body) {
     return create<ast::ElseStatement>(Expr(std::forward<CONDITION>(condition)),
                                       body);
   }
@@ -1991,9 +2046,9 @@
   /// @param elseStatements optional variadic else statements
   /// @returns the if statement pointer
   template <typename CONDITION, typename... ELSE_STATEMENTS>
-  ast::IfStatement* If(CONDITION&& condition,
-                       ast::BlockStatement* body,
-                       ELSE_STATEMENTS&&... elseStatements) {
+  const ast::IfStatement* If(CONDITION&& condition,
+                             const ast::BlockStatement* body,
+                             ELSE_STATEMENTS&&... elseStatements) {
     return create<ast::IfStatement>(
         Expr(std::forward<CONDITION>(condition)), body,
         ast::ElseStatementList{
@@ -2006,9 +2061,9 @@
   /// @param rhs the right hand side expression initializer
   /// @returns the assignment statement pointer
   template <typename LhsExpressionInit, typename RhsExpressionInit>
-  ast::AssignmentStatement* Assign(const Source& source,
-                                   LhsExpressionInit&& lhs,
-                                   RhsExpressionInit&& rhs) {
+  const ast::AssignmentStatement* Assign(const Source& source,
+                                         LhsExpressionInit&& lhs,
+                                         RhsExpressionInit&& rhs) {
     return create<ast::AssignmentStatement>(
         source, Expr(std::forward<LhsExpressionInit>(lhs)),
         Expr(std::forward<RhsExpressionInit>(rhs)));
@@ -2019,8 +2074,8 @@
   /// @param rhs the right hand side expression initializer
   /// @returns the assignment statement pointer
   template <typename LhsExpressionInit, typename RhsExpressionInit>
-  ast::AssignmentStatement* Assign(LhsExpressionInit&& lhs,
-                                   RhsExpressionInit&& rhs) {
+  const ast::AssignmentStatement* Assign(LhsExpressionInit&& lhs,
+                                         RhsExpressionInit&& rhs) {
     return create<ast::AssignmentStatement>(
         Expr(std::forward<LhsExpressionInit>(lhs)),
         Expr(std::forward<RhsExpressionInit>(rhs)));
@@ -2030,8 +2085,9 @@
   /// @param body the loop body
   /// @param continuing the optional continuing block
   /// @returns the loop statement pointer
-  ast::LoopStatement* Loop(ast::BlockStatement* body,
-                           ast::BlockStatement* continuing = nullptr) {
+  const ast::LoopStatement* Loop(
+      const ast::BlockStatement* body,
+      const ast::BlockStatement* continuing = nullptr) {
     return create<ast::LoopStatement>(body, continuing);
   }
 
@@ -2044,11 +2100,11 @@
   /// @param body the loop body
   /// @returns the for loop statement pointer
   template <typename COND>
-  ast::ForLoopStatement* For(const Source& source,
-                             ast::Statement* init,
-                             COND&& cond,
-                             ast::Statement* cont,
-                             ast::BlockStatement* body) {
+  const ast::ForLoopStatement* For(const Source& source,
+                                   const ast::Statement* init,
+                                   COND&& cond,
+                                   const ast::Statement* cont,
+                                   const ast::BlockStatement* body) {
     return create<ast::ForLoopStatement>(
         source, init, Expr(std::forward<COND>(cond)), cont, body);
   }
@@ -2061,10 +2117,10 @@
   /// @param body the loop body
   /// @returns the for loop statement pointer
   template <typename COND>
-  ast::ForLoopStatement* For(ast::Statement* init,
-                             COND&& cond,
-                             ast::Statement* cont,
-                             ast::BlockStatement* body) {
+  const ast::ForLoopStatement* For(const ast::Statement* init,
+                                   COND&& cond,
+                                   const ast::Statement* cont,
+                                   const ast::BlockStatement* body) {
     return create<ast::ForLoopStatement>(init, Expr(std::forward<COND>(cond)),
                                          cont, body);
   }
@@ -2073,14 +2129,15 @@
   /// @param source the source information
   /// @param var the variable to wrap in a decl statement
   /// @returns the variable decl statement pointer
-  ast::VariableDeclStatement* Decl(const Source& source, ast::Variable* var) {
+  const ast::VariableDeclStatement* Decl(const Source& source,
+                                         const ast::Variable* var) {
     return create<ast::VariableDeclStatement>(source, var);
   }
 
   /// Creates a ast::VariableDeclStatement for the input variable
   /// @param var the variable to wrap in a decl statement
   /// @returns the variable decl statement pointer
-  ast::VariableDeclStatement* Decl(ast::Variable* var) {
+  const ast::VariableDeclStatement* Decl(const ast::Variable* var) {
     return create<ast::VariableDeclStatement>(var);
   }
 
@@ -2090,9 +2147,9 @@
   /// @param cases case statements
   /// @returns the switch statement pointer
   template <typename ExpressionInit, typename... Cases>
-  ast::SwitchStatement* Switch(const Source& source,
-                               ExpressionInit&& condition,
-                               Cases&&... cases) {
+  const ast::SwitchStatement* Switch(const Source& source,
+                                     ExpressionInit&& condition,
+                                     Cases&&... cases) {
     return create<ast::SwitchStatement>(
         source, Expr(std::forward<ExpressionInit>(condition)),
         ast::CaseStatementList{std::forward<Cases>(cases)...});
@@ -2105,7 +2162,8 @@
   template <typename ExpressionInit,
             typename... Cases,
             typename = DisableIfSource<ExpressionInit>>
-  ast::SwitchStatement* Switch(ExpressionInit&& condition, Cases&&... cases) {
+  const ast::SwitchStatement* Switch(ExpressionInit&& condition,
+                                     Cases&&... cases) {
     return create<ast::SwitchStatement>(
         Expr(std::forward<ExpressionInit>(condition)),
         ast::CaseStatementList{std::forward<Cases>(cases)...});
@@ -2116,9 +2174,9 @@
   /// @param selectors list of selectors
   /// @param body the case body
   /// @returns the case statement pointer
-  ast::CaseStatement* Case(const Source& source,
-                           ast::CaseSelectorList selectors,
-                           ast::BlockStatement* body = nullptr) {
+  const ast::CaseStatement* Case(const Source& source,
+                                 ast::CaseSelectorList selectors,
+                                 const ast::BlockStatement* body = nullptr) {
     return create<ast::CaseStatement>(source, std::move(selectors),
                                       body ? body : Block());
   }
@@ -2127,8 +2185,8 @@
   /// @param selectors list of selectors
   /// @param body the case body
   /// @returns the case statement pointer
-  ast::CaseStatement* Case(ast::CaseSelectorList selectors,
-                           ast::BlockStatement* body = nullptr) {
+  const ast::CaseStatement* Case(ast::CaseSelectorList selectors,
+                                 const ast::BlockStatement* body = nullptr) {
     return create<ast::CaseStatement>(std::move(selectors),
                                       body ? body : Block());
   }
@@ -2137,8 +2195,8 @@
   /// @param selector a single case selector
   /// @param body the case body
   /// @returns the case statement pointer
-  ast::CaseStatement* Case(ast::IntLiteral* selector,
-                           ast::BlockStatement* body = nullptr) {
+  const ast::CaseStatement* Case(const ast::IntLiteral* selector,
+                                 const ast::BlockStatement* body = nullptr) {
     return Case(ast::CaseSelectorList{selector}, body);
   }
 
@@ -2146,15 +2204,17 @@
   /// @param source the source information
   /// @param body the case body
   /// @returns the case statement pointer
-  ast::CaseStatement* DefaultCase(const Source& source,
-                                  ast::BlockStatement* body = nullptr) {
+  const ast::CaseStatement* DefaultCase(
+      const Source& source,
+      const ast::BlockStatement* body = nullptr) {
     return Case(source, ast::CaseSelectorList{}, body);
   }
 
   /// Convenience function that creates a 'default' ast::CaseStatement
   /// @param body the case body
   /// @returns the case statement pointer
-  ast::CaseStatement* DefaultCase(ast::BlockStatement* body = nullptr) {
+  const ast::CaseStatement* DefaultCase(
+      const ast::BlockStatement* body = nullptr) {
     return Case(ast::CaseSelectorList{}, body);
   }
 
@@ -2162,14 +2222,15 @@
   /// @param source the source information
   /// @param builtin the builtin value
   /// @returns the builtin decoration pointer
-  ast::BuiltinDecoration* Builtin(const Source& source, ast::Builtin builtin) {
+  const ast::BuiltinDecoration* Builtin(const Source& source,
+                                        ast::Builtin builtin) {
     return create<ast::BuiltinDecoration>(source, builtin);
   }
 
   /// Creates an ast::BuiltinDecoration
   /// @param builtin the builtin value
   /// @returns the builtin decoration pointer
-  ast::BuiltinDecoration* Builtin(ast::Builtin builtin) {
+  const ast::BuiltinDecoration* Builtin(ast::Builtin builtin) {
     return create<ast::BuiltinDecoration>(source_, builtin);
   }
 
@@ -2178,9 +2239,10 @@
   /// @param type the interpolation type
   /// @param sampling the interpolation sampling
   /// @returns the interpolate decoration pointer
-  ast::InterpolateDecoration* Interpolate(const Source& source,
-                                          ast::InterpolationType type,
-                                          ast::InterpolationSampling sampling) {
+  const ast::InterpolateDecoration* Interpolate(
+      const Source& source,
+      ast::InterpolationType type,
+      ast::InterpolationSampling sampling) {
     return create<ast::InterpolateDecoration>(source, type, sampling);
   }
 
@@ -2188,21 +2250,22 @@
   /// @param type the interpolation type
   /// @param sampling the interpolation sampling
   /// @returns the interpolate decoration pointer
-  ast::InterpolateDecoration* Interpolate(ast::InterpolationType type,
-                                          ast::InterpolationSampling sampling) {
+  const ast::InterpolateDecoration* Interpolate(
+      ast::InterpolationType type,
+      ast::InterpolationSampling sampling) {
     return create<ast::InterpolateDecoration>(source_, type, sampling);
   }
 
   /// Creates an ast::InvariantDecoration
   /// @param source the source information
   /// @returns the invariant decoration pointer
-  ast::InvariantDecoration* Invariant(const Source& source) {
+  const ast::InvariantDecoration* Invariant(const Source& source) {
     return create<ast::InvariantDecoration>(source);
   }
 
   /// Creates an ast::InvariantDecoration
   /// @returns the invariant decoration pointer
-  ast::InvariantDecoration* Invariant() {
+  const ast::InvariantDecoration* Invariant() {
     return create<ast::InvariantDecoration>(source_);
   }
 
@@ -2210,14 +2273,15 @@
   /// @param source the source information
   /// @param location the location value
   /// @returns the location decoration pointer
-  ast::LocationDecoration* Location(const Source& source, uint32_t location) {
+  const ast::LocationDecoration* Location(const Source& source,
+                                          uint32_t location) {
     return create<ast::LocationDecoration>(source, location);
   }
 
   /// Creates an ast::LocationDecoration
   /// @param location the location value
   /// @returns the location decoration pointer
-  ast::LocationDecoration* Location(uint32_t location) {
+  const ast::LocationDecoration* Location(uint32_t location) {
     return create<ast::LocationDecoration>(source_, location);
   }
 
@@ -2225,40 +2289,41 @@
   /// @param source the source information
   /// @param id the id value
   /// @returns the override decoration pointer
-  ast::OverrideDecoration* Override(const Source& source, uint32_t id) {
+  const ast::OverrideDecoration* Override(const Source& source, uint32_t id) {
     return create<ast::OverrideDecoration>(source, id);
   }
 
   /// Creates an ast::OverrideDecoration with a specific constant ID
   /// @param id the optional id value
   /// @returns the override decoration pointer
-  ast::OverrideDecoration* Override(uint32_t id) {
+  const ast::OverrideDecoration* Override(uint32_t id) {
     return Override(source_, id);
   }
 
   /// Creates an ast::OverrideDecoration without a constant ID
   /// @param source the source information
   /// @returns the override decoration pointer
-  ast::OverrideDecoration* Override(const Source& source) {
+  const ast::OverrideDecoration* Override(const Source& source) {
     return create<ast::OverrideDecoration>(source);
   }
 
   /// Creates an ast::OverrideDecoration without a constant ID
   /// @returns the override decoration pointer
-  ast::OverrideDecoration* Override() { return Override(source_); }
+  const ast::OverrideDecoration* Override() { return Override(source_); }
 
   /// Creates an ast::StageDecoration
   /// @param source the source information
   /// @param stage the pipeline stage
   /// @returns the stage decoration pointer
-  ast::StageDecoration* Stage(const Source& source, ast::PipelineStage stage) {
+  const ast::StageDecoration* Stage(const Source& source,
+                                    ast::PipelineStage stage) {
     return create<ast::StageDecoration>(source, stage);
   }
 
   /// Creates an ast::StageDecoration
   /// @param stage the pipeline stage
   /// @returns the stage decoration pointer
-  ast::StageDecoration* Stage(ast::PipelineStage stage) {
+  const ast::StageDecoration* Stage(ast::PipelineStage stage) {
     return create<ast::StageDecoration>(source_, stage);
   }
 
@@ -2266,7 +2331,7 @@
   /// @param x the x dimension expression
   /// @returns the workgroup decoration pointer
   template <typename EXPR_X>
-  ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x) {
+  const ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x) {
     return WorkgroupSize(std::forward<EXPR_X>(x), nullptr, nullptr);
   }
 
@@ -2275,7 +2340,7 @@
   /// @param y the y dimension expression
   /// @returns the workgroup decoration pointer
   template <typename EXPR_X, typename EXPR_Y>
-  ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x, EXPR_Y&& y) {
+  const ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x, EXPR_Y&& y) {
     return WorkgroupSize(std::forward<EXPR_X>(x), std::forward<EXPR_Y>(y),
                          nullptr);
   }
@@ -2287,10 +2352,10 @@
   /// @param z the z dimension expression
   /// @returns the workgroup decoration pointer
   template <typename EXPR_X, typename EXPR_Y, typename EXPR_Z>
-  ast::WorkgroupDecoration* WorkgroupSize(const Source& source,
-                                          EXPR_X&& x,
-                                          EXPR_Y&& y,
-                                          EXPR_Z&& z) {
+  const ast::WorkgroupDecoration* WorkgroupSize(const Source& source,
+                                                EXPR_X&& x,
+                                                EXPR_Y&& y,
+                                                EXPR_Z&& z) {
     return create<ast::WorkgroupDecoration>(
         source, Expr(std::forward<EXPR_X>(x)), Expr(std::forward<EXPR_Y>(y)),
         Expr(std::forward<EXPR_Z>(z)));
@@ -2302,7 +2367,9 @@
   /// @param z the z dimension expression
   /// @returns the workgroup decoration pointer
   template <typename EXPR_X, typename EXPR_Y, typename EXPR_Z>
-  ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x, EXPR_Y&& y, EXPR_Z&& z) {
+  const ast::WorkgroupDecoration* WorkgroupSize(EXPR_X&& x,
+                                                EXPR_Y&& y,
+                                                EXPR_Z&& z) {
     return create<ast::WorkgroupDecoration>(
         source_, Expr(std::forward<EXPR_X>(x)), Expr(std::forward<EXPR_Y>(y)),
         Expr(std::forward<EXPR_Z>(z)));
@@ -2362,7 +2429,7 @@
   /// @param expr the AST expression
   /// @return the resolved semantic type for the expression, or nullptr if the
   /// expression has no resolved type.
-  sem::Type* TypeOf(const ast::Expression* expr) const;
+  const sem::Type* TypeOf(const ast::Expression* expr) const;
 
   /// Helper for returning the resolved semantic type of the variable `var`.
   /// @note As the Resolver is run when the Program is built, this will only be
@@ -2370,7 +2437,7 @@
   /// @param var the AST variable
   /// @return the resolved semantic type for the variable, or nullptr if the
   /// variable has no resolved type.
-  sem::Type* TypeOf(const ast::Variable* var) const;
+  const sem::Type* TypeOf(const ast::Variable* var) const;
 
   /// Helper for returning the resolved semantic type of the AST type `type`.
   /// @note As the Resolver is run when the Program is built, this will only be
@@ -2394,37 +2461,37 @@
   /// nodes.
   /// @param lit the ast::Literal to be wrapped by an ast::Statement
   /// @return the ast::Statement that wraps the ast::Statement
-  ast::Statement* WrapInStatement(ast::Literal* lit);
+  const ast::Statement* WrapInStatement(const ast::Literal* lit);
   /// Wraps the ast::Expression in a statement. This is used by tests that
   /// construct a partial AST and require the Resolver to reach these
   /// nodes.
   /// @param expr the ast::Expression to be wrapped by an ast::Statement
   /// @return the ast::Statement that wraps the ast::Expression
-  ast::Statement* WrapInStatement(ast::Expression* expr);
+  const ast::Statement* WrapInStatement(const ast::Expression* expr);
   /// Wraps the ast::Variable in a ast::VariableDeclStatement. This is used by
   /// tests that construct a partial AST and require the Resolver to reach
   /// these nodes.
   /// @param v the ast::Variable to be wrapped by an ast::VariableDeclStatement
   /// @return the ast::VariableDeclStatement that wraps the ast::Variable
-  ast::VariableDeclStatement* WrapInStatement(ast::Variable* v);
+  const ast::VariableDeclStatement* WrapInStatement(const ast::Variable* v);
   /// Returns the statement argument. Used as a passthrough-overload by
   /// WrapInFunction().
   /// @param stmt the ast::Statement
   /// @return `stmt`
-  ast::Statement* WrapInStatement(ast::Statement* stmt);
+  const ast::Statement* WrapInStatement(const ast::Statement* stmt);
   /// Wraps the list of arguments in a simple function so that each is reachable
   /// by the Resolver.
   /// @param args a mix of ast::Expression, ast::Statement, ast::Variables.
   /// @returns the function
   template <typename... ARGS>
-  ast::Function* WrapInFunction(ARGS&&... args) {
+  const ast::Function* WrapInFunction(ARGS&&... args) {
     ast::StatementList stmts{WrapInStatement(std::forward<ARGS>(args))...};
     return WrapInFunction(std::move(stmts));
   }
   /// @param stmts a list of ast::Statement that will be wrapped by a function,
   /// so that each statement is reachable by the Resolver.
   /// @returns the function
-  ast::Function* WrapInFunction(ast::StatementList stmts);
+  const ast::Function* WrapInFunction(ast::StatementList stmts);
 
   /// The builder types
   TypesBuilder const ty{this};
@@ -2460,31 +2527,31 @@
 // Various template specializations for ProgramBuilder::TypesBuilder::CToAST.
 template <>
 struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::i32> {
-  static ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
     return t->i32();
   }
 };
 template <>
 struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::u32> {
-  static ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
     return t->u32();
   }
 };
 template <>
 struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::f32> {
-  static ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
     return t->f32();
   }
 };
 template <>
 struct ProgramBuilder::TypesBuilder::CToAST<bool> {
-  static ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
     return t->bool_();
   }
 };
 template <>
 struct ProgramBuilder::TypesBuilder::CToAST<void> {
-  static ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
     return t->void_();
   }
 };
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index b9ef0c9..b02f467 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -641,11 +641,12 @@
     : public Castable<SwitchStatementBuilder, StatementBuilder> {
   /// Constructor
   /// @param cond the switch statement condition
-  explicit SwitchStatementBuilder(ast::Expression* cond) : condition(cond) {}
+  explicit SwitchStatementBuilder(const ast::Expression* cond)
+      : condition(cond) {}
 
   /// @param builder the program builder
   /// @returns the built ast::SwitchStatement
-  ast::SwitchStatement* Build(ProgramBuilder* builder) const override {
+  const ast::SwitchStatement* Build(ProgramBuilder* builder) const override {
     // We've listed cases in reverse order in the switch statement.
     // Reorder them to match the presentation order in WGSL.
     auto reversed_cases = cases;
@@ -656,7 +657,7 @@
   }
 
   /// Switch statement condition
-  ast::Expression* const condition;
+  const ast::Expression* const condition;
   /// Switch statement cases
   ast::CaseStatementList cases;
 };
@@ -667,18 +668,18 @@
     : public Castable<IfStatementBuilder, StatementBuilder> {
   /// Constructor
   /// @param c the if-statement condition
-  explicit IfStatementBuilder(ast::Expression* c) : cond(c) {}
+  explicit IfStatementBuilder(const ast::Expression* c) : cond(c) {}
 
   /// @param builder the program builder
   /// @returns the built ast::IfStatement
-  ast::IfStatement* Build(ProgramBuilder* builder) const override {
+  const ast::IfStatement* Build(ProgramBuilder* builder) const override {
     return builder->create<ast::IfStatement>(Source{}, cond, body, else_stmts);
   }
 
   /// If-statement condition
-  ast::Expression* const cond;
+  const ast::Expression* const cond;
   /// If-statement block body
-  ast::BlockStatement* body = nullptr;
+  const ast::BlockStatement* body = nullptr;
   /// Optional if-statement else statements
   ast::ElseStatementList else_stmts;
 };
@@ -694,9 +695,14 @@
   }
 
   /// Loop-statement block body
-  ast::BlockStatement* body = nullptr;
+  const ast::BlockStatement* body = nullptr;
   /// Loop-statement continuing body
-  ast::BlockStatement* continuing = nullptr;
+  /// @note the mutable keyword here is required as all non-StatementBuilders
+  /// `ast::Node`s are immutable and are referenced with `const` pointers.
+  /// StatementBuilders however exist to provide mutable state while the
+  /// FunctionEmitter is building the function. All StatementBuilders are
+  /// replaced with immutable AST nodes when Finalize() is called.
+  mutable const ast::BlockStatement* continuing = nullptr;
 };
 
 /// @param decos a list of parsed decorations
@@ -798,7 +804,7 @@
   finalized_ = true;
 }
 
-void FunctionEmitter::StatementBlock::Add(ast::Statement* statement) {
+void FunctionEmitter::StatementBlock::Add(const ast::Statement* statement) {
   TINT_ASSERT(Reader,
               !finalized_ /* Add() must not be called after Finalize() */);
   statements_.emplace_back(statement);
@@ -849,7 +855,8 @@
   return entry.GetStatements();
 }
 
-ast::Statement* FunctionEmitter::AddStatement(ast::Statement* statement) {
+const ast::Statement* FunctionEmitter::AddStatement(
+    const ast::Statement* statement) {
   TINT_ASSERT(Reader, !statements_stack_.empty());
   if (statement != nullptr) {
     statements_stack_.back().Add(statement);
@@ -857,7 +864,7 @@
   return statement;
 }
 
-ast::Statement* FunctionEmitter::LastStatement() {
+const ast::Statement* FunctionEmitter::LastStatement() {
   TINT_ASSERT(Reader, !statements_stack_.empty());
   auto& statement_list = statements_stack_.back().GetStatements();
   TINT_ASSERT(Reader, !statement_list.empty());
@@ -913,7 +920,7 @@
   return success();
 }
 
-ast::BlockStatement* FunctionEmitter::MakeFunctionBody() {
+const ast::BlockStatement* FunctionEmitter::MakeFunctionBody() {
   TINT_ASSERT(Reader, statements_stack_.size() == 1);
 
   if (!EmitBody()) {
@@ -1020,8 +1027,8 @@
 
   // Add a body statement to copy the parameter to the corresponding private
   // variable.
-  ast::Expression* param_value = builder_.Expr(param_name);
-  ast::Expression* store_dest = builder_.Expr(var_name);
+  const ast::Expression* param_value = builder_.Expr(param_name);
+  const ast::Expression* store_dest = builder_.Expr(var_name);
 
   // Index into the LHS as needed.
   auto* current_type = var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
@@ -1066,7 +1073,7 @@
   }
 }
 
-ast::Decoration* FunctionEmitter::GetLocation(
+const ast::Decoration* FunctionEmitter::GetLocation(
     const ast::DecorationList& decos) {
   for (auto* const& deco : decos) {
     if (deco->Is<ast::LocationDecoration>()) {
@@ -1159,7 +1166,7 @@
 
   // Create an expression to evaluate the part of the variable indexed by
   // the index_prefix.
-  ast::Expression* load_source = builder_.Expr(var_name);
+  const ast::Expression* load_source = builder_.Expr(var_name);
 
   // Index into the variable as needed to pick out the flattened member.
   auto* current_type = var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
@@ -1201,7 +1208,7 @@
   FunctionDeclaration decl;
   decl.source = source;
   decl.name = ep_info_->name;
-  ast::Type* return_type = nullptr;  // Populated below.
+  const ast::Type* return_type = nullptr;  // Populated below.
 
   // Pipeline inputs become parameters to the wrapper function, and
   // their values are saved into the corresponding private variables that
@@ -1271,7 +1278,7 @@
         builder_.Symbols().Register(return_struct_name);
 
     // Define the structure.
-    std::vector<ast::StructMember*> return_members;
+    std::vector<const ast::StructMember*> return_members;
     ast::ExpressionList return_exprs;
 
     const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
@@ -1355,10 +1362,10 @@
   if (ep_info_->stage == ast::PipelineStage::kCompute) {
     auto& size = ep_info_->workgroup_size;
     if (size.x != 0 && size.y != 0 && size.z != 0) {
-      ast::Expression* x = builder_.Expr(static_cast<int>(size.x));
-      ast::Expression* y =
+      const ast::Expression* x = builder_.Expr(static_cast<int>(size.x));
+      const ast::Expression* y =
           size.y ? builder_.Expr(static_cast<int>(size.y)) : nullptr;
-      ast::Expression* z =
+      const ast::Expression* z =
           size.z ? builder_.Expr(static_cast<int>(size.z)) : nullptr;
       fn_decos.emplace_back(
           create<ast::WorkgroupDecoration>(Source{}, x, y, z));
@@ -2487,7 +2494,7 @@
     if (failed()) {
       return false;
     }
-    ast::Expression* constructor = nullptr;
+    const ast::Expression* constructor = nullptr;
     if (inst.NumInOperands() > 1) {
       // SPIR-V initializers are always constants.
       // (OpenCL also allows the ID of an OpVariable, but we don't handle that
@@ -3228,7 +3235,7 @@
   return success();
 }
 
-ast::Statement* FunctionEmitter::MakeBranchDetailed(
+const ast::Statement* FunctionEmitter::MakeBranchDetailed(
     const BlockInfo& src_info,
     const BlockInfo& dest_info,
     bool forced,
@@ -3306,9 +3313,10 @@
   return nullptr;
 }
 
-ast::Statement* FunctionEmitter::MakeSimpleIf(ast::Expression* condition,
-                                              ast::Statement* then_stmt,
-                                              ast::Statement* else_stmt) const {
+const ast::Statement* FunctionEmitter::MakeSimpleIf(
+    const ast::Expression* condition,
+    const ast::Statement* then_stmt,
+    const ast::Statement* else_stmt) const {
   if ((then_stmt == nullptr) && (else_stmt == nullptr)) {
     return nullptr;
   }
@@ -3331,7 +3339,7 @@
 
 bool FunctionEmitter::EmitConditionalCaseFallThrough(
     const BlockInfo& src_info,
-    ast::Expression* cond,
+    const ast::Expression* cond,
     EdgeKind other_edge_kind,
     const BlockInfo& other_dest,
     bool fall_through_is_true_branch) {
@@ -3650,7 +3658,7 @@
           return true;
         case SkipReason::kSampleMaskInBuiltinPointer: {
           auto name = namer_.Name(sample_mask_in_id);
-          ast::Expression* id_expr = create<ast::IdentifierExpression>(
+          const ast::Expression* id_expr = create<ast::IdentifierExpression>(
               Source{}, builder_.Symbols().Register(name));
           // SampleMask is an array in Vulkan SPIR-V. Always access the first
           // element.
@@ -4310,7 +4318,7 @@
         constants[index] ? constants[index]->AsIntConstant() : nullptr;
     const int64_t index_const_val =
         index_const ? index_const->GetSignExtendedValue() : 0;
-    ast::Expression* next_expr = nullptr;
+    const ast::Expression* next_expr = nullptr;
 
     const auto* pointee_type_inst = def_use_mgr_->GetDef(pointee_type_id);
     if (!pointee_type_inst) {
@@ -4473,7 +4481,7 @@
         Fail() << "internal error: unhandled " << inst.PrettyPrint();
         return {};
     }
-    ast::Expression* next_expr = nullptr;
+    const ast::Expression* next_expr = nullptr;
     switch (current_type_inst->opcode()) {
       case SpvOpTypeVector: {
         // Try generating a MemberAccessor expression. That result in something
@@ -4559,12 +4567,12 @@
   return current_expr;
 }
 
-ast::Expression* FunctionEmitter::MakeTrue(const Source& source) const {
+const ast::Expression* FunctionEmitter::MakeTrue(const Source& source) const {
   return create<ast::ScalarConstructorExpression>(
       source, create<ast::BoolLiteral>(source, true));
 }
 
-ast::Expression* FunctionEmitter::MakeFalse(const Source& source) const {
+const ast::Expression* FunctionEmitter::MakeFalse(const Source& source) const {
   return create<ast::ScalarConstructorExpression>(
       source, create<ast::BoolLiteral>(source, false));
 }
@@ -5179,7 +5187,7 @@
   return result;
 }
 
-ast::Expression* FunctionEmitter::GetImageExpression(
+const ast::Expression* FunctionEmitter::GetImageExpression(
     const spvtools::opt::Instruction& inst) {
   auto* image = GetImage(inst);
   if (!image) {
@@ -5190,7 +5198,7 @@
                                            builder_.Symbols().Register(name));
 }
 
-ast::Expression* FunctionEmitter::GetSamplerExpression(
+const ast::Expression* FunctionEmitter::GetSamplerExpression(
     const spvtools::opt::Instruction& inst) {
   // The sampled image operand is always the first operand.
   const auto image_or_sampled_image_operand_id = inst.GetSingleWordInOperand(0);
@@ -5415,7 +5423,7 @@
 
   if (inst.type_id() != 0) {
     // It returns a value.
-    ast::Expression* value = call_expr;
+    const ast::Expression* value = call_expr;
 
     // The result type, derived from the SPIR-V instruction.
     auto* result_type = parser_impl_.ConvertType(inst.type_id());
@@ -5505,7 +5513,7 @@
       if (opcode == SpvOpImageQuerySizeLod) {
         dims_args.push_back(ToI32(MakeOperand(inst, 1)).expr);
       }
-      ast::Expression* dims_call =
+      const ast::Expression* dims_call =
           create<ast::CallExpression>(Source{}, dims_ident, dims_args);
       auto dims = texture_type->dims;
       if ((dims == ast::TextureDimension::kCube) ||
@@ -5539,7 +5547,7 @@
                              : "textureNumSamples";
       auto* levels_ident = create<ast::IdentifierExpression>(
           Source{}, builder_.Symbols().Register(name));
-      ast::Expression* ast_expr = create<ast::CallExpression>(
+      const ast::Expression* ast_expr = create<ast::CallExpression>(
           Source{}, levels_ident,
           ast::ExpressionList{GetImageExpression(inst)});
       auto* result_type = parser_impl_.ConvertType(inst.type_id());
@@ -5646,7 +5654,7 @@
   // Use a lambda to make it easy to only generate the expressions when we
   // will actually use them.
   auto prefix_swizzle_expr = [this, num_axes, component_type, is_proj,
-                              raw_coords]() -> ast::Expression* {
+                              raw_coords]() -> const ast::Expression* {
     auto* swizzle_type =
         (num_axes == 1) ? component_type : ty_.Vector(component_type, num_axes);
     auto* swizzle = create<ast::MemberAccessorExpression>(
@@ -5668,7 +5676,7 @@
     result.push_back(prefix_swizzle_expr());
 
     // Now get the array index.
-    ast::Expression* array_index = create<ast::MemberAccessorExpression>(
+    const ast::Expression* array_index = create<ast::MemberAccessorExpression>(
         Source{}, raw_coords.expr, Swizzle(num_axes));
     // Convert it to a signed integer type, if needed
     result.push_back(ToI32({component_type, array_index}).expr);
@@ -5686,7 +5694,7 @@
   return result;
 }
 
-ast::Expression* FunctionEmitter::ConvertTexelForStorage(
+const ast::Expression* FunctionEmitter::ConvertTexelForStorage(
     const spvtools::opt::Instruction& inst,
     TypedExpression texel,
     const Texture* texture_type) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index edf5d48..38b76a9 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -352,7 +352,7 @@
 
   /// @param builder the program builder
   /// @returns the build AST node
-  virtual ast::Statement* Build(ProgramBuilder* builder) const = 0;
+  virtual const ast::Statement* Build(ProgramBuilder* builder) const = 0;
 
  private:
   Node* Clone(CloneContext*) const override;
@@ -473,13 +473,13 @@
   /// Returns the Location dcoration, if it exists.
   /// @param decos the list of decorations to search
   /// @returns the Location decoration, or nullptr if it doesn't exist
-  ast::Decoration* GetLocation(const ast::DecorationList& decos);
+  const ast::Decoration* GetLocation(const ast::DecorationList& decos);
 
   /// Create an ast::BlockStatement representing the body of the function.
   /// This creates the statement stack, which is non-empty for the lifetime
   /// of the function.
   /// @returns the body of the function, or null on error
-  ast::BlockStatement* MakeFunctionBody();
+  const ast::BlockStatement* MakeFunctionBody();
 
   /// Emits the function body, populating the bottom entry of the statements
   /// stack.
@@ -660,8 +660,8 @@
   /// @param src_info the source block
   /// @param dest_info the destination block
   /// @returns the new statement, or a null statement
-  ast::Statement* MakeBranch(const BlockInfo& src_info,
-                             const BlockInfo& dest_info) const {
+  const ast::Statement* MakeBranch(const BlockInfo& src_info,
+                                   const BlockInfo& dest_info) const {
     return MakeBranchDetailed(src_info, dest_info, false, nullptr);
   }
 
@@ -671,8 +671,8 @@
   /// @param src_info the source block
   /// @param dest_info the destination block
   /// @returns the new statement, or a null statement
-  ast::Statement* MakeForcedBranch(const BlockInfo& src_info,
-                                   const BlockInfo& dest_info) const {
+  const ast::Statement* MakeForcedBranch(const BlockInfo& src_info,
+                                         const BlockInfo& dest_info) const {
     return MakeBranchDetailed(src_info, dest_info, true, nullptr);
   }
 
@@ -690,10 +690,11 @@
   /// @param forced if true, always emit the branch (if it exists in WGSL)
   /// @param flow_guard_name_ptr return parameter for control flow guard name
   /// @returns the new statement, or a null statement
-  ast::Statement* MakeBranchDetailed(const BlockInfo& src_info,
-                                     const BlockInfo& dest_info,
-                                     bool forced,
-                                     std::string* flow_guard_name_ptr) const;
+  const ast::Statement* MakeBranchDetailed(
+      const BlockInfo& src_info,
+      const BlockInfo& dest_info,
+      bool forced,
+      std::string* flow_guard_name_ptr) const;
 
   /// Returns a new if statement with the given statements as the then-clause
   /// and the else-clause.  Either or both clauses might be nullptr. If both
@@ -702,9 +703,9 @@
   /// @param then_stmt the statement for the then clause of the if, or nullptr
   /// @param else_stmt the statement for the else clause of the if, or nullptr
   /// @returns the new statement, or nullptr
-  ast::Statement* MakeSimpleIf(ast::Expression* condition,
-                               ast::Statement* then_stmt,
-                               ast::Statement* else_stmt) const;
+  const ast::Statement* MakeSimpleIf(const ast::Expression* condition,
+                                     const ast::Statement* then_stmt,
+                                     const ast::Statement* else_stmt) const;
 
   /// Emits the statements for an normal-terminator OpBranchConditional
   /// where one branch is a case fall through (the true branch if and only
@@ -719,7 +720,7 @@
   /// branch
   /// @returns the false if emission fails
   bool EmitConditionalCaseFallThrough(const BlockInfo& src_info,
-                                      ast::Expression* cond,
+                                      const ast::Expression* cond,
                                       EdgeKind other_edge_kind,
                                       const BlockInfo& other_dest,
                                       bool fall_through_is_true_branch);
@@ -1031,13 +1032,15 @@
   /// given instruction.
   /// @param inst the SPIR-V instruction
   /// @returns an identifier expression, or null on error
-  ast::Expression* GetImageExpression(const spvtools::opt::Instruction& inst);
+  const ast::Expression* GetImageExpression(
+      const spvtools::opt::Instruction& inst);
 
   /// Get the expression for the sampler operand from the first operand to the
   /// given instruction.
   /// @param inst the SPIR-V instruction
   /// @returns an identifier expression, or null on error
-  ast::Expression* GetSamplerExpression(const spvtools::opt::Instruction& inst);
+  const ast::Expression* GetSamplerExpression(
+      const spvtools::opt::Instruction& inst);
 
   /// Emits a texture builtin function call for a SPIR-V instruction that
   /// accesses an image or sampled image.
@@ -1059,7 +1062,7 @@
   /// @param texel the texel
   /// @param texture_type the type of the storage texture
   /// @returns the texel, after necessary conversion.
-  ast::Expression* ConvertTexelForStorage(
+  const ast::Expression* ConvertTexelForStorage(
       const spvtools::opt::Instruction& inst,
       TypedExpression texel,
       const Texture* texture_type);
@@ -1087,7 +1090,7 @@
   /// Does nothing if the statement is null.
   /// @param statement the new statement
   /// @returns a pointer to the statement.
-  ast::Statement* AddStatement(ast::Statement* statement);
+  const ast::Statement* AddStatement(const ast::Statement* statement);
 
   /// AddStatementBuilder() constructs and adds the StatementBuilder of type
   /// `T` to the top of the statement stack.
@@ -1106,7 +1109,7 @@
   Source GetSourceForInst(const spvtools::opt::Instruction& inst) const;
 
   /// @returns the last statetment in the top of the statement stack.
-  ast::Statement* LastStatement();
+  const ast::Statement* LastStatement();
 
   using CompletionAction = std::function<void(const ast::StatementList&)>;
 
@@ -1131,7 +1134,7 @@
 
     /// Add() adds `statement` to the block.
     /// Add() must not be called after calling Finalize().
-    void Add(ast::Statement* statement);
+    void Add(const ast::Statement* statement);
 
     /// AddStatementBuilder() constructs and adds the StatementBuilder of type
     /// `T` to the block.
@@ -1168,7 +1171,7 @@
     /// The ID of the block at which the completion action should be triggered
     /// and this statement block discarded. This is often the `end_id` of
     /// `construct` itself.
-    uint32_t const end_id_;
+    const uint32_t end_id_;
     /// The completion action finishes processing this statement block.
     FunctionEmitter::CompletionAction const completion_action_;
     /// The list of statements being built, if this construct is not a switch.
@@ -1200,10 +1203,10 @@
   void PushTrueGuard(uint32_t end_id);
 
   /// @returns a boolean true expression.
-  ast::Expression* MakeTrue(const Source&) const;
+  const ast::Expression* MakeTrue(const Source&) const;
 
   /// @returns a boolean false expression.
-  ast::Expression* MakeFalse(const Source&) const;
+  const ast::Expression* MakeFalse(const Source&) const;
 
   /// @param expr the expression to take the address of
   /// @returns a TypedExpression that is the address-of `expr` (`&expr`)
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index 8961756..b5ff8fc 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -262,7 +262,8 @@
 
 TypedExpression& TypedExpression::operator=(const TypedExpression&) = default;
 
-TypedExpression::TypedExpression(const Type* type_in, ast::Expression* expr_in)
+TypedExpression::TypedExpression(const Type* type_in,
+                                 const ast::Expression* expr_in)
     : type(type_in), expr(expr_in) {}
 
 ParserImpl::ParserImpl(const std::vector<uint32_t>& spv_binary)
@@ -1208,7 +1209,7 @@
   return result;
 }
 
-void ParserImpl::AddTypeDecl(Symbol name, ast::TypeDecl* decl) {
+void ParserImpl::AddTypeDecl(Symbol name, const ast::TypeDecl* decl) {
   auto iter = declared_types_.insert(name);
   if (iter.second) {
     builder_.AST().AddTypeDecl(decl);
@@ -1521,7 +1522,7 @@
 
     auto* ast_store_type = ast_type->As<Pointer>()->type;
     auto ast_storage_class = ast_type->As<Pointer>()->storage_class;
-    ast::Expression* ast_constructor = nullptr;
+    const ast::Expression* ast_constructor = nullptr;
     if (var.NumInOperands() > 1) {
       // SPIR-V initializers are always constants.
       // (OpenCL also allows the ID of an OpVariable, but we don't handle that
@@ -1543,7 +1544,7 @@
     // Make sure the variable has a name.
     namer_.SuggestSanitizedName(builtin_position_.per_vertex_var_id,
                                 "gl_Position");
-    ast::Expression* ast_constructor = nullptr;
+    const ast::Expression* ast_constructor = nullptr;
     if (builtin_position_.per_vertex_var_init_id) {
       // The initializer is complex.
       const auto* init =
@@ -1603,7 +1604,7 @@
                                         ast::StorageClass sc,
                                         const Type* storage_type,
                                         bool is_const,
-                                        ast::Expression* constructor,
+                                        const ast::Expression* constructor,
                                         ast::DecorationList decorations) {
   if (storage_type == nullptr) {
     Fail() << "internal error: can't make ast::Variable for null type";
@@ -1755,8 +1756,9 @@
   return result;
 }
 
-ast::Decoration* ParserImpl::SetLocation(ast::DecorationList* decos,
-                                         ast::Decoration* replacement) {
+const ast::Decoration* ParserImpl::SetLocation(
+    ast::DecorationList* decos,
+    const ast::Decoration* replacement) {
   if (!replacement) {
     return nullptr;
   }
@@ -1765,7 +1767,7 @@
       // Replace this location decoration with the replacement.
       // The old one doesn't leak because it's kept in the builder's AST node
       // list.
-      ast::Decoration* result = nullptr;
+      const ast::Decoration* result = nullptr;
       result = deco;
       deco = replacement;
       return result;  // Assume there is only one such decoration.
@@ -2004,7 +2006,7 @@
   return {};
 }
 
-ast::Expression* ParserImpl::MakeNullValue(const Type* type) {
+const ast::Expression* ParserImpl::MakeNullValue(const Type* type) {
   // TODO(dneto): Use the no-operands constructor syntax when it becomes
   // available in Tint.
   // https://github.com/gpuweb/gpuweb/issues/685
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index d07bb77..9895439 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -73,7 +73,7 @@
   /// Constructor
   /// @param type_in the type of the expression
   /// @param expr_in the expression
-  TypedExpression(const Type* type_in, ast::Expression* expr_in);
+  TypedExpression(const Type* type_in, const ast::Expression* expr_in);
 
   /// Assignment operator
   /// @returns this TypedExpression
@@ -83,9 +83,9 @@
   operator bool() const { return type && expr; }
 
   /// The type
-  Type const* type = nullptr;
+  const Type* type = nullptr;
   /// The expression
-  ast::Expression* expr = nullptr;
+  const ast::Expression* expr = nullptr;
 };
 
 /// Info about the WorkgroupSize builtin.
@@ -208,7 +208,7 @@
   /// Adds `decl` as a declared type if it hasn't been added yet.
   /// @param name the type's unique name
   /// @param decl the type declaration to add
-  void AddTypeDecl(Symbol name, ast::TypeDecl* decl);
+  void AddTypeDecl(Symbol name, const ast::TypeDecl* decl);
 
   /// @returns the fail stream object
   FailStream& fail_stream() { return fail_stream_; }
@@ -269,8 +269,8 @@
   /// @param replacement the location decoration to place into the list
   /// @returns the location decoration that was replaced, if one was replaced,
   /// or null otherwise.
-  ast::Decoration* SetLocation(ast::DecorationList* decos,
-                               ast::Decoration* replacement);
+  const ast::Decoration* SetLocation(ast::DecorationList* decos,
+                                     const ast::Decoration* replacement);
 
   /// Converts a SPIR-V struct member decoration into a number of AST
   /// decorations. If the decoration is recognized but deliberately dropped,
@@ -429,7 +429,7 @@
                               ast::StorageClass sc,
                               const Type* storage_type,
                               bool is_const,
-                              ast::Expression* constructor,
+                              const ast::Expression* constructor,
                               ast::DecorationList decorations);
 
   /// Returns true if a constant expression can be generated.
@@ -456,7 +456,7 @@
   /// Creates an AST expression node for the null value for the given type.
   /// @param type the AST type
   /// @returns a new expression
-  ast::Expression* MakeNullValue(const Type* type);
+  const ast::Expression* MakeNullValue(const Type* type);
 
   /// Make a typed expression for the null value for the given type.
   /// @param type the AST type
diff --git a/src/reader/spirv/parser_impl_test_helper.cc b/src/reader/spirv/parser_impl_test_helper.cc
index 79ea5d0..7a1dd99 100644
--- a/src/reader/spirv/parser_impl_test_helper.cc
+++ b/src/reader/spirv/parser_impl_test_helper.cc
@@ -47,7 +47,7 @@
 std::string ToString(const Program& program, const ast::StatementList& stmts) {
   writer::wgsl::GeneratorImpl writer(&program);
   for (const auto* stmt : stmts) {
-    if (!writer.EmitStatement(const_cast<ast::Statement*>(stmt))) {
+    if (!writer.EmitStatement(stmt)) {
       return "WGSL writer error: " + writer.error();
     }
   }
@@ -58,17 +58,17 @@
   writer::wgsl::GeneratorImpl writer(&program);
   if (auto* expr = node->As<ast::Expression>()) {
     std::stringstream out;
-    if (!writer.EmitExpression(out, const_cast<ast::Expression*>(expr))) {
+    if (!writer.EmitExpression(out, expr)) {
       return "WGSL writer error: " + writer.error();
     }
     return out.str();
   } else if (auto* stmt = node->As<ast::Statement>()) {
-    if (!writer.EmitStatement(const_cast<ast::Statement*>(stmt))) {
+    if (!writer.EmitStatement(stmt)) {
       return "WGSL writer error: " + writer.error();
     }
   } else if (auto* ty = node->As<ast::Type>()) {
     std::stringstream out;
-    if (!writer.EmitType(out, const_cast<ast::Type*>(ty))) {
+    if (!writer.EmitType(out, ty)) {
       return "WGSL writer error: " + writer.error();
     }
     return out.str();
diff --git a/src/reader/spirv/parser_type.cc b/src/reader/spirv/parser_type.cc
index a8e03c6..90f250b 100644
--- a/src/reader/spirv/parser_type.cc
+++ b/src/reader/spirv/parser_type.cc
@@ -131,23 +131,23 @@
   return a.dims == b.dims && a.format == b.format;
 }
 
-ast::Type* Void::Build(ProgramBuilder& b) const {
+const ast::Type* Void::Build(ProgramBuilder& b) const {
   return b.ty.void_();
 }
 
-ast::Type* Bool::Build(ProgramBuilder& b) const {
+const ast::Type* Bool::Build(ProgramBuilder& b) const {
   return b.ty.bool_();
 }
 
-ast::Type* U32::Build(ProgramBuilder& b) const {
+const ast::Type* U32::Build(ProgramBuilder& b) const {
   return b.ty.u32();
 }
 
-ast::Type* F32::Build(ProgramBuilder& b) const {
+const ast::Type* F32::Build(ProgramBuilder& b) const {
   return b.ty.f32();
 }
 
-ast::Type* I32::Build(ProgramBuilder& b) const {
+const ast::Type* I32::Build(ProgramBuilder& b) const {
   return b.ty.i32();
 }
 
@@ -155,7 +155,7 @@
     : type(t), storage_class(s) {}
 Pointer::Pointer(const Pointer&) = default;
 
-ast::Type* Pointer::Build(ProgramBuilder& b) const {
+const ast::Type* Pointer::Build(ProgramBuilder& b) const {
   return b.ty.pointer(type->Build(b), storage_class);
 }
 
@@ -163,14 +163,14 @@
     : type(t), storage_class(s) {}
 Reference::Reference(const Reference&) = default;
 
-ast::Type* Reference::Build(ProgramBuilder& b) const {
+const ast::Type* Reference::Build(ProgramBuilder& b) const {
   return type->Build(b);
 }
 
 Vector::Vector(const Type* t, uint32_t s) : type(t), size(s) {}
 Vector::Vector(const Vector&) = default;
 
-ast::Type* Vector::Build(ProgramBuilder& b) const {
+const ast::Type* Vector::Build(ProgramBuilder& b) const {
   return b.ty.vec(type->Build(b), size);
 }
 
@@ -178,7 +178,7 @@
     : type(t), columns(c), rows(r) {}
 Matrix::Matrix(const Matrix&) = default;
 
-ast::Type* Matrix::Build(ProgramBuilder& b) const {
+const ast::Type* Matrix::Build(ProgramBuilder& b) const {
   return b.ty.mat(type->Build(b), columns, rows);
 }
 
@@ -186,7 +186,7 @@
     : type(t), size(sz), stride(st) {}
 Array::Array(const Array&) = default;
 
-ast::Type* Array::Build(ProgramBuilder& b) const {
+const ast::Type* Array::Build(ProgramBuilder& b) const {
   if (size > 0) {
     return b.ty.array(type->Build(b), size, stride);
   } else {
@@ -197,7 +197,7 @@
 Sampler::Sampler(ast::SamplerKind k) : kind(k) {}
 Sampler::Sampler(const Sampler&) = default;
 
-ast::Type* Sampler::Build(ProgramBuilder& b) const {
+const ast::Type* Sampler::Build(ProgramBuilder& b) const {
   return b.ty.sampler(kind);
 }
 
@@ -207,7 +207,7 @@
 DepthTexture::DepthTexture(ast::TextureDimension d) : Base(d) {}
 DepthTexture::DepthTexture(const DepthTexture&) = default;
 
-ast::Type* DepthTexture::Build(ProgramBuilder& b) const {
+const ast::Type* DepthTexture::Build(ProgramBuilder& b) const {
   return b.ty.depth_texture(dims);
 }
 
@@ -216,7 +216,7 @@
 DepthMultisampledTexture::DepthMultisampledTexture(
     const DepthMultisampledTexture&) = default;
 
-ast::Type* DepthMultisampledTexture::Build(ProgramBuilder& b) const {
+const ast::Type* DepthMultisampledTexture::Build(ProgramBuilder& b) const {
   return b.ty.depth_multisampled_texture(dims);
 }
 
@@ -224,7 +224,7 @@
     : Base(d), type(t) {}
 MultisampledTexture::MultisampledTexture(const MultisampledTexture&) = default;
 
-ast::Type* MultisampledTexture::Build(ProgramBuilder& b) const {
+const ast::Type* MultisampledTexture::Build(ProgramBuilder& b) const {
   return b.ty.multisampled_texture(dims, type->Build(b));
 }
 
@@ -232,7 +232,7 @@
     : Base(d), type(t) {}
 SampledTexture::SampledTexture(const SampledTexture&) = default;
 
-ast::Type* SampledTexture::Build(ProgramBuilder& b) const {
+const ast::Type* SampledTexture::Build(ProgramBuilder& b) const {
   return b.ty.sampled_texture(dims, type->Build(b));
 }
 
@@ -242,7 +242,7 @@
     : Base(d), format(f), access(a) {}
 StorageTexture::StorageTexture(const StorageTexture&) = default;
 
-ast::Type* StorageTexture::Build(ProgramBuilder& b) const {
+const ast::Type* StorageTexture::Build(ProgramBuilder& b) const {
   return b.ty.storage_texture(dims, format, access);
 }
 
@@ -253,7 +253,7 @@
 Alias::Alias(Symbol n, const Type* ty) : Base(n), type(ty) {}
 Alias::Alias(const Alias&) = default;
 
-ast::Type* Alias::Build(ProgramBuilder& b) const {
+const ast::Type* Alias::Build(ProgramBuilder& b) const {
   return b.ty.type_name(name);
 }
 
@@ -261,7 +261,7 @@
 Struct::Struct(const Struct&) = default;
 Struct::~Struct() = default;
 
-ast::Type* Struct::Build(ProgramBuilder& b) const {
+const ast::Type* Struct::Build(ProgramBuilder& b) const {
   return b.ty.type_name(name);
 }
 
diff --git a/src/reader/spirv/parser_type.h b/src/reader/spirv/parser_type.h
index fad7161..4466a63 100644
--- a/src/reader/spirv/parser_type.h
+++ b/src/reader/spirv/parser_type.h
@@ -44,7 +44,7 @@
  public:
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  virtual ast::Type* Build(ProgramBuilder& b) const = 0;
+  virtual const ast::Type* Build(ProgramBuilder& b) const = 0;
 
   /// @returns the inner most store type if this is a pointer, `this` otherwise
   const Type* UnwrapPtr() const;
@@ -95,7 +95,7 @@
 struct Void : public Castable<Void, Type> {
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -107,7 +107,7 @@
 struct Bool : public Castable<Bool, Type> {
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -119,7 +119,7 @@
 struct U32 : public Castable<U32, Type> {
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -131,7 +131,7 @@
 struct F32 : public Castable<F32, Type> {
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -143,7 +143,7 @@
 struct I32 : public Castable<I32, Type> {
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -164,7 +164,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -192,7 +192,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -218,7 +218,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -228,7 +228,7 @@
   /// the element type
   Type const* const type;
   /// the number of elements in the vector
-  uint32_t const size;
+  const uint32_t size;
 };
 
 /// `matNxM<T>` type
@@ -245,7 +245,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -255,9 +255,9 @@
   /// the matrix element type
   Type const* const type;
   /// the number of columns in the matrix
-  uint32_t const columns;
+  const uint32_t columns;
   /// the number of rows in the matrix
-  uint32_t const rows;
+  const uint32_t rows;
 };
 
 /// `array<T, N>` type
@@ -275,7 +275,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -285,9 +285,9 @@
   /// the element type
   Type const* const type;
   /// the number of elements in the array. 0 represents runtime-sized array.
-  uint32_t const size;
+  const uint32_t size;
   /// the byte stride of the array
-  uint32_t const stride;
+  const uint32_t stride;
 };
 
 /// `sampler` type
@@ -302,7 +302,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -339,7 +339,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -360,7 +360,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -381,7 +381,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -405,7 +405,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -430,7 +430,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
 #ifndef NDEBUG
   /// @returns a string representation of the type, for debug purposes only
@@ -463,7 +463,7 @@
 #endif  // NDEBUG
 
   /// the type name
-  Symbol const name;
+  const Symbol name;
 };
 
 /// `type T = N` type
@@ -479,7 +479,7 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
   /// the aliased type
   Type const* const type;
@@ -501,10 +501,10 @@
 
   /// @param b the ProgramBuilder used to construct the AST types
   /// @returns the constructed ast::Type node for the given type
-  ast::Type* Build(ProgramBuilder& b) const override;
+  const ast::Type* Build(ProgramBuilder& b) const override;
 
   /// the member types
-  TypeList const members;
+  const TypeList members;
 };
 
 /// A manager of types
diff --git a/src/reader/wgsl/parser_impl.cc b/src/reader/wgsl/parser_impl.cc
index 57c20ac..6ab154a 100644
--- a/src/reader/wgsl/parser_impl.cc
+++ b/src/reader/wgsl/parser_impl.cc
@@ -216,7 +216,7 @@
 
 ParserImpl::TypedIdentifier::TypedIdentifier(const TypedIdentifier&) = default;
 
-ParserImpl::TypedIdentifier::TypedIdentifier(ast::Type* type_in,
+ParserImpl::TypedIdentifier::TypedIdentifier(const ast::Type* type_in,
                                              std::string name_in,
                                              Source source_in)
     : type(type_in), name(std::move(name_in)), source(std::move(source_in)) {}
@@ -230,7 +230,7 @@
 ParserImpl::FunctionHeader::FunctionHeader(Source src,
                                            std::string n,
                                            ast::VariableList p,
-                                           ast::Type* ret_ty,
+                                           const ast::Type* ret_ty,
                                            ast::DecorationList ret_decos)
     : source(src),
       name(n),
@@ -251,7 +251,7 @@
                                      std::string name_in,
                                      ast::StorageClass storage_class_in,
                                      ast::Access access_in,
-                                     ast::Type* type_in)
+                                     const ast::Type* type_in)
     : source(std::move(source_in)),
       name(std::move(name_in)),
       storage_class(storage_class_in),
@@ -407,7 +407,7 @@
       if (!expect("type alias", Token::Type::kSemicolon))
         return Failure::kErrored;
 
-      builder_.AST().AddTypeDecl(const_cast<ast::Alias*>(ta.value));
+      builder_.AST().AddTypeDecl(ta.value);
       return true;
     }
 
@@ -478,7 +478,7 @@
 // global_variable_decl
 //  : variable_decoration_list* variable_decl
 //  | variable_decoration_list* variable_decl EQUAL const_expr
-Maybe<ast::Variable*> ParserImpl::global_variable_decl(
+Maybe<const ast::Variable*> ParserImpl::global_variable_decl(
     ast::DecorationList& decos) {
   auto decl = variable_decl();
   if (decl.errored)
@@ -486,7 +486,7 @@
   if (!decl.matched)
     return Failure::kNoMatch;
 
-  ast::Expression* constructor = nullptr;
+  const ast::Expression* constructor = nullptr;
   if (match(Token::Type::kEqual)) {
     auto expr = expect_const_expr();
     if (expr.errored)
@@ -509,7 +509,7 @@
 //  : attribute_list* LET variable_ident_decl global_const_initializer?
 // global_const_initializer
 //  : EQUAL const_expr
-Maybe<ast::Variable*> ParserImpl::global_constant_decl(
+Maybe<const ast::Variable*> ParserImpl::global_constant_decl(
     ast::DecorationList& decos) {
   if (!match(Token::Type::kLet)) {
     return Failure::kNoMatch;
@@ -571,7 +571,7 @@
 //  | multisampled_texture_type LESS_THAN type_decl GREATER_THAN
 //  | storage_texture_type LESS_THAN image_storage_type
 //                         COMMA access GREATER_THAN
-Maybe<ast::Type*> ParserImpl::texture_sampler_types() {
+Maybe<const ast::Type*> ParserImpl::texture_sampler_types() {
   auto type = sampler_type();
   if (type.matched)
     return type;
@@ -646,7 +646,7 @@
 // sampler_type
 //  : SAMPLER
 //  | SAMPLER_COMPARISON
-Maybe<ast::Type*> ParserImpl::sampler_type() {
+Maybe<const ast::Type*> ParserImpl::sampler_type() {
   Source source;
   if (match(Token::Type::kSampler, &source))
     return builder_.ty.sampler(source, ast::SamplerKind::kSampler);
@@ -664,7 +664,7 @@
 //  | TEXTURE_SAMPLED_3D
 //  | TEXTURE_SAMPLED_CUBE
 //  | TEXTURE_SAMPLED_CUBE_ARRAY
-Maybe<ast::TextureDimension> ParserImpl::sampled_texture_type() {
+Maybe<const ast::TextureDimension> ParserImpl::sampled_texture_type() {
   if (match(Token::Type::kTextureSampled1d))
     return ast::TextureDimension::k1d;
 
@@ -688,7 +688,7 @@
 
 // external_texture_type
 //  : TEXTURE_EXTERNAL
-Maybe<ast::Type*> ParserImpl::external_texture_type() {
+Maybe<const ast::Type*> ParserImpl::external_texture_type() {
   Source source;
   if (match(Token::Type::kTextureExternal, &source)) {
     return builder_.ty.external_texture(source);
@@ -699,7 +699,7 @@
 
 // multisampled_texture_type
 //  : TEXTURE_MULTISAMPLED_2D
-Maybe<ast::TextureDimension> ParserImpl::multisampled_texture_type() {
+Maybe<const ast::TextureDimension> ParserImpl::multisampled_texture_type() {
   if (match(Token::Type::kTextureMultisampled2d))
     return ast::TextureDimension::k2d;
 
@@ -711,7 +711,7 @@
 //  | TEXTURE_STORAGE_2D
 //  | TEXTURE_STORAGE_2D_ARRAY
 //  | TEXTURE_STORAGE_3D
-Maybe<ast::TextureDimension> ParserImpl::storage_texture_type() {
+Maybe<const ast::TextureDimension> ParserImpl::storage_texture_type() {
   if (match(Token::Type::kTextureStorage1d))
     return ast::TextureDimension::k1d;
   if (match(Token::Type::kTextureStorage2d))
@@ -730,7 +730,7 @@
 //  | TEXTURE_DEPTH_CUBE
 //  | TEXTURE_DEPTH_CUBE_ARRAY
 //  | TEXTURE_DEPTH_MULTISAMPLED_2D
-Maybe<ast::Type*> ParserImpl::depth_texture_type() {
+Maybe<const ast::Type*> ParserImpl::depth_texture_type() {
   Source source;
   if (match(Token::Type::kTextureDepth2d, &source)) {
     return builder_.ty.depth_texture(source, ast::TextureDimension::k2d);
@@ -979,7 +979,7 @@
 
 // type_alias
 //   : TYPE IDENT EQUAL type_decl
-Maybe<ast::Alias*> ParserImpl::type_alias() {
+Maybe<const ast::Alias*> ParserImpl::type_alias() {
   if (!peek_is(Token::Type::kType))
     return Failure::kNoMatch;
 
@@ -1029,7 +1029,7 @@
 //   | MAT4x3 LESS_THAN type_decl GREATER_THAN
 //   | MAT4x4 LESS_THAN type_decl GREATER_THAN
 //   | texture_sampler_types
-Maybe<ast::Type*> ParserImpl::type_decl() {
+Maybe<const ast::Type*> ParserImpl::type_decl() {
   auto decos = decoration_list();
   if (decos.errored)
     return Failure::kErrored;
@@ -1048,7 +1048,7 @@
   return type;
 }
 
-Maybe<ast::Type*> ParserImpl::type_decl(ast::DecorationList& decos) {
+Maybe<const ast::Type*> ParserImpl::type_decl(ast::DecorationList& decos) {
   auto t = peek();
   Source source;
   if (match(Token::Type::kIdentifier, &source)) {
@@ -1104,7 +1104,7 @@
   return Failure::kNoMatch;
 }
 
-Expect<ast::Type*> ParserImpl::expect_type(const std::string& use) {
+Expect<const ast::Type*> ParserImpl::expect_type(const std::string& use) {
   auto type = type_decl();
   if (type.errored)
     return Failure::kErrored;
@@ -1113,13 +1113,13 @@
   return type.value;
 }
 
-Expect<ast::Type*> ParserImpl::expect_type_decl_pointer(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_pointer(Token t) {
   const char* use = "ptr declaration";
 
   auto storage_class = ast::StorageClass::kNone;
   auto access = ast::Access::kUndefined;
 
-  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<ast::Type*> {
+  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<const ast::Type*> {
     auto sc = expect_storage_class(use);
     if (sc.errored) {
       return Failure::kErrored;
@@ -1154,7 +1154,7 @@
                              storage_class, access);
 }
 
-Expect<ast::Type*> ParserImpl::expect_type_decl_atomic(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_atomic(Token t) {
   const char* use = "atomic declaration";
 
   auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
@@ -1165,7 +1165,7 @@
   return builder_.ty.atomic(make_source_range_from(t.source()), subtype.value);
 }
 
-Expect<ast::Type*> ParserImpl::expect_type_decl_vector(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_vector(Token t) {
   uint32_t count = 2;
   if (t.Is(Token::Type::kVec3))
     count = 3;
@@ -1182,14 +1182,14 @@
                          count);
 }
 
-Expect<ast::Type*> ParserImpl::expect_type_decl_array(
+Expect<const ast::Type*> ParserImpl::expect_type_decl_array(
     Token t,
     ast::DecorationList decos) {
   const char* use = "array declaration";
 
-  ast::Expression* size = nullptr;
+  const ast::Expression* size = nullptr;
 
-  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<ast::Type*> {
+  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<const ast::Type*> {
     auto type = expect_type(use);
     if (type.errored)
       return Failure::kErrored;
@@ -1216,7 +1216,7 @@
                            size, std::move(decos));
 }
 
-Expect<ast::Type*> ParserImpl::expect_type_decl_matrix(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_matrix(Token t) {
   uint32_t rows = 2;
   uint32_t columns = 2;
   if (t.IsMat3xN()) {
@@ -1276,7 +1276,7 @@
 
 // struct_decl
 //   : struct_decoration_decl* STRUCT IDENT struct_body_decl
-Maybe<ast::Struct*> ParserImpl::struct_decl(ast::DecorationList& decos) {
+Maybe<const ast::Struct*> ParserImpl::struct_decl(ast::DecorationList& decos) {
   auto t = peek();
   auto source = t.source();
 
@@ -1351,7 +1351,8 @@
 
 // function_decl
 //   : function_header body_stmt
-Maybe<ast::Function*> ParserImpl::function_decl(ast::DecorationList& decos) {
+Maybe<const ast::Function*> ParserImpl::function_decl(
+    ast::DecorationList& decos) {
   auto header = function_header();
   if (header.errored) {
     if (sync_to(Token::Type::kBraceLeft, /* consume: */ false)) {
@@ -1412,7 +1413,7 @@
     }
   }
 
-  ast::Type* return_type = nullptr;
+  const ast::Type* return_type = nullptr;
   ast::DecorationList return_decorations;
 
   if (match(Token::Type::kArrow)) {
@@ -1556,8 +1557,8 @@
 
 // paren_rhs_stmt
 //   : PAREN_LEFT logical_or_expression PAREN_RIGHT
-Expect<ast::Expression*> ParserImpl::expect_paren_rhs_stmt() {
-  return expect_paren_block("", [&]() -> Expect<ast::Expression*> {
+Expect<const ast::Expression*> ParserImpl::expect_paren_rhs_stmt() {
+  return expect_paren_block("", [&]() -> Expect<const ast::Expression*> {
     auto expr = logical_or_expression();
     if (expr.errored)
       return Failure::kErrored;
@@ -1606,7 +1607,7 @@
 //      | continue_stmt SEMICOLON
 //      | DISCARD SEMICOLON
 //      | assignment_stmt SEMICOLON
-Maybe<ast::Statement*> ParserImpl::statement() {
+Maybe<const ast::Statement*> ParserImpl::statement() {
   while (match(Token::Type::kSemicolon)) {
     // Skip empty statements
   }
@@ -1662,8 +1663,8 @@
 //   | continue_stmt SEMICOLON
 //   | DISCARD SEMICOLON
 //   | assignment_stmt SEMICOLON
-Maybe<ast::Statement*> ParserImpl::non_block_statement() {
-  auto stmt = [&]() -> Maybe<ast::Statement*> {
+Maybe<const ast::Statement*> ParserImpl::non_block_statement() {
+  auto stmt = [&]() -> Maybe<const ast::Statement*> {
     auto ret_stmt = return_stmt();
     if (ret_stmt.errored)
       return Failure::kErrored;
@@ -1715,7 +1716,7 @@
 
 // return_stmt
 //   : RETURN logical_or_expression?
-Maybe<ast::ReturnStatement*> ParserImpl::return_stmt() {
+Maybe<const ast::ReturnStatement*> ParserImpl::return_stmt() {
   Source source;
   if (!match(Token::Type::kReturn, &source))
     return Failure::kNoMatch;
@@ -1735,7 +1736,7 @@
 //   : variable_decl
 //   | variable_decl EQUAL logical_or_expression
 //   | CONST variable_ident_decl EQUAL logical_or_expression
-Maybe<ast::VariableDeclStatement*> ParserImpl::variable_stmt() {
+Maybe<const ast::VariableDeclStatement*> ParserImpl::variable_stmt() {
   if (match(Token::Type::kLet)) {
     auto decl = expect_variable_ident_decl("let declaration",
                                            /*allow_inferred = */ true);
@@ -1770,7 +1771,7 @@
   if (!decl.matched)
     return Failure::kNoMatch;
 
-  ast::Expression* constructor = nullptr;
+  const ast::Expression* constructor = nullptr;
   if (match(Token::Type::kEqual)) {
     auto constructor_expr = logical_or_expression();
     if (constructor_expr.errored)
@@ -1796,7 +1797,7 @@
 
 // if_stmt
 //   : IF paren_rhs_stmt body_stmt elseif_stmt? else_stmt?
-Maybe<ast::IfStatement*> ParserImpl::if_stmt() {
+Maybe<const ast::IfStatement*> ParserImpl::if_stmt() {
   Source source;
   if (!match(Token::Type::kIf, &source))
     return Failure::kNoMatch;
@@ -1852,7 +1853,7 @@
 
 // else_stmt
 //   : ELSE body_stmt
-Maybe<ast::ElseStatement*> ParserImpl::else_stmt() {
+Maybe<const ast::ElseStatement*> ParserImpl::else_stmt() {
   Source source;
   if (!match(Token::Type::kElse, &source))
     return Failure::kNoMatch;
@@ -1866,7 +1867,7 @@
 
 // switch_stmt
 //   : SWITCH paren_rhs_stmt BRACKET_LEFT switch_body+ BRACKET_RIGHT
-Maybe<ast::SwitchStatement*> ParserImpl::switch_stmt() {
+Maybe<const ast::SwitchStatement*> ParserImpl::switch_stmt() {
   Source source;
   if (!match(Token::Type::kSwitch, &source))
     return Failure::kNoMatch;
@@ -1903,7 +1904,7 @@
 // switch_body
 //   : CASE case_selectors COLON BRACKET_LEFT case_body BRACKET_RIGHT
 //   | DEFAULT COLON BRACKET_LEFT case_body BRACKET_RIGHT
-Maybe<ast::CaseStatement*> ParserImpl::switch_body() {
+Maybe<const ast::CaseStatement*> ParserImpl::switch_body() {
   if (!peek_is(Token::Type::kCase) && !peek_is(Token::Type::kDefault))
     return Failure::kNoMatch;
 
@@ -1967,7 +1968,7 @@
 //   :
 //   | statement case_body
 //   | FALLTHROUGH SEMICOLON
-Maybe<ast::BlockStatement*> ParserImpl::case_body() {
+Maybe<const ast::BlockStatement*> ParserImpl::case_body() {
   ast::StatementList stmts;
   while (continue_parsing()) {
     Source source;
@@ -1993,12 +1994,12 @@
 
 // loop_stmt
 //   : LOOP BRACKET_LEFT statements continuing_stmt? BRACKET_RIGHT
-Maybe<ast::LoopStatement*> ParserImpl::loop_stmt() {
+Maybe<const ast::LoopStatement*> ParserImpl::loop_stmt() {
   Source source;
   if (!match(Token::Type::kLoop, &source))
     return Failure::kNoMatch;
 
-  return expect_brace_block("loop", [&]() -> Maybe<ast::LoopStatement*> {
+  return expect_brace_block("loop", [&]() -> Maybe<const ast::LoopStatement*> {
     auto stmts = expect_statements();
     if (stmts.errored)
       return Failure::kErrored;
@@ -2012,15 +2013,15 @@
   });
 }
 
-ForHeader::ForHeader(ast::Statement* init,
-                     ast::Expression* cond,
-                     ast::Statement* cont)
+ForHeader::ForHeader(const ast::Statement* init,
+                     const ast::Expression* cond,
+                     const ast::Statement* cont)
     : initializer(init), condition(cond), continuing(cont) {}
 
 ForHeader::~ForHeader() = default;
 
 // (variable_stmt | assignment_stmt | func_call_stmt)?
-Maybe<ast::Statement*> ParserImpl::for_header_initializer() {
+Maybe<const ast::Statement*> ParserImpl::for_header_initializer() {
   auto call = func_call_stmt();
   if (call.errored)
     return Failure::kErrored;
@@ -2043,7 +2044,7 @@
 }
 
 // (assignment_stmt | func_call_stmt)?
-Maybe<ast::Statement*> ParserImpl::for_header_continuing() {
+Maybe<const ast::Statement*> ParserImpl::for_header_continuing() {
   auto call_stmt = func_call_stmt();
   if (call_stmt.errored)
     return Failure::kErrored;
@@ -2089,7 +2090,7 @@
 
 // for_statement
 //   : FOR PAREN_LEFT for_header PAREN_RIGHT BRACE_LEFT statements BRACE_RIGHT
-Maybe<ast::ForLoopStatement*> ParserImpl::for_stmt() {
+Maybe<const ast::ForLoopStatement*> ParserImpl::for_stmt() {
   Source source;
   if (!match(Token::Type::kFor, &source))
     return Failure::kNoMatch;
@@ -2111,7 +2112,7 @@
 
 // func_call_stmt
 //    : IDENT argument_expression_list
-Maybe<ast::CallStatement*> ParserImpl::func_call_stmt() {
+Maybe<const ast::CallStatement*> ParserImpl::func_call_stmt() {
   auto t = peek();
   auto t2 = peek(1);
   if (!t.IsIdentifier() || !t2.Is(Token::Type::kParenLeft))
@@ -2136,7 +2137,7 @@
 
 // break_stmt
 //   : BREAK
-Maybe<ast::BreakStatement*> ParserImpl::break_stmt() {
+Maybe<const ast::BreakStatement*> ParserImpl::break_stmt() {
   Source source;
   if (!match(Token::Type::kBreak, &source))
     return Failure::kNoMatch;
@@ -2146,7 +2147,7 @@
 
 // continue_stmt
 //   : CONTINUE
-Maybe<ast::ContinueStatement*> ParserImpl::continue_stmt() {
+Maybe<const ast::ContinueStatement*> ParserImpl::continue_stmt() {
   Source source;
   if (!match(Token::Type::kContinue, &source))
     return Failure::kNoMatch;
@@ -2156,7 +2157,7 @@
 
 // continuing_stmt
 //   : CONTINUING body_stmt
-Maybe<ast::BlockStatement*> ParserImpl::continuing_stmt() {
+Maybe<const ast::BlockStatement*> ParserImpl::continuing_stmt() {
   if (!match(Token::Type::kContinuing))
     return create<ast::BlockStatement>(Source{}, ast::StatementList{});
 
@@ -2169,7 +2170,7 @@
 //   | const_literal
 //   | paren_rhs_stmt
 //   | BITCAST LESS_THAN type_decl GREATER_THAN paren_rhs_stmt
-Maybe<ast::Expression*> ParserImpl::primary_expression() {
+Maybe<const ast::Expression*> ParserImpl::primary_expression() {
   auto t = peek();
   auto source = t.source();
 
@@ -2238,8 +2239,8 @@
 //   :
 //   | BRACE_LEFT logical_or_expression BRACE_RIGHT postfix_expr
 //   | PERIOD IDENTIFIER postfix_expr
-Maybe<ast::Expression*> ParserImpl::postfix_expression(
-    ast::Expression* prefix) {
+Maybe<const ast::Expression*> ParserImpl::postfix_expression(
+    const ast::Expression* prefix) {
   Source source;
 
   while (continue_parsing()) {
@@ -2252,8 +2253,8 @@
     }
 
     if (match(Token::Type::kBracketLeft, &source)) {
-      auto res =
-          sync(Token::Type::kBracketRight, [&]() -> Maybe<ast::Expression*> {
+      auto res = sync(
+          Token::Type::kBracketRight, [&]() -> Maybe<const ast::Expression*> {
             auto param = logical_or_expression();
             if (param.errored)
               return Failure::kErrored;
@@ -2297,7 +2298,7 @@
 
 // singular_expression
 //   : primary_expression postfix_expr
-Maybe<ast::Expression*> ParserImpl::singular_expression() {
+Maybe<const ast::Expression*> ParserImpl::singular_expression() {
   auto prefix = primary_expression();
   if (prefix.errored)
     return Failure::kErrored;
@@ -2338,7 +2339,7 @@
 //   | TILDE unary_expression
 //   | STAR unary_expression
 //   | AND unary_expression
-Maybe<ast::Expression*> ParserImpl::unary_expression() {
+Maybe<const ast::Expression*> ParserImpl::unary_expression() {
   auto t = peek();
 
   if (match(Token::Type::kPlusPlus) || match(Token::Type::kMinusMinus)) {
@@ -2391,8 +2392,8 @@
 //   | STAR unary_expression multiplicative_expr
 //   | FORWARD_SLASH unary_expression multiplicative_expr
 //   | MODULO unary_expression multiplicative_expr
-Expect<ast::Expression*> ParserImpl::expect_multiplicative_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_multiplicative_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     ast::BinaryOp op = ast::BinaryOp::kNone;
     if (peek_is(Token::Type::kStar))
@@ -2423,7 +2424,7 @@
 
 // multiplicative_expression
 //   : unary_expression multiplicative_expr
-Maybe<ast::Expression*> ParserImpl::multiplicative_expression() {
+Maybe<const ast::Expression*> ParserImpl::multiplicative_expression() {
   auto lhs = unary_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2437,8 +2438,8 @@
 //   :
 //   | PLUS multiplicative_expression additive_expr
 //   | MINUS multiplicative_expression additive_expr
-Expect<ast::Expression*> ParserImpl::expect_additive_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_additive_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     ast::BinaryOp op = ast::BinaryOp::kNone;
     if (peek_is(Token::Type::kPlus))
@@ -2464,7 +2465,7 @@
 
 // additive_expression
 //   : multiplicative_expression additive_expr
-Maybe<ast::Expression*> ParserImpl::additive_expression() {
+Maybe<const ast::Expression*> ParserImpl::additive_expression() {
   auto lhs = multiplicative_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2478,7 +2479,8 @@
 //   :
 //   | SHIFT_LEFT additive_expression shift_expr
 //   | SHIFT_RIGHT additive_expression shift_expr
-Expect<ast::Expression*> ParserImpl::expect_shift_expr(ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_shift_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     auto* name = "";
     ast::BinaryOp op = ast::BinaryOp::kNone;
@@ -2509,7 +2511,7 @@
 
 // shift_expression
 //   : additive_expression shift_expr
-Maybe<ast::Expression*> ParserImpl::shift_expression() {
+Maybe<const ast::Expression*> ParserImpl::shift_expression() {
   auto lhs = additive_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2525,8 +2527,8 @@
 //   | GREATER_THAN shift_expression relational_expr
 //   | LESS_THAN_EQUAL shift_expression relational_expr
 //   | GREATER_THAN_EQUAL shift_expression relational_expr
-Expect<ast::Expression*> ParserImpl::expect_relational_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_relational_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     ast::BinaryOp op = ast::BinaryOp::kNone;
     if (peek_is(Token::Type::kLessThan))
@@ -2559,7 +2561,7 @@
 
 // relational_expression
 //   : shift_expression relational_expr
-Maybe<ast::Expression*> ParserImpl::relational_expression() {
+Maybe<const ast::Expression*> ParserImpl::relational_expression() {
   auto lhs = shift_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2573,8 +2575,8 @@
 //   :
 //   | EQUAL_EQUAL relational_expression equality_expr
 //   | NOT_EQUAL relational_expression equality_expr
-Expect<ast::Expression*> ParserImpl::expect_equality_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_equality_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     ast::BinaryOp op = ast::BinaryOp::kNone;
     if (peek_is(Token::Type::kEqualEqual))
@@ -2603,7 +2605,7 @@
 
 // equality_expression
 //   : relational_expression equality_expr
-Maybe<ast::Expression*> ParserImpl::equality_expression() {
+Maybe<const ast::Expression*> ParserImpl::equality_expression() {
   auto lhs = relational_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2616,7 +2618,8 @@
 // and_expr
 //   :
 //   | AND equality_expression and_expr
-Expect<ast::Expression*> ParserImpl::expect_and_expr(ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_and_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     if (!peek_is(Token::Type::kAnd)) {
       return lhs;
@@ -2639,7 +2642,7 @@
 
 // and_expression
 //   : equality_expression and_expr
-Maybe<ast::Expression*> ParserImpl::and_expression() {
+Maybe<const ast::Expression*> ParserImpl::and_expression() {
   auto lhs = equality_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2652,8 +2655,8 @@
 // exclusive_or_expr
 //   :
 //   | XOR and_expression exclusive_or_expr
-Expect<ast::Expression*> ParserImpl::expect_exclusive_or_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_exclusive_or_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     Source source;
     if (!match(Token::Type::kXor, &source))
@@ -2673,7 +2676,7 @@
 
 // exclusive_or_expression
 //   : and_expression exclusive_or_expr
-Maybe<ast::Expression*> ParserImpl::exclusive_or_expression() {
+Maybe<const ast::Expression*> ParserImpl::exclusive_or_expression() {
   auto lhs = and_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2686,8 +2689,8 @@
 // inclusive_or_expr
 //   :
 //   | OR exclusive_or_expression inclusive_or_expr
-Expect<ast::Expression*> ParserImpl::expect_inclusive_or_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_inclusive_or_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     Source source;
     if (!match(Token::Type::kOr))
@@ -2707,7 +2710,7 @@
 
 // inclusive_or_expression
 //   : exclusive_or_expression inclusive_or_expr
-Maybe<ast::Expression*> ParserImpl::inclusive_or_expression() {
+Maybe<const ast::Expression*> ParserImpl::inclusive_or_expression() {
   auto lhs = exclusive_or_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2720,8 +2723,8 @@
 // logical_and_expr
 //   :
 //   | AND_AND inclusive_or_expression logical_and_expr
-Expect<ast::Expression*> ParserImpl::expect_logical_and_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_logical_and_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     if (!peek_is(Token::Type::kAndAnd)) {
       return lhs;
@@ -2744,7 +2747,7 @@
 
 // logical_and_expression
 //   : inclusive_or_expression logical_and_expr
-Maybe<ast::Expression*> ParserImpl::logical_and_expression() {
+Maybe<const ast::Expression*> ParserImpl::logical_and_expression() {
   auto lhs = inclusive_or_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2757,8 +2760,8 @@
 // logical_or_expr
 //   :
 //   | OR_OR logical_and_expression logical_or_expr
-Expect<ast::Expression*> ParserImpl::expect_logical_or_expr(
-    ast::Expression* lhs) {
+Expect<const ast::Expression*> ParserImpl::expect_logical_or_expr(
+    const ast::Expression* lhs) {
   while (continue_parsing()) {
     Source source;
     if (!match(Token::Type::kOrOr))
@@ -2778,7 +2781,7 @@
 
 // logical_or_expression
 //   : logical_and_expression logical_or_expr
-Maybe<ast::Expression*> ParserImpl::logical_or_expression() {
+Maybe<const ast::Expression*> ParserImpl::logical_or_expression() {
   auto lhs = logical_and_expression();
   if (lhs.errored)
     return Failure::kErrored;
@@ -2790,7 +2793,7 @@
 
 // assignment_stmt
 //   : unary_expression EQUAL logical_or_expression
-Maybe<ast::AssignmentStatement*> ParserImpl::assignment_stmt() {
+Maybe<const ast::AssignmentStatement*> ParserImpl::assignment_stmt() {
   auto t = peek();
   auto source = t.source();
 
@@ -2826,7 +2829,7 @@
 //   | FLOAT_LITERAL
 //   | TRUE
 //   | FALSE
-Maybe<ast::Literal*> ParserImpl::const_literal() {
+Maybe<const ast::Literal*> ParserImpl::const_literal() {
   auto t = peek();
   if (t.IsError()) {
     return add_error(t.source(), t.to_str());
@@ -2979,7 +2982,7 @@
   });
 }
 
-Expect<ast::Decoration*> ParserImpl::expect_decoration() {
+Expect<const ast::Decoration*> ParserImpl::expect_decoration() {
   auto t = peek();
   auto deco = decoration();
   if (deco.errored)
@@ -2989,8 +2992,8 @@
   return add_error(t, "expected decoration");
 }
 
-Maybe<ast::Decoration*> ParserImpl::decoration() {
-  using Result = Maybe<ast::Decoration*>;
+Maybe<const ast::Decoration*> ParserImpl::decoration() {
+  using Result = Maybe<const ast::Decoration*>;
   auto t = next();
 
   if (!t.IsIdentifier()) {
@@ -3083,9 +3086,9 @@
 
   if (s == kWorkgroupSizeDecoration) {
     return expect_paren_block("workgroup_size decoration", [&]() -> Result {
-      ast::Expression* x = nullptr;
-      ast::Expression* y = nullptr;
-      ast::Expression* z = nullptr;
+      const ast::Expression* x = nullptr;
+      const ast::Expression* y = nullptr;
+      const ast::Expression* z = nullptr;
 
       auto expr = primary_expression();
       if (expr.errored) {
@@ -3187,7 +3190,7 @@
   return Failure::kNoMatch;
 }
 
-bool ParserImpl::expect_decorations_consumed(const ast::DecorationList& in) {
+bool ParserImpl::expect_decorations_consumed(ast::DecorationList& in) {
   if (in.empty()) {
     return true;
   }
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
index 33a6d64..40335aa 100644
--- a/src/reader/wgsl/parser_impl.h
+++ b/src/reader/wgsl/parser_impl.h
@@ -52,16 +52,18 @@
   /// @param init the initializer statement
   /// @param cond the condition statement
   /// @param cont the continuing statement
-  ForHeader(ast::Statement* init, ast::Expression* cond, ast::Statement* cont);
+  ForHeader(const ast::Statement* init,
+            const ast::Expression* cond,
+            const ast::Statement* cont);
 
   ~ForHeader();
 
   /// The for loop initializer
-  ast::Statement* initializer = nullptr;
+  const ast::Statement* initializer = nullptr;
   /// The for loop condition
-  ast::Expression* condition = nullptr;
+  const ast::Expression* condition = nullptr;
   /// The for loop continuing statement
-  ast::Statement* continuing = nullptr;
+  const ast::Statement* continuing = nullptr;
 };
 
 /// ParserImpl for WGSL source data
@@ -210,12 +212,14 @@
     /// @param type_in parsed type
     /// @param name_in parsed identifier
     /// @param source_in source to the identifier
-    TypedIdentifier(ast::Type* type_in, std::string name_in, Source source_in);
+    TypedIdentifier(const ast::Type* type_in,
+                    std::string name_in,
+                    Source source_in);
     /// Destructor
     ~TypedIdentifier();
 
     /// Parsed type. May be nullptr for inferred types.
-    ast::Type* type = nullptr;
+    const ast::Type* type = nullptr;
     /// Parsed identifier.
     std::string name;
     /// Source to the identifier.
@@ -238,7 +242,7 @@
     FunctionHeader(Source src,
                    std::string n,
                    ast::VariableList p,
-                   ast::Type* ret_ty,
+                   const ast::Type* ret_ty,
                    ast::DecorationList ret_decos);
     /// Destructor
     ~FunctionHeader();
@@ -254,7 +258,7 @@
     /// Function parameters
     ast::VariableList params;
     /// Function return type
-    ast::Type* return_type;
+    const ast::Type* return_type = nullptr;
     /// Function return type decorations
     ast::DecorationList return_type_decorations;
   };
@@ -276,7 +280,7 @@
                 std::string name_in,
                 ast::StorageClass storage_class_in,
                 ast::Access access_in,
-                ast::Type* type_in);
+                const ast::Type* type_in);
     /// Destructor
     ~VarDeclInfo();
 
@@ -289,7 +293,7 @@
     /// Variable access control
     ast::Access access = ast::Access::kUndefined;
     /// Variable type
-    ast::Type* type = nullptr;
+    const ast::Type* type = nullptr;
   };
 
   /// VariableQualifier contains the parsed information for a variable qualifier
@@ -396,12 +400,12 @@
   /// `variable_decoration_list*` provided as `decos`
   /// @returns the variable parsed or nullptr
   /// @param decos the list of decorations for the variable declaration.
-  Maybe<ast::Variable*> global_variable_decl(ast::DecorationList& decos);
+  Maybe<const ast::Variable*> global_variable_decl(ast::DecorationList& decos);
   /// Parses a `global_constant_decl` grammar element with the initial
   /// `variable_decoration_list*` provided as `decos`
   /// @returns the const object or nullptr
   /// @param decos the list of decorations for the constant declaration.
-  Maybe<ast::Variable*> global_constant_decl(ast::DecorationList& decos);
+  Maybe<const ast::Variable*> global_constant_decl(ast::DecorationList& decos);
   /// Parses a `variable_decl` grammar element
   /// @param allow_inferred if true, do not fail if variable decl does not
   /// specify type
@@ -421,15 +425,15 @@
   Maybe<VariableQualifier> variable_qualifier();
   /// Parses a `type_alias` grammar element
   /// @returns the type alias or nullptr on error
-  Maybe<ast::Alias*> type_alias();
+  Maybe<const ast::Alias*> type_alias();
   /// Parses a `type_decl` grammar element
   /// @returns the parsed Type or nullptr if none matched.
-  Maybe<ast::Type*> type_decl();
+  Maybe<const ast::Type*> type_decl();
   /// Parses a `type_decl` grammar element with the given pre-parsed
   /// decorations.
   /// @param decos the list of decorations for the type.
   /// @returns the parsed Type or nullptr if none matched.
-  Maybe<ast::Type*> type_decl(ast::DecorationList& decos);
+  Maybe<const ast::Type*> type_decl(ast::DecorationList& decos);
   /// Parses a `storage_class` grammar element, erroring on parse failure.
   /// @param use a description of what was being parsed if an error was raised.
   /// @returns the storage class or StorageClass::kNone if none matched
@@ -438,7 +442,7 @@
   /// `struct_decoration_decl*` provided as `decos`.
   /// @returns the struct type or nullptr on error
   /// @param decos the list of decorations for the struct declaration.
-  Maybe<ast::Struct*> struct_decl(ast::DecorationList& decos);
+  Maybe<const ast::Struct*> struct_decl(ast::DecorationList& decos);
   /// Parses a `struct_body_decl` grammar element, erroring on parse failure.
   /// @returns the struct members
   Expect<ast::StructMemberList> expect_struct_body_decl();
@@ -452,30 +456,30 @@
   /// `function_decoration_decl*` provided as `decos`.
   /// @param decos the list of decorations for the function declaration.
   /// @returns the parsed function, nullptr otherwise
-  Maybe<ast::Function*> function_decl(ast::DecorationList& decos);
+  Maybe<const ast::Function*> function_decl(ast::DecorationList& decos);
   /// Parses a `texture_sampler_types` grammar element
   /// @returns the parsed Type or nullptr if none matched.
-  Maybe<ast::Type*> texture_sampler_types();
+  Maybe<const ast::Type*> texture_sampler_types();
   /// Parses a `sampler_type` grammar element
   /// @returns the parsed Type or nullptr if none matched.
-  Maybe<ast::Type*> sampler_type();
+  Maybe<const ast::Type*> sampler_type();
   /// Parses a `multisampled_texture_type` grammar element
   /// @returns returns the multisample texture dimension or kNone if none
   /// matched.
-  Maybe<ast::TextureDimension> multisampled_texture_type();
+  Maybe<const ast::TextureDimension> multisampled_texture_type();
   /// Parses a `sampled_texture_type` grammar element
   /// @returns returns the sample texture dimension or kNone if none matched.
-  Maybe<ast::TextureDimension> sampled_texture_type();
+  Maybe<const ast::TextureDimension> sampled_texture_type();
   /// Parses a `storage_texture_type` grammar element
   /// @returns returns the storage texture dimension.
   /// Returns kNone if none matched.
-  Maybe<ast::TextureDimension> storage_texture_type();
+  Maybe<const ast::TextureDimension> storage_texture_type();
   /// Parses a `depth_texture_type` grammar element
   /// @returns the parsed Type or nullptr if none matched.
-  Maybe<ast::Type*> depth_texture_type();
+  Maybe<const ast::Type*> depth_texture_type();
   /// Parses a 'texture_external_type' grammar element
   /// @returns the parsed Type or nullptr if none matched
-  Maybe<ast::Type*> external_texture_type();
+  Maybe<const ast::Type*> external_texture_type();
   /// Parses a `image_storage_type` grammar element
   /// @param use a description of what was being parsed if an error was raised
   /// @returns returns the image format or kNone if none matched.
@@ -507,70 +511,70 @@
   Expect<ast::BlockStatement*> expect_body_stmt();
   /// Parses a `paren_rhs_stmt` grammar element, erroring on parse failure.
   /// @returns the parsed element or nullptr
-  Expect<ast::Expression*> expect_paren_rhs_stmt();
+  Expect<const ast::Expression*> expect_paren_rhs_stmt();
   /// Parses a `statements` grammar element
   /// @returns the statements parsed
   Expect<ast::StatementList> expect_statements();
   /// Parses a `statement` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::Statement*> statement();
+  Maybe<const ast::Statement*> statement();
   /// Parses a `break_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::BreakStatement*> break_stmt();
+  Maybe<const ast::BreakStatement*> break_stmt();
   /// Parses a `return_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::ReturnStatement*> return_stmt();
+  Maybe<const ast::ReturnStatement*> return_stmt();
   /// Parses a `continue_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::ContinueStatement*> continue_stmt();
+  Maybe<const ast::ContinueStatement*> continue_stmt();
   /// Parses a `variable_stmt` grammar element
   /// @returns the parsed variable or nullptr
-  Maybe<ast::VariableDeclStatement*> variable_stmt();
+  Maybe<const ast::VariableDeclStatement*> variable_stmt();
   /// Parses a `if_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::IfStatement*> if_stmt();
+  Maybe<const ast::IfStatement*> if_stmt();
   /// Parses a `elseif_stmt` grammar element
   /// @returns the parsed elements
   Maybe<ast::ElseStatementList> elseif_stmt();
   /// Parses a `else_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::ElseStatement*> else_stmt();
+  Maybe<const ast::ElseStatement*> else_stmt();
   /// Parses a `switch_stmt` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::SwitchStatement*> switch_stmt();
+  Maybe<const ast::SwitchStatement*> switch_stmt();
   /// Parses a `switch_body` grammar element
   /// @returns the parsed statement or nullptr
-  Maybe<ast::CaseStatement*> switch_body();
+  Maybe<const ast::CaseStatement*> switch_body();
   /// Parses a `case_selectors` grammar element
   /// @returns the list of literals
   Expect<ast::CaseSelectorList> expect_case_selectors();
   /// Parses a `case_body` grammar element
   /// @returns the parsed statements
-  Maybe<ast::BlockStatement*> case_body();
+  Maybe<const ast::BlockStatement*> case_body();
   /// Parses a `func_call_stmt` grammar element
   /// @returns the parsed function call or nullptr
-  Maybe<ast::CallStatement*> func_call_stmt();
+  Maybe<const ast::CallStatement*> func_call_stmt();
   /// Parses a `loop_stmt` grammar element
   /// @returns the parsed loop or nullptr
-  Maybe<ast::LoopStatement*> loop_stmt();
+  Maybe<const ast::LoopStatement*> loop_stmt();
   /// Parses a `for_header` grammar element, erroring on parse failure.
   /// @returns the parsed for header or nullptr
   Expect<std::unique_ptr<ForHeader>> expect_for_header();
   /// Parses a `for_stmt` grammar element
   /// @returns the parsed for loop or nullptr
-  Maybe<ast::ForLoopStatement*> for_stmt();
+  Maybe<const ast::ForLoopStatement*> for_stmt();
   /// Parses a `continuing_stmt` grammar element
   /// @returns the parsed statements
-  Maybe<ast::BlockStatement*> continuing_stmt();
+  Maybe<const ast::BlockStatement*> continuing_stmt();
   /// Parses a `const_literal` grammar element
   /// @returns the const literal parsed or nullptr if none found
-  Maybe<ast::Literal*> const_literal();
+  Maybe<const ast::Literal*> const_literal();
   /// Parses a `const_expr` grammar element, erroring on parse failure.
   /// @returns the parsed constructor expression or nullptr on error
   Expect<ast::ConstructorExpression*> expect_const_expr();
   /// Parses a `primary_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> primary_expression();
+  Maybe<const ast::Expression*> primary_expression();
   /// Parses a `argument_expression_list` grammar element, erroring on parse
   /// failure.
   /// @param use a description of what was being parsed if an error was raised
@@ -580,96 +584,105 @@
   /// Parses the recursive portion of the postfix_expression
   /// @param prefix the left side of the expression
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> postfix_expression(ast::Expression* prefix);
+  Maybe<const ast::Expression*> postfix_expression(
+      const ast::Expression* prefix);
   /// Parses a `singular_expression` grammar elment
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> singular_expression();
+  Maybe<const ast::Expression*> singular_expression();
   /// Parses a `unary_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> unary_expression();
+  Maybe<const ast::Expression*> unary_expression();
   /// Parses the recursive part of the `multiplicative_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_multiplicative_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_multiplicative_expr(
+      const ast::Expression* lhs);
   /// Parses the `multiplicative_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> multiplicative_expression();
+  Maybe<const ast::Expression*> multiplicative_expression();
   /// Parses the recursive part of the `additive_expression`, erroring on parse
   /// failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_additive_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_additive_expr(
+      const ast::Expression* lhs);
   /// Parses the `additive_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> additive_expression();
+  Maybe<const ast::Expression*> additive_expression();
   /// Parses the recursive part of the `shift_expression`, erroring on parse
   /// failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_shift_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_shift_expr(const ast::Expression* lhs);
   /// Parses the `shift_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> shift_expression();
+  Maybe<const ast::Expression*> shift_expression();
   /// Parses the recursive part of the `relational_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_relational_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_relational_expr(
+      const ast::Expression* lhs);
   /// Parses the `relational_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> relational_expression();
+  Maybe<const ast::Expression*> relational_expression();
   /// Parses the recursive part of the `equality_expression`, erroring on parse
   /// failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_equality_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_equality_expr(
+      const ast::Expression* lhs);
   /// Parses the `equality_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> equality_expression();
+  Maybe<const ast::Expression*> equality_expression();
   /// Parses the recursive part of the `and_expression`, erroring on parse
   /// failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_and_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_and_expr(const ast::Expression* lhs);
   /// Parses the `and_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> and_expression();
+  Maybe<const ast::Expression*> and_expression();
   /// Parses the recursive part of the `exclusive_or_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_exclusive_or_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_exclusive_or_expr(
+      const ast::Expression* lhs);
   /// Parses the `exclusive_or_expression` grammar elememnt
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> exclusive_or_expression();
+  Maybe<const ast::Expression*> exclusive_or_expression();
   /// Parses the recursive part of the `inclusive_or_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_inclusive_or_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_inclusive_or_expr(
+      const ast::Expression* lhs);
   /// Parses the `inclusive_or_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> inclusive_or_expression();
+  Maybe<const ast::Expression*> inclusive_or_expression();
   /// Parses the recursive part of the `logical_and_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_logical_and_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_logical_and_expr(
+      const ast::Expression* lhs);
   /// Parses a `logical_and_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> logical_and_expression();
+  Maybe<const ast::Expression*> logical_and_expression();
   /// Parses the recursive part of the `logical_or_expression`, erroring on
   /// parse failure.
   /// @param lhs the left side of the expression
   /// @returns the parsed expression or nullptr
-  Expect<ast::Expression*> expect_logical_or_expr(ast::Expression* lhs);
+  Expect<const ast::Expression*> expect_logical_or_expr(
+      const ast::Expression* lhs);
   /// Parses a `logical_or_expression` grammar element
   /// @returns the parsed expression or nullptr
-  Maybe<ast::Expression*> logical_or_expression();
+  Maybe<const ast::Expression*> logical_or_expression();
   /// Parses a `assignment_stmt` grammar element
   /// @returns the parsed assignment or nullptr
-  Maybe<ast::AssignmentStatement*> assignment_stmt();
+  Maybe<const ast::AssignmentStatement*> assignment_stmt();
   /// Parses one or more bracketed decoration lists.
   /// @return the parsed decoration list, or an empty list on error.
   Maybe<ast::DecorationList> decoration_list();
@@ -686,12 +699,12 @@
   /// * `global_const_decoration`
   /// * `function_decoration`
   /// @return the parsed decoration, or nullptr.
-  Maybe<ast::Decoration*> decoration();
+  Maybe<const ast::Decoration*> decoration();
   /// Parses a single decoration, reporting an error if the next token does not
   /// represent a decoration.
   /// @see #decoration for the full list of decorations this method parses.
   /// @return the parsed decoration, or nullptr on error.
-  Expect<ast::Decoration*> expect_decoration();
+  Expect<const ast::Decoration*> expect_decoration();
 
  private:
   /// ReturnType resolves to the return type for the function or lambda F.
@@ -845,19 +858,20 @@
 
   /// Reports an error if the decoration list `list` is not empty.
   /// Used to ensure that all decorations are consumed.
-  bool expect_decorations_consumed(const ast::DecorationList& list);
+  bool expect_decorations_consumed(ast::DecorationList& list);
 
-  Expect<ast::Type*> expect_type_decl_pointer(Token t);
-  Expect<ast::Type*> expect_type_decl_atomic(Token t);
-  Expect<ast::Type*> expect_type_decl_vector(Token t);
-  Expect<ast::Type*> expect_type_decl_array(Token t, ast::DecorationList decos);
-  Expect<ast::Type*> expect_type_decl_matrix(Token t);
+  Expect<const ast::Type*> expect_type_decl_pointer(Token t);
+  Expect<const ast::Type*> expect_type_decl_atomic(Token t);
+  Expect<const ast::Type*> expect_type_decl_vector(Token t);
+  Expect<const ast::Type*> expect_type_decl_array(Token t,
+                                                  ast::DecorationList decos);
+  Expect<const ast::Type*> expect_type_decl_matrix(Token t);
 
-  Expect<ast::Type*> expect_type(const std::string& use);
+  Expect<const ast::Type*> expect_type(const std::string& use);
 
-  Maybe<ast::Statement*> non_block_statement();
-  Maybe<ast::Statement*> for_header_initializer();
-  Maybe<ast::Statement*> for_header_continuing();
+  Maybe<const ast::Statement*> non_block_statement();
+  Maybe<const ast::Statement*> for_header_initializer();
+  Maybe<const ast::Statement*> for_header_continuing();
 
   class MultiTokenSource;
   MultiTokenSource make_source_range();
diff --git a/src/reader/wgsl/parser_impl_function_decoration_list_test.cc b/src/reader/wgsl/parser_impl_function_decoration_list_test.cc
index dae3f9a..1a5924a 100644
--- a/src/reader/wgsl/parser_impl_function_decoration_list_test.cc
+++ b/src/reader/wgsl/parser_impl_function_decoration_list_test.cc
@@ -34,7 +34,7 @@
   ASSERT_NE(deco_1, nullptr);
 
   ASSERT_TRUE(deco_0->Is<ast::WorkgroupDecoration>());
-  ast::Expression* x = deco_0->As<ast::WorkgroupDecoration>()->x;
+  const ast::Expression* x = deco_0->As<ast::WorkgroupDecoration>()->x;
   ASSERT_NE(x, nullptr);
   auto* x_scalar = x->As<ast::ScalarConstructorExpression>();
   ASSERT_NE(x_scalar, nullptr);
diff --git a/src/resolver/intrinsic_test.cc b/src/resolver/intrinsic_test.cc
index 677b64a..0b13c32 100644
--- a/src/resolver/intrinsic_test.cc
+++ b/src/resolver/intrinsic_test.cc
@@ -224,7 +224,8 @@
   /// @param dim dimensionality of the texture being sampled
   /// @param scalar the scalar type
   /// @returns a pointer to a type appropriate for the coord param
-  ast::Type* GetCoordsType(ast::TextureDimension dim, ast::Type* scalar) {
+  const ast::Type* GetCoordsType(ast::TextureDimension dim,
+                                 const ast::Type* scalar) {
     switch (dim) {
       case ast::TextureDimension::k1d:
         return scalar;
@@ -257,7 +258,7 @@
 
     call_params->push_back(Expr(name));
   }
-  ast::Type* subtype(Texture type) {
+  const ast::Type* subtype(Texture type) {
     if (type == Texture::kF32) {
       return ty.f32();
     }
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 19f461a..369a4ce 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -304,8 +304,7 @@
     }
     if (auto* t = ty->As<ast::Vector>()) {
       if (auto* el = Type(t->type)) {
-        if (auto* vector = builder_->create<sem::Vector>(
-                const_cast<sem::Type*>(el), t->width)) {
+        if (auto* vector = builder_->create<sem::Vector>(el, t->width)) {
           if (ValidateVector(vector, t->source)) {
             return vector;
           }
@@ -315,8 +314,7 @@
     }
     if (auto* t = ty->As<ast::Matrix>()) {
       if (auto* el = Type(t->type)) {
-        if (auto* column_type = builder_->create<sem::Vector>(
-                const_cast<sem::Type*>(el), t->rows)) {
+        if (auto* column_type = builder_->create<sem::Vector>(el, t->rows)) {
           if (auto* matrix =
                   builder_->create<sem::Matrix>(column_type, t->columns)) {
             if (ValidateMatrix(matrix, t->source)) {
@@ -332,7 +330,7 @@
     }
     if (auto* t = ty->As<ast::Atomic>()) {
       if (auto* el = Type(t->type)) {
-        auto* a = builder_->create<sem::Atomic>(const_cast<sem::Type*>(el));
+        auto* a = builder_->create<sem::Atomic>(el);
         if (!ValidateAtomic(t, a)) {
           return nullptr;
         }
@@ -346,8 +344,7 @@
         if (access == ast::kUndefined) {
           access = DefaultAccessForStorageClass(t->storage_class);
         }
-        return builder_->create<sem::Pointer>(const_cast<sem::Type*>(el),
-                                              t->storage_class, access);
+        return builder_->create<sem::Pointer>(el, t->storage_class, access);
       }
       return nullptr;
     }
@@ -356,15 +353,13 @@
     }
     if (auto* t = ty->As<ast::SampledTexture>()) {
       if (auto* el = Type(t->type)) {
-        return builder_->create<sem::SampledTexture>(
-            t->dim, const_cast<sem::Type*>(el));
+        return builder_->create<sem::SampledTexture>(t->dim, el);
       }
       return nullptr;
     }
     if (auto* t = ty->As<ast::MultisampledTexture>()) {
       if (auto* el = Type(t->type)) {
-        return builder_->create<sem::MultisampledTexture>(
-            t->dim, const_cast<sem::Type*>(el));
+        return builder_->create<sem::MultisampledTexture>(t->dim, el);
       }
       return nullptr;
     }
@@ -379,8 +374,8 @@
         if (!ValidateStorageTexture(t)) {
           return nullptr;
         }
-        return builder_->create<sem::StorageTexture>(
-            t->dim, t->format, t->access, const_cast<sem::Type*>(el));
+        return builder_->create<sem::StorageTexture>(t->dim, t->format,
+                                                     t->access, el);
       }
       return nullptr;
     }
@@ -447,7 +442,7 @@
   return true;
 }
 
-Resolver::VariableInfo* Resolver::Variable(ast::Variable* var,
+Resolver::VariableInfo* Resolver::Variable(const ast::Variable* var,
                                            VariableKind kind,
                                            uint32_t index /* = 0 */) {
   if (variable_to_info_.count(var)) {
@@ -651,7 +646,7 @@
   return true;
 }
 
-bool Resolver::GlobalVariable(ast::Variable* var) {
+bool Resolver::GlobalVariable(const ast::Variable* var) {
   if (!ValidateNoDuplicateDefinition(var->symbol, var->source,
                                      /* check_global_scope_only */ true)) {
     return false;
@@ -1515,118 +1510,115 @@
   };
 
   // Inner lambda that is applied to a type and all of its members.
-  auto validate_entry_point_decorations_inner =
-      [&](const ast::DecorationList& decos, sem::Type* ty, Source source,
-          ParamOrRetType param_or_ret, bool is_struct_member) {
-        // Scan decorations for pipeline IO attributes.
-        // Check for overlap with attributes that have been seen previously.
-        ast::Decoration* pipeline_io_attribute = nullptr;
-        ast::InvariantDecoration* invariant_attribute = nullptr;
-        for (auto* deco : decos) {
-          auto is_invalid_compute_shader_decoration = false;
-          if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
-            if (pipeline_io_attribute) {
-              AddError("multiple entry point IO attributes", deco->source);
-              AddNote(
-                  "previously consumed " + deco_to_str(pipeline_io_attribute),
+  auto validate_entry_point_decorations_inner = [&](const ast::DecorationList&
+                                                        decos,
+                                                    sem::Type* ty,
+                                                    Source source,
+                                                    ParamOrRetType param_or_ret,
+                                                    bool is_struct_member) {
+    // Scan decorations for pipeline IO attributes.
+    // Check for overlap with attributes that have been seen previously.
+    const ast::Decoration* pipeline_io_attribute = nullptr;
+    const ast::InvariantDecoration* invariant_attribute = nullptr;
+    for (auto* deco : decos) {
+      auto is_invalid_compute_shader_decoration = false;
+      if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
+        if (pipeline_io_attribute) {
+          AddError("multiple entry point IO attributes", deco->source);
+          AddNote("previously consumed " + deco_to_str(pipeline_io_attribute),
                   pipeline_io_attribute->source);
-              return false;
-            }
-            pipeline_io_attribute = deco;
+          return false;
+        }
+        pipeline_io_attribute = deco;
 
-            if (builtins.count(builtin->builtin)) {
-              AddError(
-                  deco_to_str(builtin) +
-                      " attribute appears multiple times as pipeline " +
-                      (param_or_ret == ParamOrRetType::kParameter ? "input"
-                                                                  : "output"),
-                  func->source);
-              return false;
-            }
-
-            if (!ValidateBuiltinDecoration(builtin, ty,
-                                           /* is_input */ param_or_ret ==
-                                               ParamOrRetType::kParameter)) {
-              return false;
-            }
-            builtins.emplace(builtin->builtin);
-          } else if (auto* location = deco->As<ast::LocationDecoration>()) {
-            if (pipeline_io_attribute) {
-              AddError("multiple entry point IO attributes", deco->source);
-              AddNote(
-                  "previously consumed " + deco_to_str(pipeline_io_attribute),
-                  pipeline_io_attribute->source);
-              return false;
-            }
-            pipeline_io_attribute = deco;
-
-            bool is_input = param_or_ret == ParamOrRetType::kParameter;
-            if (!ValidateLocationDecoration(location, ty, locations, source,
-                                            is_input)) {
-              return false;
-            }
-          } else if (auto* interpolate =
-                         deco->As<ast::InterpolateDecoration>()) {
-            if (func->PipelineStage() == ast::PipelineStage::kCompute) {
-              is_invalid_compute_shader_decoration = true;
-            } else if (!ValidateInterpolateDecoration(interpolate, ty)) {
-              return false;
-            }
-          } else if (auto* invariant = deco->As<ast::InvariantDecoration>()) {
-            if (func->PipelineStage() == ast::PipelineStage::kCompute) {
-              is_invalid_compute_shader_decoration = true;
-            }
-            invariant_attribute = invariant;
-          }
-          if (is_invalid_compute_shader_decoration) {
-            std::string input_or_output =
-                param_or_ret == ParamOrRetType::kParameter ? "inputs"
-                                                           : "output";
-            AddError(
-                "decoration is not valid for compute shader " + input_or_output,
-                deco->source);
-            return false;
-          }
+        if (builtins.count(builtin->builtin)) {
+          AddError(deco_to_str(builtin) +
+                       " attribute appears multiple times as pipeline " +
+                       (param_or_ret == ParamOrRetType::kParameter ? "input"
+                                                                   : "output"),
+                   func->source);
+          return false;
         }
 
-        if (IsValidationEnabled(
-                decos, ast::DisabledValidation::kEntryPointParameter)) {
-          if (is_struct_member && ty->Is<sem::Struct>()) {
-            AddError("nested structures cannot be used for entry point IO",
-                     source);
-            return false;
-          }
+        if (!ValidateBuiltinDecoration(
+                builtin, ty,
+                /* is_input */ param_or_ret == ParamOrRetType::kParameter)) {
+          return false;
+        }
+        builtins.emplace(builtin->builtin);
+      } else if (auto* location = deco->As<ast::LocationDecoration>()) {
+        if (pipeline_io_attribute) {
+          AddError("multiple entry point IO attributes", deco->source);
+          AddNote("previously consumed " + deco_to_str(pipeline_io_attribute),
+                  pipeline_io_attribute->source);
+          return false;
+        }
+        pipeline_io_attribute = deco;
 
-          if (!ty->Is<sem::Struct>() && !pipeline_io_attribute) {
-            std::string err = "missing entry point IO attribute";
-            if (!is_struct_member) {
-              err += (param_or_ret == ParamOrRetType::kParameter
-                          ? " on parameter"
-                          : " on return type");
-            }
-            AddError(err, source);
-            return false;
-          }
+        bool is_input = param_or_ret == ParamOrRetType::kParameter;
+        if (!ValidateLocationDecoration(location, ty, locations, source,
+                                        is_input)) {
+          return false;
+        }
+      } else if (auto* interpolate = deco->As<ast::InterpolateDecoration>()) {
+        if (func->PipelineStage() == ast::PipelineStage::kCompute) {
+          is_invalid_compute_shader_decoration = true;
+        } else if (!ValidateInterpolateDecoration(interpolate, ty)) {
+          return false;
+        }
+      } else if (auto* invariant = deco->As<ast::InvariantDecoration>()) {
+        if (func->PipelineStage() == ast::PipelineStage::kCompute) {
+          is_invalid_compute_shader_decoration = true;
+        }
+        invariant_attribute = invariant;
+      }
+      if (is_invalid_compute_shader_decoration) {
+        std::string input_or_output =
+            param_or_ret == ParamOrRetType::kParameter ? "inputs" : "output";
+        AddError(
+            "decoration is not valid for compute shader " + input_or_output,
+            deco->source);
+        return false;
+      }
+    }
 
-          if (invariant_attribute) {
-            bool has_position = false;
-            if (pipeline_io_attribute) {
-              if (auto* builtin =
-                      pipeline_io_attribute->As<ast::BuiltinDecoration>()) {
-                has_position = (builtin->builtin == ast::Builtin::kPosition);
-              }
-            }
-            if (!has_position) {
-              AddError(
-                  "invariant attribute must only be applied to a position "
-                  "builtin",
-                  invariant_attribute->source);
-              return false;
-            }
+    if (IsValidationEnabled(decos,
+                            ast::DisabledValidation::kEntryPointParameter)) {
+      if (is_struct_member && ty->Is<sem::Struct>()) {
+        AddError("nested structures cannot be used for entry point IO", source);
+        return false;
+      }
+
+      if (!ty->Is<sem::Struct>() && !pipeline_io_attribute) {
+        std::string err = "missing entry point IO attribute";
+        if (!is_struct_member) {
+          err +=
+              (param_or_ret == ParamOrRetType::kParameter ? " on parameter"
+                                                          : " on return type");
+        }
+        AddError(err, source);
+        return false;
+      }
+
+      if (invariant_attribute) {
+        bool has_position = false;
+        if (pipeline_io_attribute) {
+          if (auto* builtin =
+                  pipeline_io_attribute->As<ast::BuiltinDecoration>()) {
+            has_position = (builtin->builtin == ast::Builtin::kPosition);
           }
         }
-        return true;
-      };
+        if (!has_position) {
+          AddError(
+              "invariant attribute must only be applied to a position "
+              "builtin",
+              invariant_attribute->source);
+          return false;
+        }
+      }
+    }
+    return true;
+  };
 
   // Outer lambda for validating the entry point decorations for a type.
   auto validate_entry_point_decorations = [&](const ast::DecorationList& decos,
@@ -1742,7 +1734,7 @@
   return true;
 }
 
-bool Resolver::Function(ast::Function* func) {
+bool Resolver::Function(const ast::Function* func) {
   auto* info = function_infos_.Create<FunctionInfo>(func);
 
   if (func->IsEntryPoint()) {
@@ -2019,7 +2011,7 @@
   return true;
 }
 
-bool Resolver::Statement(ast::Statement* stmt) {
+bool Resolver::Statement(const ast::Statement* stmt) {
   if (stmt->Is<ast::CaseStatement>()) {
     AddError("case statement can only be used inside a switch statement",
              stmt->source);
@@ -2129,7 +2121,7 @@
   return false;
 }
 
-bool Resolver::CaseStatement(ast::CaseStatement* stmt) {
+bool Resolver::CaseStatement(const ast::CaseStatement* stmt) {
   auto* sem = builder_->create<sem::SwitchCaseBlockStatement>(
       stmt->body, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -2141,7 +2133,7 @@
   return Scope(sem, [&] { return Statements(stmt->body->statements); });
 }
 
-bool Resolver::IfStatement(ast::IfStatement* stmt) {
+bool Resolver::IfStatement(const ast::IfStatement* stmt) {
   auto* sem =
       builder_->create<sem::IfStatement>(stmt, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -2177,7 +2169,7 @@
   });
 }
 
-bool Resolver::ElseStatement(ast::ElseStatement* stmt) {
+bool Resolver::ElseStatement(const ast::ElseStatement* stmt) {
   auto* sem =
       builder_->create<sem::ElseStatement>(stmt, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -2205,14 +2197,14 @@
   });
 }
 
-bool Resolver::BlockStatement(ast::BlockStatement* stmt) {
+bool Resolver::BlockStatement(const ast::BlockStatement* stmt) {
   auto* sem = builder_->create<sem::BlockStatement>(
       stmt->As<ast::BlockStatement>(), current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
   return Scope(sem, [&] { return Statements(stmt->statements); });
 }
 
-bool Resolver::LoopStatement(ast::LoopStatement* stmt) {
+bool Resolver::LoopStatement(const ast::LoopStatement* stmt) {
   auto* sem =
       builder_->create<sem::LoopStatement>(stmt, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -2245,7 +2237,7 @@
   });
 }
 
-bool Resolver::ForLoopStatement(ast::ForLoopStatement* stmt) {
+bool Resolver::ForLoopStatement(const ast::ForLoopStatement* stmt) {
   auto* sem = builder_->create<sem::ForLoopStatement>(
       stmt, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -2287,12 +2279,12 @@
   });
 }
 
-bool Resolver::TraverseExpressions(ast::Expression* root,
-                                   std::vector<ast::Expression*>& out) {
-  std::vector<ast::Expression*> to_visit;
+bool Resolver::TraverseExpressions(const ast::Expression* root,
+                                   std::vector<const ast::Expression*>& out) {
+  std::vector<const ast::Expression*> to_visit;
   to_visit.emplace_back(root);
 
-  auto add = [&](ast::Expression* e) {
+  auto add = [&](const ast::Expression* e) {
     Mark(e);
     to_visit.emplace_back(e);
   };
@@ -2336,8 +2328,8 @@
   return true;
 }
 
-bool Resolver::Expression(ast::Expression* root) {
-  std::vector<ast::Expression*> sorted;
+bool Resolver::Expression(const ast::Expression* root) {
+  std::vector<const ast::Expression*> sorted;
   if (!TraverseExpressions(root, sorted)) {
     return false;
   }
@@ -2373,7 +2365,7 @@
   return true;
 }
 
-bool Resolver::ArrayAccessor(ast::ArrayAccessorExpression* expr) {
+bool Resolver::ArrayAccessor(const ast::ArrayAccessorExpression* expr) {
   auto* idx = expr->index;
   auto* res = TypeOf(expr->array);
   auto* parent_type = res->UnwrapRef();
@@ -2420,7 +2412,7 @@
   return true;
 }
 
-bool Resolver::Bitcast(ast::BitcastExpression* expr) {
+bool Resolver::Bitcast(const ast::BitcastExpression* expr) {
   auto* ty = Type(expr->type);
   if (!ty) {
     return false;
@@ -2433,7 +2425,7 @@
   return true;
 }
 
-bool Resolver::Call(ast::CallExpression* call) {
+bool Resolver::Call(const ast::CallExpression* call) {
   Mark(call->func);
   auto* ident = call->func;
   auto name = builder_->Symbols().NameFor(ident->symbol);
@@ -2452,7 +2444,7 @@
   return ValidateCall(call);
 }
 
-bool Resolver::ValidateCall(ast::CallExpression* call) {
+bool Resolver::ValidateCall(const ast::CallExpression* call) {
   if (TypeOf(call)->Is<sem::Void>()) {
     bool is_call_statement = false;
     if (current_statement_) {
@@ -2483,7 +2475,7 @@
   return true;
 }
 
-bool Resolver::ValidateCallStatement(ast::CallStatement* stmt) {
+bool Resolver::ValidateCallStatement(const ast::CallStatement* stmt) {
   const sem::Type* return_type = TypeOf(stmt->expr);
   if (!return_type->Is<sem::Void>()) {
     // https://gpuweb.github.io/gpuweb/wgsl/#function-call-statement
@@ -2501,7 +2493,7 @@
   return true;
 }
 
-bool Resolver::IntrinsicCall(ast::CallExpression* call,
+bool Resolver::IntrinsicCall(const ast::CallExpression* call,
                              sem::IntrinsicType intrinsic_type) {
   std::vector<const sem::Type*> arg_tys;
   arg_tys.reserve(call->args.size());
@@ -2710,7 +2702,7 @@
   return true;
 }
 
-bool Resolver::Constructor(ast::ConstructorExpression* expr) {
+bool Resolver::Constructor(const ast::ConstructorExpression* expr) {
   if (auto* type_ctor = expr->As<ast::TypeConstructorExpression>()) {
     auto* type = Type(type_ctor->type);
     if (!type) {
@@ -2997,7 +2989,7 @@
   return true;
 }
 
-bool Resolver::Identifier(ast::IdentifierExpression* expr) {
+bool Resolver::Identifier(const ast::IdentifierExpression* expr) {
   auto symbol = expr->symbol;
   VariableInfo* var;
   if (variable_stack_.get(symbol, &var)) {
@@ -3061,11 +3053,11 @@
   return false;
 }
 
-bool Resolver::MemberAccessor(ast::MemberAccessorExpression* expr) {
+bool Resolver::MemberAccessor(const ast::MemberAccessorExpression* expr) {
   auto* structure = TypeOf(expr->structure);
   auto* storage_type = structure->UnwrapRef();
 
-  sem::Type* ret = nullptr;
+  const sem::Type* ret = nullptr;
   std::vector<uint32_t> swizzle;
 
   if (auto* str = storage_type->As<sem::Struct>()) {
@@ -3181,7 +3173,7 @@
   return true;
 }
 
-bool Resolver::Binary(ast::BinaryExpression* expr) {
+bool Resolver::Binary(const ast::BinaryExpression* expr) {
   using Bool = sem::Bool;
   using F32 = sem::F32;
   using I32 = sem::I32;
@@ -3189,8 +3181,8 @@
   using Matrix = sem::Matrix;
   using Vector = sem::Vector;
 
-  auto* lhs_type = const_cast<sem::Type*>(TypeOf(expr->lhs)->UnwrapRef());
-  auto* rhs_type = const_cast<sem::Type*>(TypeOf(expr->rhs)->UnwrapRef());
+  auto* lhs_type = TypeOf(expr->lhs)->UnwrapRef();
+  auto* rhs_type = TypeOf(expr->rhs)->UnwrapRef();
 
   auto* lhs_vec = lhs_type->As<Vector>();
   auto* lhs_vec_elem_type = lhs_vec ? lhs_vec->type() : nullptr;
@@ -3386,7 +3378,7 @@
   return false;
 }
 
-bool Resolver::UnaryOp(ast::UnaryOpExpression* unary) {
+bool Resolver::UnaryOp(const ast::UnaryOpExpression* unary) {
   auto* expr_type = TypeOf(unary->expr);
   if (!expr_type) {
     return false;
@@ -3466,7 +3458,7 @@
 }
 
 bool Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
-  ast::Variable* var = stmt->variable;
+  const ast::Variable* var = stmt->variable;
   Mark(var);
 
   if (!ValidateNoDuplicateDefinition(var->symbol, var->source)) {
@@ -3819,8 +3811,8 @@
     }
 
     auto* sem_func = builder_->create<sem::Function>(
-        info->declaration, const_cast<sem::Type*>(info->return_type),
-        parameters, remap_vars(info->referenced_module_vars),
+        info->declaration, info->return_type, parameters,
+        remap_vars(info->referenced_module_vars),
         remap_vars(info->local_referenced_module_vars), info->return_statements,
         info->callsites, ancestor_entry_points[func->symbol],
         info->workgroup_size);
@@ -3845,8 +3837,7 @@
       continue;
     }
     sem.Add(expr, builder_->create<sem::Expression>(
-                      const_cast<ast::Expression*>(expr), info.type,
-                      info.statement, info.constant_value));
+                      expr, info.type, info.statement, info.constant_value));
   }
 }
 
@@ -4057,7 +4048,7 @@
     }
 
     auto has_position = false;
-    ast::InvariantDecoration* invariant_attribute = nullptr;
+    const ast::InvariantDecoration* invariant_attribute = nullptr;
     for (auto* deco : member->Declaration()->decorations) {
       if (!deco->IsAnyOf<ast::BuiltinDecoration,             //
                          ast::InternalDecoration,            //
@@ -4187,7 +4178,7 @@
   // validation.
   uint64_t struct_size = 0;
   uint64_t struct_align = 1;
-  std::unordered_map<Symbol, ast::StructMember*> member_map;
+  std::unordered_map<Symbol, const ast::StructMember*> member_map;
 
   for (auto* member : str->members) {
     Mark(member);
@@ -4275,8 +4266,7 @@
     }
 
     auto* sem_member = builder_->create<sem::StructMember>(
-        member, member->symbol, const_cast<sem::Type*>(type),
-        static_cast<uint32_t>(sem_members.size()),
+        member, member->symbol, type, static_cast<uint32_t>(sem_members.size()),
         static_cast<uint32_t>(offset), static_cast<uint32_t>(align),
         static_cast<uint32_t>(size));
     builder_->Sem().Add(member, sem_member);
@@ -4360,7 +4350,7 @@
   return true;
 }
 
-bool Resolver::Return(ast::ReturnStatement* ret) {
+bool Resolver::Return(const ast::ReturnStatement* ret) {
   current_function_->return_statements.push_back(ret);
 
   if (auto* value = ret->value) {
@@ -4443,7 +4433,7 @@
   return true;
 }
 
-bool Resolver::SwitchStatement(ast::SwitchStatement* stmt) {
+bool Resolver::SwitchStatement(const ast::SwitchStatement* stmt) {
   auto* sem =
       builder_->create<sem::SwitchStatement>(stmt, current_compound_statement_);
   builder_->Sem().Add(stmt, sem);
@@ -4465,7 +4455,7 @@
   });
 }
 
-bool Resolver::Assignment(ast::AssignmentStatement* a) {
+bool Resolver::Assignment(const ast::AssignmentStatement* a) {
   Mark(a->lhs);
   Mark(a->rhs);
 
@@ -4688,7 +4678,8 @@
 
 Resolver::VariableInfo::~VariableInfo() = default;
 
-Resolver::FunctionInfo::FunctionInfo(ast::Function* decl) : declaration(decl) {}
+Resolver::FunctionInfo::FunctionInfo(const ast::Function* decl)
+    : declaration(decl) {}
 Resolver::FunctionInfo::~FunctionInfo() = default;
 
 }  // namespace resolver
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index 3b05f2c..b7f839b 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -112,7 +112,7 @@
     std::string const type_name;
     ast::StorageClass storage_class;
     ast::Access const access;
-    std::vector<ast::IdentifierExpression*> users;
+    std::vector<const ast::IdentifierExpression*> users;
     sem::BindingPoint binding_point;
     VariableKind kind;
     uint32_t index = 0;  // Parameter index, if kind == kParameter
@@ -130,10 +130,10 @@
   /// Structure holding semantic information about a function.
   /// Used to build the sem::Function nodes at the end of resolving.
   struct FunctionInfo {
-    explicit FunctionInfo(ast::Function* decl);
+    explicit FunctionInfo(const ast::Function* decl);
     ~FunctionInfo();
 
-    ast::Function* const declaration;
+    const ast::Function* const declaration;
     std::vector<VariableInfo*> parameters;
     UniqueVector<VariableInfo*> referenced_module_vars;
     UniqueVector<VariableInfo*> local_referenced_module_vars;
@@ -192,7 +192,7 @@
     }
 
     ast::BlockStatement const* const block;
-    Type const type;
+    const Type type;
     BlockInfo* const parent;
     std::vector<const ast::Variable*> decls;
 
@@ -235,31 +235,31 @@
 
   // AST and Type traversal methods
   // Each return true on success, false on failure.
-  bool ArrayAccessor(ast::ArrayAccessorExpression*);
-  bool Assignment(ast::AssignmentStatement* a);
-  bool Binary(ast::BinaryExpression*);
-  bool Bitcast(ast::BitcastExpression*);
-  bool BlockStatement(ast::BlockStatement*);
-  bool Call(ast::CallExpression*);
-  bool CaseStatement(ast::CaseStatement*);
-  bool Constructor(ast::ConstructorExpression*);
-  bool ElseStatement(ast::ElseStatement*);
-  bool Expression(ast::Expression*);
-  bool ForLoopStatement(ast::ForLoopStatement*);
-  bool Function(ast::Function*);
+  bool ArrayAccessor(const ast::ArrayAccessorExpression*);
+  bool Assignment(const ast::AssignmentStatement* a);
+  bool Binary(const ast::BinaryExpression*);
+  bool Bitcast(const ast::BitcastExpression*);
+  bool BlockStatement(const ast::BlockStatement*);
+  bool Call(const ast::CallExpression*);
+  bool CaseStatement(const ast::CaseStatement*);
+  bool Constructor(const ast::ConstructorExpression*);
+  bool ElseStatement(const ast::ElseStatement*);
+  bool Expression(const ast::Expression*);
+  bool ForLoopStatement(const ast::ForLoopStatement*);
+  bool Function(const ast::Function*);
   bool FunctionCall(const ast::CallExpression* call);
-  bool GlobalVariable(ast::Variable* var);
-  bool Identifier(ast::IdentifierExpression*);
-  bool IfStatement(ast::IfStatement*);
-  bool IntrinsicCall(ast::CallExpression*, sem::IntrinsicType);
-  bool LoopStatement(ast::LoopStatement*);
-  bool MemberAccessor(ast::MemberAccessorExpression*);
-  bool Parameter(ast::Variable* param);
-  bool Return(ast::ReturnStatement* ret);
-  bool Statement(ast::Statement*);
+  bool GlobalVariable(const ast::Variable* var);
+  bool Identifier(const ast::IdentifierExpression*);
+  bool IfStatement(const ast::IfStatement*);
+  bool IntrinsicCall(const ast::CallExpression*, sem::IntrinsicType);
+  bool LoopStatement(const ast::LoopStatement*);
+  bool MemberAccessor(const ast::MemberAccessorExpression*);
+  bool Parameter(const ast::Variable* param);
+  bool Return(const ast::ReturnStatement* ret);
+  bool Statement(const ast::Statement*);
   bool Statements(const ast::StatementList&);
-  bool SwitchStatement(ast::SwitchStatement* s);
-  bool UnaryOp(ast::UnaryOpExpression*);
+  bool SwitchStatement(const ast::SwitchStatement* s);
+  bool UnaryOp(const ast::UnaryOpExpression*);
   bool VariableDeclStatement(const ast::VariableDeclStatement*);
 
   /// Performs a depth-first traversal of the expression nodes from `root`,
@@ -268,8 +268,8 @@
   /// @param out the ordered list of visited expression nodes, starting with the
   ///        root node, and ending with leaf nodes
   /// @return true on success, false on error
-  bool TraverseExpressions(ast::Expression* root,
-                           std::vector<ast::Expression*>& out);
+  bool TraverseExpressions(const ast::Expression* root,
+                           std::vector<const ast::Expression*>& out);
 
   // AST and Type validation methods
   // Each return true on success, false on failure.
@@ -284,8 +284,8 @@
   bool ValidateBuiltinDecoration(const ast::BuiltinDecoration* deco,
                                  const sem::Type* storage_type,
                                  const bool is_input);
-  bool ValidateCall(ast::CallExpression* call);
-  bool ValidateCallStatement(ast::CallStatement* stmt);
+  bool ValidateCall(const ast::CallExpression* call);
+  bool ValidateCallStatement(const ast::CallStatement* stmt);
   bool ValidateEntryPoint(const ast::Function* func, const FunctionInfo* info);
   bool ValidateFunction(const ast::Function* func, const FunctionInfo* info);
   bool ValidateFunctionCall(const ast::CallExpression* call,
@@ -371,7 +371,7 @@
   /// @param var the variable to create or return the `VariableInfo` for
   /// @param kind what kind of variable we are declaring
   /// @param index the index of the parameter, if this variable is a parameter
-  VariableInfo* Variable(ast::Variable* var,
+  VariableInfo* Variable(const ast::Variable* var,
                          VariableKind kind,
                          uint32_t index = 0);
 
diff --git a/src/resolver/resolver_test.cc b/src/resolver/resolver_test.cc
index 776cf4c..94bc94a 100644
--- a/src/resolver/resolver_test.cc
+++ b/src/resolver/resolver_test.cc
@@ -1641,9 +1641,9 @@
   uint32_t mat_rows = std::get<2>(GetParam());
   uint32_t mat_cols = std::get<3>(GetParam());
 
-  ast::Type* lhs_type;
-  ast::Type* rhs_type;
-  sem::Type* result_type;
+  const ast::Type* lhs_type = nullptr;
+  const ast::Type* rhs_type = nullptr;
+  const sem::Type* result_type = nullptr;
   bool is_valid_expr;
 
   if (vec_by_mat) {
diff --git a/src/resolver/resolver_test_helper.h b/src/resolver/resolver_test_helper.h
index a0a17c2..9f5ff52 100644
--- a/src/resolver/resolver_test_helper.h
+++ b/src/resolver/resolver_test_helper.h
@@ -45,16 +45,16 @@
   /// @param expr the ast::Expression
   /// @return the ast::Statement of the ast::Expression, or nullptr if the
   /// expression is not owned by a statement.
-  const ast::Statement* StmtOf(ast::Expression* expr) {
+  const ast::Statement* StmtOf(const ast::Expression* expr) {
     auto* sem_stmt = Sem().Get(expr)->Stmt();
     return sem_stmt ? sem_stmt->Declaration() : nullptr;
   }
 
   /// Returns the BlockStatement that holds the given statement.
-  /// @param stmt the ast::Statment
+  /// @param stmt the ast::Statement
   /// @return the ast::BlockStatement that holds the ast::Statement, or nullptr
   /// if the statement is not owned by a BlockStatement.
-  const ast::BlockStatement* BlockOf(ast::Statement* stmt) {
+  const ast::BlockStatement* BlockOf(const ast::Statement* stmt) {
     auto* sem_stmt = Sem().Get(stmt);
     return sem_stmt ? sem_stmt->Block()->Declaration() : nullptr;
   }
@@ -63,7 +63,7 @@
   /// @param expr the ast::Expression
   /// @return the ast::Statement of the ast::Expression, or nullptr if the
   /// expression is not indirectly owned by a BlockStatement.
-  const ast::BlockStatement* BlockOf(ast::Expression* expr) {
+  const ast::BlockStatement* BlockOf(const ast::Expression* expr) {
     auto* sem_stmt = Sem().Get(expr)->Stmt();
     return sem_stmt ? sem_stmt->Block()->Declaration() : nullptr;
   }
@@ -72,7 +72,7 @@
   /// @param expr the identifier expression
   /// @return the resolved sem::Variable of the identifier, or nullptr if
   /// the expression did not resolve to a variable.
-  const sem::Variable* VarOf(ast::Expression* expr) {
+  const sem::Variable* VarOf(const ast::Expression* expr) {
     auto* sem_ident = Sem().Get(expr);
     auto* var_user = sem_ident ? sem_ident->As<sem::VariableUser>() : nullptr;
     return var_user ? var_user->Variable() : nullptr;
@@ -82,8 +82,8 @@
   /// @param var the variable to check
   /// @param expected_users the expected users of the variable
   /// @return true if all users are as expected
-  bool CheckVarUsers(ast::Variable* var,
-                     std::vector<ast::Expression*>&& expected_users) {
+  bool CheckVarUsers(const ast::Variable* var,
+                     std::vector<const ast::Expression*>&& expected_users) {
     auto& var_users = Sem().Get(var)->Users();
     if (var_users.size() != expected_users.size()) {
       return false;
@@ -171,10 +171,10 @@
 template <typename TO>
 using alias3 = alias<TO, 3>;
 
-using ast_type_func_ptr = ast::Type* (*)(ProgramBuilder& b);
-using ast_expr_func_ptr = ast::Expression* (*)(ProgramBuilder& b,
-                                               int elem_value);
-using sem_type_func_ptr = sem::Type* (*)(ProgramBuilder& b);
+using ast_type_func_ptr = const ast::Type* (*)(ProgramBuilder& b);
+using ast_expr_func_ptr = const ast::Expression* (*)(ProgramBuilder& b,
+                                                     int elem_value);
+using sem_type_func_ptr = const sem::Type* (*)(ProgramBuilder& b);
 
 template <typename T>
 struct DataType {};
@@ -187,16 +187,16 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST bool type
-  static inline ast::Type* AST(ProgramBuilder& b) { return b.ty.bool_(); }
+  static inline const ast::Type* AST(ProgramBuilder& b) { return b.ty.bool_(); }
   /// @param b the ProgramBuilder
   /// @return the semantic bool type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::Bool>();
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the b
   /// @return a new AST expression of the bool type
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Expr(elem_value == 0);
   }
 };
@@ -209,16 +209,16 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST i32 type
-  static inline ast::Type* AST(ProgramBuilder& b) { return b.ty.i32(); }
+  static inline const ast::Type* AST(ProgramBuilder& b) { return b.ty.i32(); }
   /// @param b the ProgramBuilder
   /// @return the semantic i32 type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::I32>();
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the value i32 will be initialized with
   /// @return a new AST i32 literal value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Expr(static_cast<i32>(elem_value));
   }
 };
@@ -231,16 +231,16 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST u32 type
-  static inline ast::Type* AST(ProgramBuilder& b) { return b.ty.u32(); }
+  static inline const ast::Type* AST(ProgramBuilder& b) { return b.ty.u32(); }
   /// @param b the ProgramBuilder
   /// @return the semantic u32 type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::U32>();
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the value u32 will be initialized with
   /// @return a new AST u32 literal value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Expr(static_cast<u32>(elem_value));
   }
 };
@@ -253,16 +253,16 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST f32 type
-  static inline ast::Type* AST(ProgramBuilder& b) { return b.ty.f32(); }
+  static inline const ast::Type* AST(ProgramBuilder& b) { return b.ty.f32(); }
   /// @param b the ProgramBuilder
   /// @return the semantic f32 type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::F32>();
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the value f32 will be initialized with
   /// @return a new AST f32 literal value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Expr(static_cast<f32>(elem_value));
   }
 };
@@ -275,19 +275,19 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST vector type
-  static inline ast::Type* AST(ProgramBuilder& b) {
+  static inline const ast::Type* AST(ProgramBuilder& b) {
     return b.ty.vec(DataType<T>::AST(b), N);
   }
   /// @param b the ProgramBuilder
   /// @return the semantic vector type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::Vector>(DataType<T>::Sem(b), N);
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the value each element in the vector will be initialized
   /// with
   /// @return a new AST vector value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Construct(AST(b), ExprArgs(b, elem_value));
   }
 
@@ -312,12 +312,12 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST matrix type
-  static inline ast::Type* AST(ProgramBuilder& b) {
+  static inline const ast::Type* AST(ProgramBuilder& b) {
     return b.ty.mat(DataType<T>::AST(b), N, M);
   }
   /// @param b the ProgramBuilder
   /// @return the semantic matrix type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     auto* column_type = b.create<sem::Vector>(DataType<T>::Sem(b), M);
     return b.create<sem::Matrix>(column_type, N);
   }
@@ -325,7 +325,7 @@
   /// @param elem_value the value each element in the matrix will be initialized
   /// with
   /// @return a new AST matrix value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Construct(AST(b), ExprArgs(b, elem_value));
   }
 
@@ -350,7 +350,7 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST alias type
-  static inline ast::Type* AST(ProgramBuilder& b) {
+  static inline const ast::Type* AST(ProgramBuilder& b) {
     auto name = b.Symbols().Register("alias_" + std::to_string(ID));
     if (!b.AST().LookupType(name)) {
       auto* type = DataType<T>::AST(b);
@@ -360,7 +360,7 @@
   }
   /// @param b the ProgramBuilder
   /// @return the semantic aliased type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return DataType<T>::Sem(b);
   }
 
@@ -368,7 +368,7 @@
   /// @param elem_value the value nested elements will be initialized with
   /// @return a new AST expression of the alias type
   template <bool IS_COMPOSITE = is_composite>
-  static inline traits::EnableIf<!IS_COMPOSITE, ast::Expression*> Expr(
+  static inline traits::EnableIf<!IS_COMPOSITE, const ast::Expression*> Expr(
       ProgramBuilder& b,
       int elem_value) {
     // Cast
@@ -379,7 +379,7 @@
   /// @param elem_value the value nested elements will be initialized with
   /// @return a new AST expression of the alias type
   template <bool IS_COMPOSITE = is_composite>
-  static inline traits::EnableIf<IS_COMPOSITE, ast::Expression*> Expr(
+  static inline traits::EnableIf<IS_COMPOSITE, const ast::Expression*> Expr(
       ProgramBuilder& b,
       int elem_value) {
     // Construct
@@ -395,19 +395,19 @@
 
   /// @param b the ProgramBuilder
   /// @return a new AST array type
-  static inline ast::Type* AST(ProgramBuilder& b) {
+  static inline const ast::Type* AST(ProgramBuilder& b) {
     return b.ty.array(DataType<T>::AST(b), N);
   }
   /// @param b the ProgramBuilder
   /// @return the semantic array type
-  static inline sem::Type* Sem(ProgramBuilder& b) {
+  static inline const sem::Type* Sem(ProgramBuilder& b) {
     return b.create<sem::Array>(DataType<T>::Sem(b), N);
   }
   /// @param b the ProgramBuilder
   /// @param elem_value the value each element in the array will be initialized
   /// with
   /// @return a new AST array value expression
-  static inline ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
+  static inline const ast::Expression* Expr(ProgramBuilder& b, int elem_value) {
     return b.Construct(AST(b), ExprArgs(b, elem_value));
   }
 
diff --git a/src/scope_stack_test.cc b/src/scope_stack_test.cc
index 9c6c0a5..0006347 100644
--- a/src/scope_stack_test.cc
+++ b/src/scope_stack_test.cc
@@ -33,10 +33,10 @@
 
 TEST_F(ScopeStackTest, Global_SetWithPointer) {
   auto* v = Var("my_var", ty.f32(), ast::StorageClass::kNone);
-  ScopeStack<ast::Variable*> s;
+  ScopeStack<const ast::Variable*> s;
   s.set_global(v->symbol, v);
 
-  ast::Variable* v2 = nullptr;
+  const ast::Variable* v2 = nullptr;
   EXPECT_TRUE(s.get(v->symbol, &v2));
   EXPECT_EQ(v2->symbol, v->symbol);
 }
diff --git a/src/sem/array.h b/src/sem/array.h
index 8ae7a97..b1992ff 100644
--- a/src/sem/array.h
+++ b/src/sem/array.h
@@ -99,12 +99,12 @@
 
  private:
   Type const* const element_;
-  uint32_t const count_;
-  uint32_t const align_;
-  uint32_t const size_;
-  uint32_t const stride_;
-  uint32_t const implicit_stride_;
-  bool const constructible_;
+  const uint32_t count_;
+  const uint32_t align_;
+  const uint32_t size_;
+  const uint32_t stride_;
+  const uint32_t implicit_stride_;
+  const bool constructible_;
 };
 
 }  // namespace sem
diff --git a/src/sem/block_statement.cc b/src/sem/block_statement.cc
index 8022cff..c0dbd68 100644
--- a/src/sem/block_statement.cc
+++ b/src/sem/block_statement.cc
@@ -35,7 +35,7 @@
   return Base::Declaration()->As<ast::BlockStatement>();
 }
 
-void BlockStatement::AddDecl(ast::Variable* var) {
+void BlockStatement::AddDecl(const ast::Variable* var) {
   decls_.push_back(var);
 }
 
diff --git a/src/sem/block_statement.h b/src/sem/block_statement.h
index 7a99fee..6ab6403 100644
--- a/src/sem/block_statement.h
+++ b/src/sem/block_statement.h
@@ -55,7 +55,7 @@
 
   /// Associates a declaration with this block.
   /// @param var a variable declaration to be added to the block
-  void AddDecl(ast::Variable* var);
+  void AddDecl(const ast::Variable* var);
 
  private:
   std::vector<const ast::Variable*> decls_;
diff --git a/src/sem/call_target.cc b/src/sem/call_target.cc
index 9ea38bf..b2eb89e 100644
--- a/src/sem/call_target.cc
+++ b/src/sem/call_target.cc
@@ -22,7 +22,8 @@
 namespace tint {
 namespace sem {
 
-CallTarget::CallTarget(sem::Type* return_type, const ParameterList& parameters)
+CallTarget::CallTarget(const sem::Type* return_type,
+                       const ParameterList& parameters)
     : signature_{return_type, parameters} {
   TINT_ASSERT(Semantic, return_type);
 }
@@ -30,7 +31,7 @@
 CallTarget::CallTarget(const CallTarget&) = default;
 CallTarget::~CallTarget() = default;
 
-CallTargetSignature::CallTargetSignature(sem::Type* ret_ty,
+CallTargetSignature::CallTargetSignature(const sem::Type* ret_ty,
                                          const ParameterList& params)
     : return_type(ret_ty), parameters(params) {}
 CallTargetSignature::CallTargetSignature(const CallTargetSignature&) = default;
diff --git a/src/sem/call_target.h b/src/sem/call_target.h
index c0e7b8e..3ed05ab 100644
--- a/src/sem/call_target.h
+++ b/src/sem/call_target.h
@@ -32,7 +32,7 @@
   /// Constructor
   /// @param ret_ty the call target return type
   /// @param params the call target parameters
-  CallTargetSignature(sem::Type* ret_ty, const ParameterList& params);
+  CallTargetSignature(const sem::Type* ret_ty, const ParameterList& params);
 
   /// Copy constructor
   CallTargetSignature(const CallTargetSignature&);
@@ -41,9 +41,9 @@
   ~CallTargetSignature();
 
   /// The type of the call target return value
-  sem::Type* const return_type = nullptr;
+  const sem::Type* const return_type = nullptr;
   /// The parameters of the call target
-  ParameterList const parameters;
+  const ParameterList parameters;
 
   /// Equality operator
   /// @param other the signature to compare this to
@@ -62,7 +62,7 @@
   /// Constructor
   /// @param return_type the return type of the call target
   /// @param parameters the parameters for the call target
-  CallTarget(sem::Type* return_type, const ParameterList& parameters);
+  CallTarget(const sem::Type* return_type, const ParameterList& parameters);
 
   /// Copy constructor
   CallTarget(const CallTarget&);
@@ -71,7 +71,7 @@
   ~CallTarget() override;
 
   /// @return the return type of the call target
-  sem::Type* ReturnType() const { return signature_.return_type; }
+  const sem::Type* ReturnType() const { return signature_.return_type; }
 
   /// @return the parameters of the call target
   const ParameterList& Parameters() const { return signature_.parameters; }
diff --git a/src/sem/expression.cc b/src/sem/expression.cc
index f6ce44b..6cb318a 100644
--- a/src/sem/expression.cc
+++ b/src/sem/expression.cc
@@ -23,7 +23,7 @@
 
 Expression::Expression(const ast::Expression* declaration,
                        const sem::Type* type,
-                       Statement* statement,
+                       const Statement* statement,
                        Constant constant)
     : declaration_(declaration),
       type_(type),
diff --git a/src/sem/expression.h b/src/sem/expression.h
index ccb7dd8..ba001cb 100644
--- a/src/sem/expression.h
+++ b/src/sem/expression.h
@@ -35,31 +35,29 @@
   /// @param constant the constant value of the expression. May be invalid
   Expression(const ast::Expression* declaration,
              const sem::Type* type,
-             Statement* statement,
+             const Statement* statement,
              Constant constant);
 
   /// Destructor
   ~Expression() override;
 
   /// @return the resolved type of the expression
-  sem::Type* Type() const { return const_cast<sem::Type*>(type_); }
+  const sem::Type* Type() const { return type_; }
 
   /// @return the statement that owns this expression
-  Statement* Stmt() const { return statement_; }
+  const Statement* Stmt() const { return statement_; }
 
   /// @return the constant value of this expression
   const Constant& ConstantValue() const { return constant_; }
 
   /// @returns the AST node
-  ast::Expression* Declaration() const {
-    return const_cast<ast::Expression*>(declaration_);
-  }
+  const ast::Expression* Declaration() const { return declaration_; }
 
  private:
-  const ast::Expression* declaration_;
+  const ast::Expression* const declaration_;
   const sem::Type* const type_;
-  Statement* const statement_;
-  Constant const constant_;
+  const Statement* const statement_;
+  const Constant constant_;
 };
 
 }  // namespace sem
diff --git a/src/sem/function.cc b/src/sem/function.cc
index 0743cb4..ac81698 100644
--- a/src/sem/function.cc
+++ b/src/sem/function.cc
@@ -28,7 +28,7 @@
 namespace tint {
 namespace sem {
 
-Function::Function(ast::Function* declaration,
+Function::Function(const ast::Function* declaration,
                    Type* return_type,
                    std::vector<Parameter*> parameters,
                    std::vector<const Variable*> referenced_module_vars,
@@ -52,9 +52,9 @@
 
 Function::~Function() = default;
 
-std::vector<std::pair<const Variable*, ast::LocationDecoration*>>
+std::vector<std::pair<const Variable*, const ast::LocationDecoration*>>
 Function::ReferencedLocationVariables() const {
-  std::vector<std::pair<const Variable*, ast::LocationDecoration*>> ret;
+  std::vector<std::pair<const Variable*, const ast::LocationDecoration*>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
     for (auto* deco : var->Declaration()->decorations) {
@@ -97,9 +97,9 @@
   return ret;
 }
 
-std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
+std::vector<std::pair<const Variable*, const ast::BuiltinDecoration*>>
 Function::ReferencedBuiltinVariables() const {
-  std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>> ret;
+  std::vector<std::pair<const Variable*, const ast::BuiltinDecoration*>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
     for (auto* deco : var->Declaration()->decorations) {
diff --git a/src/sem/function.h b/src/sem/function.h
index cd0c014..18e4617 100644
--- a/src/sem/function.h
+++ b/src/sem/function.h
@@ -66,7 +66,7 @@
   /// @param callsites the callsites of the function
   /// @param ancestor_entry_points the ancestor entry points
   /// @param workgroup_size the workgroup size
-  Function(ast::Function* declaration,
+  Function(const ast::Function* declaration,
            Type* return_type,
            std::vector<Parameter*> parameters,
            std::vector<const Variable*> referenced_module_vars,
@@ -80,7 +80,7 @@
   ~Function() override;
 
   /// @returns the ast::Function declaration
-  ast::Function* Declaration() const { return declaration_; }
+  const ast::Function* Declaration() const { return declaration_; }
 
   /// Note: If this function calls other functions, the return will also include
   /// all of the referenced variables from the callees.
@@ -106,12 +106,12 @@
   }
   /// Retrieves any referenced location variables
   /// @returns the <variable, decoration> pair.
-  std::vector<std::pair<const Variable*, ast::LocationDecoration*>>
+  std::vector<std::pair<const Variable*, const ast::LocationDecoration*>>
   ReferencedLocationVariables() const;
 
   /// Retrieves any referenced builtin variables
   /// @returns the <variable, decoration> pair.
-  std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
+  std::vector<std::pair<const Variable*, const ast::BuiltinDecoration*>>
   ReferencedBuiltinVariables() const;
 
   /// Retrieves any referenced uniform variables. Note, the variables must be
@@ -174,7 +174,7 @@
   VariableBindings ReferencedSampledTextureVariablesImpl(
       bool multisampled) const;
 
-  ast::Function* const declaration_;
+  const ast::Function* const declaration_;
   std::vector<const Variable*> const referenced_module_vars_;
   std::vector<const Variable*> const local_referenced_module_vars_;
   std::vector<const ast::ReturnStatement*> const return_statements_;
diff --git a/src/sem/intrinsic.cc b/src/sem/intrinsic.cc
index 0b1988e..13ccd21 100644
--- a/src/sem/intrinsic.cc
+++ b/src/sem/intrinsic.cc
@@ -102,7 +102,7 @@
 }
 
 Intrinsic::Intrinsic(IntrinsicType type,
-                     sem::Type* return_type,
+                     const sem::Type* return_type,
                      std::vector<Parameter*> parameters,
                      PipelineStageSet supported_stages,
                      bool is_deprecated)
diff --git a/src/sem/intrinsic.h b/src/sem/intrinsic.h
index 27f3136..fb55b48 100644
--- a/src/sem/intrinsic.h
+++ b/src/sem/intrinsic.h
@@ -88,7 +88,7 @@
   /// @param is_deprecated true if the particular overload is considered
   /// deprecated
   Intrinsic(IntrinsicType type,
-            sem::Type* return_type,
+            const sem::Type* return_type,
             std::vector<Parameter*> parameters,
             PipelineStageSet supported_stages,
             bool is_deprecated);
@@ -140,9 +140,9 @@
   bool IsAtomic() const;
 
  private:
-  IntrinsicType const type_;
-  PipelineStageSet const supported_stages_;
-  bool const is_deprecated_;
+  const IntrinsicType type_;
+  const PipelineStageSet supported_stages_;
+  const bool is_deprecated_;
 };
 
 }  // namespace sem
diff --git a/src/sem/matrix_type.cc b/src/sem/matrix_type.cc
index 34de689..14bd0ba 100644
--- a/src/sem/matrix_type.cc
+++ b/src/sem/matrix_type.cc
@@ -22,7 +22,7 @@
 namespace tint {
 namespace sem {
 
-Matrix::Matrix(Vector* column_type, uint32_t columns)
+Matrix::Matrix(const Vector* column_type, uint32_t columns)
     : subtype_(column_type->type()),
       column_type_(column_type),
       rows_(column_type->Width()),
diff --git a/src/sem/matrix_type.h b/src/sem/matrix_type.h
index 9c69b79..896eb3b 100644
--- a/src/sem/matrix_type.h
+++ b/src/sem/matrix_type.h
@@ -31,20 +31,20 @@
   /// Constructor
   /// @param column_type the type of a column of the matrix
   /// @param columns the number of columns in the matrix
-  Matrix(Vector* column_type, uint32_t columns);
+  Matrix(const Vector* column_type, uint32_t columns);
   /// Move constructor
   Matrix(Matrix&&);
   ~Matrix() override;
 
   /// @returns the type of the matrix
-  Type* type() const { return subtype_; }
+  const Type* type() const { return subtype_; }
   /// @returns the number of rows in the matrix
   uint32_t rows() const { return rows_; }
   /// @returns the number of columns in the matrix
   uint32_t columns() const { return columns_; }
 
   /// @returns the column-vector type of the matrix
-  Vector* ColumnType() const { return column_type_; }
+  const Vector* ColumnType() const { return column_type_; }
 
   /// @returns the name for this type
   std::string type_name() const override;
@@ -69,10 +69,10 @@
   uint32_t ColumnStride() const;
 
  private:
-  Type* const subtype_;
-  Vector* const column_type_;
-  uint32_t const rows_;
-  uint32_t const columns_;
+  const Type* const subtype_;
+  const Vector* const column_type_;
+  const uint32_t rows_;
+  const uint32_t columns_;
 };
 
 }  // namespace sem
diff --git a/src/sem/member_accessor_expression.cc b/src/sem/member_accessor_expression.cc
index 309785b..d8dcd69 100644
--- a/src/sem/member_accessor_expression.cc
+++ b/src/sem/member_accessor_expression.cc
@@ -25,25 +25,25 @@
 namespace sem {
 
 MemberAccessorExpression::MemberAccessorExpression(
-    ast::MemberAccessorExpression* declaration,
+    const ast::MemberAccessorExpression* declaration,
     const sem::Type* type,
-    Statement* statement)
+    const Statement* statement)
     : Base(declaration, type, statement, Constant{}) {}
 
 MemberAccessorExpression::~MemberAccessorExpression() = default;
 
 StructMemberAccess::StructMemberAccess(
-    ast::MemberAccessorExpression* declaration,
+    const ast::MemberAccessorExpression* declaration,
     const sem::Type* type,
-    Statement* statement,
+    const Statement* statement,
     const StructMember* member)
     : Base(declaration, type, statement), member_(member) {}
 
 StructMemberAccess::~StructMemberAccess() = default;
 
-Swizzle::Swizzle(ast::MemberAccessorExpression* declaration,
+Swizzle::Swizzle(const ast::MemberAccessorExpression* declaration,
                  const sem::Type* type,
-                 Statement* statement,
+                 const Statement* statement,
                  std::vector<uint32_t> indices)
     : Base(declaration, type, statement), indices_(std::move(indices)) {}
 
diff --git a/src/sem/member_accessor_expression.h b/src/sem/member_accessor_expression.h
index e298024..fcc6c6f 100644
--- a/src/sem/member_accessor_expression.h
+++ b/src/sem/member_accessor_expression.h
@@ -41,9 +41,9 @@
   /// @param declaration the AST node
   /// @param type the resolved type of the expression
   /// @param statement the statement that owns this expression
-  MemberAccessorExpression(ast::MemberAccessorExpression* declaration,
+  MemberAccessorExpression(const ast::MemberAccessorExpression* declaration,
                            const sem::Type* type,
-                           Statement* statement);
+                           const Statement* statement);
 
   /// Destructor
   ~MemberAccessorExpression() override;
@@ -60,9 +60,9 @@
   /// @param type the resolved type of the expression
   /// @param statement the statement that owns this expression
   /// @param member the structure member
-  StructMemberAccess(ast::MemberAccessorExpression* declaration,
+  StructMemberAccess(const ast::MemberAccessorExpression* declaration,
                      const sem::Type* type,
-                     Statement* statement,
+                     const Statement* statement,
                      const StructMember* member);
 
   /// Destructor
@@ -84,9 +84,9 @@
   /// @param type the resolved type of the expression
   /// @param statement the statement that
   /// @param indices the swizzle indices
-  Swizzle(ast::MemberAccessorExpression* declaration,
+  Swizzle(const ast::MemberAccessorExpression* declaration,
           const sem::Type* type,
-          Statement* statement,
+          const Statement* statement,
           std::vector<uint32_t> indices);
 
   /// Destructor
diff --git a/src/sem/multisampled_texture_type.h b/src/sem/multisampled_texture_type.h
index 715d6aa..f67ee1a 100644
--- a/src/sem/multisampled_texture_type.h
+++ b/src/sem/multisampled_texture_type.h
@@ -34,7 +34,7 @@
   ~MultisampledTexture() override;
 
   /// @returns the subtype of the sampled texture
-  Type* type() const { return const_cast<Type*>(type_); }
+  const Type* type() const { return type_; }
 
   /// @returns the name for this type
   std::string type_name() const override;
diff --git a/src/sem/struct.cc b/src/sem/struct.cc
index 9238ec9..d764115 100644
--- a/src/sem/struct.cc
+++ b/src/sem/struct.cc
@@ -78,7 +78,7 @@
   return constructible_;
 }
 
-StructMember::StructMember(ast::StructMember* declaration,
+StructMember::StructMember(const ast::StructMember* declaration,
                            Symbol name,
                            sem::Type* type,
                            uint32_t index,
diff --git a/src/sem/struct.h b/src/sem/struct.h
index f6014aa..3e3b62d 100644
--- a/src/sem/struct.h
+++ b/src/sem/struct.h
@@ -161,11 +161,11 @@
   uint64_t LargestMemberBaseAlignment(MemoryLayout mem_layout) const;
 
   ast::Struct const* const declaration_;
-  Symbol const name_;
-  StructMemberList const members_;
-  uint32_t const align_;
-  uint32_t const size_;
-  uint32_t const size_no_padding_;
+  const Symbol name_;
+  const StructMemberList members_;
+  const uint32_t align_;
+  const uint32_t size_;
+  const uint32_t size_no_padding_;
   std::unordered_set<ast::StorageClass> storage_class_usage_;
   std::unordered_set<PipelineStageUsage> pipeline_stage_uses_;
   bool constructible_;
@@ -182,7 +182,7 @@
   /// @param offset the byte offset from the base of the structure
   /// @param align the byte alignment of the member
   /// @param size the byte size of the member
-  StructMember(ast::StructMember* declaration,
+  StructMember(const ast::StructMember* declaration,
                Symbol name,
                sem::Type* type,
                uint32_t index,
@@ -194,7 +194,7 @@
   ~StructMember() override;
 
   /// @returns the AST declaration node
-  ast::StructMember* Declaration() const { return declaration_; }
+  const ast::StructMember* Declaration() const { return declaration_; }
 
   /// @returns the name of the structure
   Symbol Name() const { return name_; }
@@ -215,13 +215,13 @@
   uint32_t Size() const { return size_; }
 
  private:
-  ast::StructMember* const declaration_;
-  Symbol const name_;
+  const ast::StructMember* const declaration_;
+  const Symbol name_;
   sem::Type* const type_;
-  uint32_t const index_;
-  uint32_t const offset_;
-  uint32_t const align_;
-  uint32_t const size_;
+  const uint32_t index_;
+  const uint32_t offset_;
+  const uint32_t align_;
+  const uint32_t size_;
 };
 
 }  // namespace sem
diff --git a/src/sem/variable.cc b/src/sem/variable.cc
index bc783a1..a1e0eb8 100644
--- a/src/sem/variable.cc
+++ b/src/sem/variable.cc
@@ -80,7 +80,7 @@
 
 Parameter::~Parameter() = default;
 
-VariableUser::VariableUser(ast::IdentifierExpression* declaration,
+VariableUser::VariableUser(const ast::IdentifierExpression* declaration,
                            const sem::Type* type,
                            Statement* statement,
                            sem::Variable* variable,
diff --git a/src/sem/variable.h b/src/sem/variable.h
index 5bbe3e1..782fda9 100644
--- a/src/sem/variable.h
+++ b/src/sem/variable.h
@@ -59,7 +59,7 @@
   const ast::Variable* Declaration() const { return declaration_; }
 
   /// @returns the canonical type for the variable
-  sem::Type* Type() const { return const_cast<sem::Type*>(type_); }
+  const sem::Type* Type() const { return type_; }
 
   /// @returns the storage class for the variable
   ast::StorageClass StorageClass() const { return storage_class_; }
@@ -135,7 +135,7 @@
 
  private:
   sem::BindingPoint binding_point_;
-  bool const is_pipeline_constant_;
+  const bool is_pipeline_constant_;
   uint16_t const constant_id_ = 0;
 };
 
@@ -172,8 +172,8 @@
   void SetOwner(CallTarget const* owner) { owner_ = owner; }
 
  private:
-  uint32_t const index_;
-  ParameterUsage const usage_;
+  const uint32_t index_;
+  const ParameterUsage usage_;
   CallTarget const* owner_;
 };
 
@@ -190,7 +190,7 @@
   /// @param statement the statement that owns this expression
   /// @param variable the semantic variable
   /// @param constant_value the constant value for the variable. May be invalid
-  VariableUser(ast::IdentifierExpression* declaration,
+  VariableUser(const ast::IdentifierExpression* declaration,
                const sem::Type* type,
                Statement* statement,
                sem::Variable* variable,
diff --git a/src/sem/vector_type.h b/src/sem/vector_type.h
index 91633a7..3f3b714 100644
--- a/src/sem/vector_type.h
+++ b/src/sem/vector_type.h
@@ -34,7 +34,7 @@
   ~Vector() override;
 
   /// @returns the type of the vector elements
-  Type* type() const { return const_cast<Type*>(subtype_); }
+  const Type* type() const { return subtype_; }
 
   /// @returns the name for th type
   std::string type_name() const override;
@@ -68,7 +68,7 @@
 
  private:
   Type const* const subtype_;
-  uint32_t const width_;
+  const uint32_t width_;
 };
 
 }  // namespace sem
diff --git a/src/transform/array_length_from_uniform.cc b/src/transform/array_length_from_uniform.cc
index 70ba5fc..65f8f0a 100644
--- a/src/transform/array_length_from_uniform.cc
+++ b/src/transform/array_length_from_uniform.cc
@@ -64,7 +64,7 @@
 
   // Get (or create, on first call) the uniform buffer that will receive the
   // size of each storage buffer in the module.
-  ast::Variable* buffer_size_ubo = nullptr;
+  const ast::Variable* buffer_size_ubo = nullptr;
   auto get_ubo = [&]() {
     if (!buffer_size_ubo) {
       // Emit an array<vec4<u32>, N>, where N is 1/4 number of elements.
diff --git a/src/transform/array_length_from_uniform.h b/src/transform/array_length_from_uniform.h
index a310616..306ed8a 100644
--- a/src/transform/array_length_from_uniform.h
+++ b/src/transform/array_length_from_uniform.h
@@ -89,7 +89,7 @@
     ~Result() override;
 
     /// True if the transform generated the buffer sizes UBO.
-    bool const needs_buffer_sizes;
+    const bool needs_buffer_sizes;
   };
 
  protected:
diff --git a/src/transform/binding_remapper.cc b/src/transform/binding_remapper.cc
index 3fe4e6e..ef77fed 100644
--- a/src/transform/binding_remapper.cc
+++ b/src/transform/binding_remapper.cc
@@ -129,7 +129,7 @@
           return;
         }
         auto* ty = sem->Type()->UnwrapRef();
-        ast::Type* inner_ty = CreateASTTypeFor(ctx, ty);
+        const ast::Type* inner_ty = CreateASTTypeFor(ctx, ty);
         auto* new_var = ctx.dst->create<ast::Variable>(
             ctx.Clone(var->source), ctx.Clone(var->symbol),
             var->declared_storage_class, ac, inner_ty, var->is_const,
diff --git a/src/transform/binding_remapper.h b/src/transform/binding_remapper.h
index 5fd2fc8..8e0d8fd 100644
--- a/src/transform/binding_remapper.h
+++ b/src/transform/binding_remapper.h
@@ -54,14 +54,14 @@
     ~Remappings() override;
 
     /// A map of old binding point to new binding point
-    BindingPoints const binding_points;
+    const BindingPoints binding_points;
 
     /// A map of old binding point to new access controls
-    AccessControls const access_controls;
+    const AccessControls access_controls;
 
     /// If true, then validation will be disabled for binding point collisions
     /// generated by this transform
-    bool const allow_collisions;
+    const bool allow_collisions;
   };
 
   /// Constructor
diff --git a/src/transform/calculate_array_length.cc b/src/transform/calculate_array_length.cc
index 37394a0..849d062 100644
--- a/src/transform/calculate_array_length.cc
+++ b/src/transform/calculate_array_length.cc
@@ -61,7 +61,7 @@
   return "intrinsic_buffer_size";
 }
 
-CalculateArrayLength::BufferSizeIntrinsic*
+const CalculateArrayLength::BufferSizeIntrinsic*
 CalculateArrayLength::BufferSizeIntrinsic::Clone(CloneContext* ctx) const {
   return ctx->dst->ASTNodes().Create<CalculateArrayLength::BufferSizeIntrinsic>(
       ctx->dst->ID());
diff --git a/src/transform/calculate_array_length.h b/src/transform/calculate_array_length.h
index 2003058..55aabe4 100644
--- a/src/transform/calculate_array_length.h
+++ b/src/transform/calculate_array_length.h
@@ -48,7 +48,7 @@
     /// Performs a deep clone of this object using the CloneContext `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned object
-    BufferSizeIntrinsic* Clone(CloneContext* ctx) const override;
+    const BufferSizeIntrinsic* Clone(CloneContext* ctx) const override;
   };
 
   /// Constructor
diff --git a/src/transform/canonicalize_entry_point_io.cc b/src/transform/canonicalize_entry_point_io.cc
index 28378b1..51b5e46 100644
--- a/src/transform/canonicalize_entry_point_io.cc
+++ b/src/transform/canonicalize_entry_point_io.cc
@@ -82,11 +82,11 @@
     /// The name of the output value.
     std::string name;
     /// The type of the output value.
-    ast::Type* type;
+    const ast::Type* type;
     /// The shader IO attributes.
     ast::DecorationList attributes;
     /// The value itself.
-    ast::Expression* value;
+    const ast::Expression* value;
   };
 
   /// The clone context.
@@ -94,9 +94,9 @@
   /// The transform config.
   CanonicalizeEntryPointIO::Config const cfg;
   /// The entry point function (AST).
-  ast::Function* func_ast;
+  const ast::Function* func_ast;
   /// The entry point function (SEM).
-  sem::Function const* func_sem;
+  const sem::Function* func_sem;
 
   /// The new entry point wrapper function's parameters.
   ast::VariableList wrapper_ep_parameters;
@@ -121,7 +121,7 @@
   /// @param function the entry point function
   State(CloneContext& context,
         const CanonicalizeEntryPointIO::Config& config,
-        ast::Function* function)
+        const ast::Function* function)
       : ctx(context),
         cfg(config),
         func_ast(function),
@@ -154,9 +154,9 @@
   /// @param type the type of the shader input
   /// @param attributes the attributes to apply to the shader input
   /// @returns an expression which evaluates to the value of the shader input
-  ast::Expression* AddInput(std::string name,
-                            sem::Type* type,
-                            ast::DecorationList attributes) {
+  const ast::Expression* AddInput(std::string name,
+                                  const sem::Type* type,
+                                  ast::DecorationList attributes) {
     auto* ast_type = CreateASTTypeFor(ctx, type);
     if (cfg.shader_style == ShaderStyle::kSpirv) {
       // Vulkan requires that integer user-defined fragment inputs are
@@ -175,7 +175,7 @@
 
       // Create the global variable and use its value for the shader input.
       auto symbol = ctx.dst->Symbols().New(name);
-      ast::Expression* value = ctx.dst->Expr(symbol);
+      const ast::Expression* value = ctx.dst->Expr(symbol);
       if (HasSampleMask(attributes)) {
         // Vulkan requires the type of a SampleMask builtin to be an array.
         // Declare it as array<u32, 1> and then load the first element.
@@ -212,9 +212,9 @@
   /// @param attributes the attributes to apply to the shader output
   /// @param value the value of the shader output
   void AddOutput(std::string name,
-                 sem::Type* type,
+                 const sem::Type* type,
                  ast::DecorationList attributes,
-                 ast::Expression* value) {
+                 const ast::Expression* value) {
     // Vulkan requires that integer user-defined vertex outputs are
     // always decorated with `Flat`.
     if (cfg.shader_style == ShaderStyle::kSpirv &&
@@ -417,7 +417,7 @@
       // Create the global variable and assign it the output value.
       auto name = ctx.dst->Symbols().New(outval.name);
       auto* type = outval.type;
-      ast::Expression* lhs = ctx.dst->Expr(name);
+      const ast::Expression* lhs = ctx.dst->Expr(name);
       if (HasSampleMask(attributes)) {
         // Vulkan requires the type of a SampleMask builtin to be an array.
         // Declare it as array<u32, 1> and then store to the first element.
@@ -432,7 +432,7 @@
 
   // Recreate the original function without entry point attributes and call it.
   /// @returns the inner function call expression
-  ast::CallExpression* CallInnerFunction() {
+  const ast::CallExpression* CallInnerFunction() {
     // Add a suffix to the function name, as the wrapper function will take the
     // original entry point name.
     auto ep_name = ctx.src->Symbols().NameFor(func_ast->symbol);
@@ -492,7 +492,7 @@
     auto* call_inner = CallInnerFunction();
 
     // Process the return type, and start building the wrapper function body.
-    std::function<ast::Type*()> wrapper_ret_type = [&] {
+    std::function<const ast::Type*()> wrapper_ret_type = [&] {
       return ctx.dst->ty.void_();
     };
     if (func_sem->ReturnType()->Is<sem::Void>()) {
diff --git a/src/transform/canonicalize_entry_point_io.h b/src/transform/canonicalize_entry_point_io.h
index 87c3e10..baaa65a 100644
--- a/src/transform/canonicalize_entry_point_io.h
+++ b/src/transform/canonicalize_entry_point_io.h
@@ -110,14 +110,14 @@
     ~Config() override;
 
     /// The approach to use for emitting shader IO.
-    ShaderStyle const shader_style;
+    const ShaderStyle shader_style;
 
     /// A fixed sample mask to combine into masks produced by fragment shaders.
-    uint32_t const fixed_sample_mask;
+    const uint32_t fixed_sample_mask;
 
     /// Set to `true` to generate a pointsize builtin and have it set to 1.0
     /// from all vertex shaders in the module.
-    bool const emit_vertex_point_size;
+    const bool emit_vertex_point_size;
   };
 
   /// Constructor
diff --git a/src/transform/decompose_memory_access.cc b/src/transform/decompose_memory_access.cc
index d6b5a40..5d32b02 100644
--- a/src/transform/decompose_memory_access.cc
+++ b/src/transform/decompose_memory_access.cc
@@ -51,17 +51,17 @@
 /// offsets for storage and uniform buffer accesses.
 struct Offset : Castable<Offset> {
   /// @returns builds and returns the ast::Expression in `ctx.dst`
-  virtual ast::Expression* Build(CloneContext& ctx) const = 0;
+  virtual const ast::Expression* Build(CloneContext& ctx) const = 0;
 };
 
 /// OffsetExpr is an implementation of Offset that clones and casts the given
 /// expression to `u32`.
 struct OffsetExpr : Offset {
-  ast::Expression* const expr = nullptr;
+  const ast::Expression* const expr = nullptr;
 
-  explicit OffsetExpr(ast::Expression* e) : expr(e) {}
+  explicit OffsetExpr(const ast::Expression* e) : expr(e) {}
 
-  ast::Expression* Build(CloneContext& ctx) const override {
+  const ast::Expression* Build(CloneContext& ctx) const override {
     auto* type = ctx.src->Sem().Get(expr)->Type()->UnwrapRef();
     auto* res = ctx.Clone(expr);
     if (!type->Is<sem::U32>()) {
@@ -78,7 +78,7 @@
 
   explicit OffsetLiteral(uint32_t lit) : literal(lit) {}
 
-  ast::Expression* Build(CloneContext& ctx) const override {
+  const ast::Expression* Build(CloneContext& ctx) const override {
     return ctx.dst->Expr(literal);
   }
 };
@@ -90,7 +90,7 @@
   Offset const* lhs = nullptr;
   Offset const* rhs = nullptr;
 
-  ast::Expression* Build(CloneContext& ctx) const override {
+  const ast::Expression* Build(CloneContext& ctx) const override {
     return ctx.dst->create<ast::BinaryExpression>(op, lhs->Build(ctx),
                                                   rhs->Build(ctx));
   }
@@ -304,9 +304,9 @@
   /// expressions chain the access.
   /// Subset of #expression_order, as expressions are not removed from
   /// #expression_order.
-  std::unordered_map<ast::Expression*, BufferAccess> accesses;
+  std::unordered_map<const ast::Expression*, BufferAccess> accesses;
   /// The visited order of AST expressions (superset of #accesses)
-  std::vector<ast::Expression*> expression_order;
+  std::vector<const ast::Expression*> expression_order;
   /// [buffer-type, element-type] -> load function name
   std::unordered_map<LoadStoreKey, Symbol, LoadStoreKey::Hasher> load_funcs;
   /// [buffer-type, element-type] -> store function name
@@ -330,7 +330,7 @@
 
   /// @param expr the expression to convert to an Offset
   /// @returns an Offset for the given ast::Expression
-  const Offset* ToOffset(ast::Expression* expr) {
+  const Offset* ToOffset(const ast::Expression* expr) {
     if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
       if (auto* u32 = scalar->literal->As<ast::UintLiteral>()) {
         return offsets_.Create<OffsetLiteral>(u32->value);
@@ -415,7 +415,7 @@
   /// to #expression_order.
   /// @param expr the expression that performs the access
   /// @param access the access
-  void AddAccess(ast::Expression* expr, const BufferAccess& access) {
+  void AddAccess(const ast::Expression* expr, const BufferAccess& access) {
     TINT_ASSERT(Transform, access.type);
     accesses.emplace(expr, access);
     expression_order.emplace_back(expr);
@@ -426,7 +426,7 @@
   /// `node`, an invalid BufferAccess is returned.
   /// @param node the expression that performed an access
   /// @return the BufferAccess for the given expression
-  BufferAccess TakeAccess(ast::Expression* node) {
+  BufferAccess TakeAccess(const ast::Expression* node) {
     auto lhs_it = accesses.find(node);
     if (lhs_it == accesses.end()) {
       return {};
@@ -793,7 +793,7 @@
   return ss.str();
 }
 
-DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone(
+const DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone(
     CloneContext* ctx) const {
   return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
       ctx->dst->ID(), op, storage_class, type);
diff --git a/src/transform/decompose_memory_access.h b/src/transform/decompose_memory_access.h
index 66cdf37..67f3eb9 100644
--- a/src/transform/decompose_memory_access.h
+++ b/src/transform/decompose_memory_access.h
@@ -88,16 +88,16 @@
     /// Performs a deep clone of this object using the CloneContext `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned object
-    Intrinsic* Clone(CloneContext* ctx) const override;
+    const Intrinsic* Clone(CloneContext* ctx) const override;
 
     /// The op of the intrinsic
-    Op const op;
+    const Op op;
 
     /// The storage class of the buffer this intrinsic operates on
     ast::StorageClass const storage_class;
 
     /// The type of the intrinsic
-    DataType const type;
+    const DataType type;
   };
 
   /// Constructor
diff --git a/src/transform/decompose_strided_matrix.cc b/src/transform/decompose_strided_matrix.cc
index 497a891..2d21a5b 100644
--- a/src/transform/decompose_strided_matrix.cc
+++ b/src/transform/decompose_strided_matrix.cc
@@ -37,11 +37,11 @@
   /// The stride in bytes between columns of the matrix
   uint32_t stride = 0;
   /// The type of the matrix
-  sem::Matrix const* matrix = nullptr;
+  const sem::Matrix* matrix = nullptr;
 
   /// @returns a new ast::Array that holds an vector column for each row of the
   /// matrix.
-  ast::Array* array(ProgramBuilder* b) const {
+  const ast::Array* array(ProgramBuilder* b) const {
     return b->ty.array(b->ty.vec<ProgramBuilder::f32>(matrix->rows()),
                        matrix->columns(), stride);
   }
@@ -126,7 +126,7 @@
   // Scan the program for all storage and uniform structure matrix members with
   // a custom stride attribute. Replace these matrices with an equivalent array,
   // and populate the `decomposed` map with the members that have been replaced.
-  std::unordered_map<ast::StructMember*, MatrixInfo> decomposed;
+  std::unordered_map<const ast::StructMember*, MatrixInfo> decomposed;
   GatherCustomStrideMatrixMembers(
       ctx.src, [&](const sem::StructMember* member, sem::Matrix* matrix,
                    uint32_t stride) {
@@ -144,19 +144,19 @@
   // preserve these without calling conversion functions.
   // Example:
   //   ssbo.mat[2] -> ssbo.mat[2]
-  ctx.ReplaceAll(
-      [&](ast::ArrayAccessorExpression* expr) -> ast::ArrayAccessorExpression* {
-        if (auto* access =
-                ctx.src->Sem().Get<sem::StructMemberAccess>(expr->array)) {
-          auto it = decomposed.find(access->Member()->Declaration());
-          if (it != decomposed.end()) {
-            auto* obj = ctx.CloneWithoutTransform(expr->array);
-            auto* idx = ctx.Clone(expr->index);
-            return ctx.dst->IndexAccessor(obj, idx);
-          }
-        }
-        return nullptr;
-      });
+  ctx.ReplaceAll([&](const ast::ArrayAccessorExpression* expr)
+                     -> const ast::ArrayAccessorExpression* {
+    if (auto* access =
+            ctx.src->Sem().Get<sem::StructMemberAccess>(expr->array)) {
+      auto it = decomposed.find(access->Member()->Declaration());
+      if (it != decomposed.end()) {
+        auto* obj = ctx.CloneWithoutTransform(expr->array);
+        auto* idx = ctx.Clone(expr->index);
+        return ctx.dst->IndexAccessor(obj, idx);
+      }
+    }
+    return nullptr;
+  });
 
   // For all struct member accesses to the matrix on the LHS of an assignment,
   // we need to convert the matrix to the array before assigning to the
@@ -164,7 +164,8 @@
   // Example:
   //   ssbo.mat = mat_to_arr(m)
   std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> mat_to_arr;
-  ctx.ReplaceAll([&](ast::AssignmentStatement* stmt) -> ast::Statement* {
+  ctx.ReplaceAll([&](const ast::AssignmentStatement* stmt)
+                     -> const ast::Statement* {
     if (auto* access = ctx.src->Sem().Get<sem::StructMemberAccess>(stmt->lhs)) {
       auto it = decomposed.find(access->Member()->Declaration());
       if (it == decomposed.end()) {
@@ -206,42 +207,44 @@
   // matrix type. Example:
   //   m = arr_to_mat(ssbo.mat)
   std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> arr_to_mat;
-  ctx.ReplaceAll([&](ast::MemberAccessorExpression* expr) -> ast::Expression* {
-    if (auto* access = ctx.src->Sem().Get<sem::StructMemberAccess>(expr)) {
-      auto it = decomposed.find(access->Member()->Declaration());
-      if (it == decomposed.end()) {
-        return nullptr;
-      }
-      MatrixInfo info = it->second;
-      auto fn = utils::GetOrCreate(arr_to_mat, info, [&] {
-        auto name = ctx.dst->Symbols().New(
-            "arr_to_mat" + std::to_string(info.matrix->columns()) + "x" +
-            std::to_string(info.matrix->rows()) + "_stride_" +
-            std::to_string(info.stride));
+  ctx.ReplaceAll(
+      [&](const ast::MemberAccessorExpression* expr) -> const ast::Expression* {
+        if (auto* access = ctx.src->Sem().Get<sem::StructMemberAccess>(expr)) {
+          auto it = decomposed.find(access->Member()->Declaration());
+          if (it == decomposed.end()) {
+            return nullptr;
+          }
+          MatrixInfo info = it->second;
+          auto fn = utils::GetOrCreate(arr_to_mat, info, [&] {
+            auto name = ctx.dst->Symbols().New(
+                "arr_to_mat" + std::to_string(info.matrix->columns()) + "x" +
+                std::to_string(info.matrix->rows()) + "_stride_" +
+                std::to_string(info.stride));
 
-        auto matrix = [&] { return CreateASTTypeFor(ctx, info.matrix); };
-        auto array = [&] { return info.array(ctx.dst); };
+            auto matrix = [&] { return CreateASTTypeFor(ctx, info.matrix); };
+            auto array = [&] { return info.array(ctx.dst); };
 
-        auto arr = ctx.dst->Sym("arr");
-        ast::ExpressionList columns(info.matrix->columns());
-        for (uint32_t i = 0; i < static_cast<uint32_t>(columns.size()); i++) {
-          columns[i] = ctx.dst->IndexAccessor(arr, i);
+            auto arr = ctx.dst->Sym("arr");
+            ast::ExpressionList columns(info.matrix->columns());
+            for (uint32_t i = 0; i < static_cast<uint32_t>(columns.size());
+                 i++) {
+              columns[i] = ctx.dst->IndexAccessor(arr, i);
+            }
+            ctx.dst->Func(
+                name,
+                {
+                    ctx.dst->Param(arr, array()),
+                },
+                matrix(),
+                {
+                    ctx.dst->Return(ctx.dst->Construct(matrix(), columns)),
+                });
+            return name;
+          });
+          return ctx.dst->Call(fn, ctx.CloneWithoutTransform(expr));
         }
-        ctx.dst->Func(
-            name,
-            {
-                ctx.dst->Param(arr, array()),
-            },
-            matrix(),
-            {
-                ctx.dst->Return(ctx.dst->Construct(matrix(), columns)),
-            });
-        return name;
+        return nullptr;
       });
-      return ctx.dst->Call(fn, ctx.CloneWithoutTransform(expr));
-    }
-    return nullptr;
-  });
 
   ctx.Clone();
 }
diff --git a/src/transform/first_index_offset.cc b/src/transform/first_index_offset.cc
index 0ae7ffa..4bc21c2 100644
--- a/src/transform/first_index_offset.cc
+++ b/src/transform/first_index_offset.cc
@@ -80,7 +80,7 @@
   // parameters) or structure member accesses.
   for (auto* node : ctx.src->ASTNodes().Objects()) {
     if (auto* var = node->As<ast::Variable>()) {
-      for (ast::Decoration* dec : var->decorations) {
+      for (auto* dec : var->decorations) {
         if (auto* builtin_dec = dec->As<ast::BuiltinDecoration>()) {
           ast::Builtin builtin = builtin_dec->builtin;
           if (builtin == ast::Builtin::kVertexIndex) {
@@ -97,7 +97,7 @@
       }
     }
     if (auto* member = node->As<ast::StructMember>()) {
-      for (ast::Decoration* dec : member->decorations) {
+      for (auto* dec : member->decorations) {
         if (auto* builtin_dec = dec->As<ast::BuiltinDecoration>()) {
           ast::Builtin builtin = builtin_dec->builtin;
           if (builtin == ast::Builtin::kVertexIndex) {
@@ -147,28 +147,29 @@
                     });
 
     // Fix up all references to the builtins with the offsets
-    ctx.ReplaceAll([=, &ctx](ast::Expression* expr) -> ast::Expression* {
-      if (auto* sem = ctx.src->Sem().Get(expr)) {
-        if (auto* user = sem->As<sem::VariableUser>()) {
-          auto it = builtin_vars.find(user->Variable());
-          if (it != builtin_vars.end()) {
-            return ctx.dst->Add(
-                ctx.CloneWithoutTransform(expr),
-                ctx.dst->MemberAccessor(buffer_name, it->second));
+    ctx.ReplaceAll(
+        [=, &ctx](const ast::Expression* expr) -> const ast::Expression* {
+          if (auto* sem = ctx.src->Sem().Get(expr)) {
+            if (auto* user = sem->As<sem::VariableUser>()) {
+              auto it = builtin_vars.find(user->Variable());
+              if (it != builtin_vars.end()) {
+                return ctx.dst->Add(
+                    ctx.CloneWithoutTransform(expr),
+                    ctx.dst->MemberAccessor(buffer_name, it->second));
+              }
+            }
+            if (auto* access = sem->As<sem::StructMemberAccess>()) {
+              auto it = builtin_members.find(access->Member());
+              if (it != builtin_members.end()) {
+                return ctx.dst->Add(
+                    ctx.CloneWithoutTransform(expr),
+                    ctx.dst->MemberAccessor(buffer_name, it->second));
+              }
+            }
           }
-        }
-        if (auto* access = sem->As<sem::StructMemberAccess>()) {
-          auto it = builtin_members.find(access->Member());
-          if (it != builtin_members.end()) {
-            return ctx.dst->Add(
-                ctx.CloneWithoutTransform(expr),
-                ctx.dst->MemberAccessor(buffer_name, it->second));
-          }
-        }
-      }
-      // Not interested in this experssion. Just clone.
-      return nullptr;
-    });
+          // Not interested in this experssion. Just clone.
+          return nullptr;
+        });
   }
 
   ctx.Clone();
diff --git a/src/transform/first_index_offset.h b/src/transform/first_index_offset.h
index 2ba27b5..21a618a 100644
--- a/src/transform/first_index_offset.h
+++ b/src/transform/first_index_offset.h
@@ -98,13 +98,13 @@
     ~Data() override;
 
     /// True if the shader uses vertex_index
-    bool const has_vertex_index;
+    const bool has_vertex_index;
     /// True if the shader uses instance_index
-    bool const has_instance_index;
+    const bool has_instance_index;
     /// Offset of first vertex into constant buffer
-    uint32_t const first_vertex_offset;
+    const uint32_t first_vertex_offset;
     /// Offset of first instance into constant buffer
-    uint32_t const first_instance_offset;
+    const uint32_t first_instance_offset;
   };
 
   /// Constructor
diff --git a/src/transform/fold_constants.cc b/src/transform/fold_constants.cc
index 54f7f15..271a930 100644
--- a/src/transform/fold_constants.cc
+++ b/src/transform/fold_constants.cc
@@ -31,7 +31,7 @@
 FoldConstants::~FoldConstants() = default;
 
 void FoldConstants::Run(CloneContext& ctx, const DataMap&, DataMap&) {
-  ctx.ReplaceAll([&](ast::Expression* expr) -> ast::Expression* {
+  ctx.ReplaceAll([&](const ast::Expression* expr) -> const ast::Expression* {
     auto* sem = ctx.src->Sem().Get(expr);
     if (!sem) {
       return nullptr;
diff --git a/src/transform/fold_trivial_single_use_lets.cc b/src/transform/fold_trivial_single_use_lets.cc
index 78d77e3..0427075 100644
--- a/src/transform/fold_trivial_single_use_lets.cc
+++ b/src/transform/fold_trivial_single_use_lets.cc
@@ -27,7 +27,7 @@
 namespace transform {
 namespace {
 
-ast::VariableDeclStatement* AsTrivialLetDecl(ast::Statement* stmt) {
+const ast::VariableDeclStatement* AsTrivialLetDecl(const ast::Statement* stmt) {
   auto* var_decl = stmt->As<ast::VariableDeclStatement>();
   if (!var_decl) {
     return nullptr;
diff --git a/src/transform/for_loop_to_loop.cc b/src/transform/for_loop_to_loop.cc
index 6b43064..1eab804 100644
--- a/src/transform/for_loop_to_loop.cc
+++ b/src/transform/for_loop_to_loop.cc
@@ -26,37 +26,39 @@
 ForLoopToLoop::~ForLoopToLoop() = default;
 
 void ForLoopToLoop::Run(CloneContext& ctx, const DataMap&, DataMap&) {
-  ctx.ReplaceAll([&](ast::ForLoopStatement* for_loop) -> ast::Statement* {
-    ast::StatementList stmts;
-    if (auto* cond = for_loop->condition) {
-      // !condition
-      auto* not_cond = ctx.dst->create<ast::UnaryOpExpression>(
-          ast::UnaryOp::kNot, ctx.Clone(cond));
+  ctx.ReplaceAll(
+      [&](const ast::ForLoopStatement* for_loop) -> const ast::Statement* {
+        ast::StatementList stmts;
+        if (auto* cond = for_loop->condition) {
+          // !condition
+          auto* not_cond = ctx.dst->create<ast::UnaryOpExpression>(
+              ast::UnaryOp::kNot, ctx.Clone(cond));
 
-      // { break; }
-      auto* break_body = ctx.dst->Block(ctx.dst->create<ast::BreakStatement>());
+          // { break; }
+          auto* break_body =
+              ctx.dst->Block(ctx.dst->create<ast::BreakStatement>());
 
-      // if (!condition) { break; }
-      stmts.emplace_back(ctx.dst->If(not_cond, break_body));
-    }
-    for (auto* stmt : for_loop->body->statements) {
-      stmts.emplace_back(ctx.Clone(stmt));
-    }
+          // if (!condition) { break; }
+          stmts.emplace_back(ctx.dst->If(not_cond, break_body));
+        }
+        for (auto* stmt : for_loop->body->statements) {
+          stmts.emplace_back(ctx.Clone(stmt));
+        }
 
-    ast::BlockStatement* continuing = nullptr;
-    if (auto* cont = for_loop->continuing) {
-      continuing = ctx.dst->Block(ctx.Clone(cont));
-    }
+        const ast::BlockStatement* continuing = nullptr;
+        if (auto* cont = for_loop->continuing) {
+          continuing = ctx.dst->Block(ctx.Clone(cont));
+        }
 
-    auto* body = ctx.dst->Block(stmts);
-    auto* loop = ctx.dst->create<ast::LoopStatement>(body, continuing);
+        auto* body = ctx.dst->Block(stmts);
+        auto* loop = ctx.dst->create<ast::LoopStatement>(body, continuing);
 
-    if (auto* init = for_loop->initializer) {
-      return ctx.dst->Block(ctx.Clone(init), loop);
-    }
+        if (auto* init = for_loop->initializer) {
+          return ctx.dst->Block(ctx.Clone(init), loop);
+        }
 
-    return loop;
-  });
+        return loop;
+      });
 
   ctx.Clone();
 }
diff --git a/src/transform/inline_pointer_lets.cc b/src/transform/inline_pointer_lets.cc
index 85287a2..196c0d5 100644
--- a/src/transform/inline_pointer_lets.cc
+++ b/src/transform/inline_pointer_lets.cc
@@ -41,7 +41,7 @@
 /// expression
 template <typename F>
 void CollectSavedArrayIndices(const Program* program,
-                              ast::Expression* expr,
+                              const ast::Expression* expr,
                               F&& cb) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     CollectSavedArrayIndices(program, a->array, cb);
@@ -95,7 +95,7 @@
   // * Sub-expressions inside the pointer-typed `let` initializer expression
   // that have been hoisted to a saved variable are replaced with the saved
   // variable identifier.
-  ctx.ReplaceAll([&](ast::Expression* expr) -> ast::Expression* {
+  ctx.ReplaceAll([&](const ast::Expression* expr) -> const ast::Expression* {
     if (current_ptr_let) {
       // We're currently processing the initializer expression of a
       // pointer-typed `let` declaration. Look to see if we need to swap this
@@ -150,7 +150,7 @@
       // to be hoist to temporary "saved" variables.
       CollectSavedArrayIndices(
           ctx.src, var->Declaration()->constructor,
-          [&](ast::Expression* idx_expr) {
+          [&](const ast::Expression* idx_expr) {
             // We have a sub-expression that needs to be saved.
             // Create a new variable
             auto saved_name = ctx.dst->Symbols().New(
diff --git a/src/transform/loop_to_for_loop.cc b/src/transform/loop_to_for_loop.cc
index 34d4524..d2da837 100644
--- a/src/transform/loop_to_for_loop.cc
+++ b/src/transform/loop_to_for_loop.cc
@@ -29,7 +29,7 @@
 namespace transform {
 namespace {
 
-bool IsBlockWithSingleBreak(ast::BlockStatement* block) {
+bool IsBlockWithSingleBreak(const ast::BlockStatement* block) {
   if (block->statements.size() != 1) {
     return false;
   }
@@ -37,8 +37,8 @@
 }
 
 bool IsVarUsedByStmt(const sem::Info& sem,
-                     ast::Variable* var,
-                     ast::Statement* stmt) {
+                     const ast::Variable* var,
+                     const ast::Statement* stmt) {
   auto* var_sem = sem.Get(var);
   for (auto* user : var_sem->Users()) {
     if (auto* s = user->Stmt()) {
@@ -57,7 +57,7 @@
 LoopToForLoop::~LoopToForLoop() = default;
 
 void LoopToForLoop::Run(CloneContext& ctx, const DataMap&, DataMap&) {
-  ctx.ReplaceAll([&](ast::LoopStatement* loop) -> ast::Statement* {
+  ctx.ReplaceAll([&](const ast::LoopStatement* loop) -> const ast::Statement* {
     // For loop condition is taken from the first statement in the loop.
     // This requires an if-statement with either:
     //  * A true block with no else statements, and the true block contains a
@@ -90,7 +90,7 @@
 
     // The continuing block must be empty or contain a single, assignment or
     // function call statement.
-    ast::Statement* continuing = nullptr;
+    const ast::Statement* continuing = nullptr;
     if (auto* loop_cont = loop->continuing) {
       if (loop_cont->statements.size() != 1) {
         return nullptr;
diff --git a/src/transform/module_scope_var_to_entry_point_param.cc b/src/transform/module_scope_var_to_entry_point_param.cc
index f71a010..0eeaa53 100644
--- a/src/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/transform/module_scope_var_to_entry_point_param.cc
@@ -77,7 +77,7 @@
       // Clone the struct and add it to the global declaration list.
       // Remove the old declaration.
       auto* ast_str = str->Declaration();
-      ctx.dst->AST().AddTypeDecl(ctx.Clone(const_cast<ast::Struct*>(ast_str)));
+      ctx.dst->AST().AddTypeDecl(ctx.Clone(ast_str));
       ctx.Remove(ctx.src->AST().GlobalDeclarations(), ast_str);
     } else if (auto* arr = ty->As<sem::Array>()) {
       CloneStructTypes(arr->ElemType());
@@ -90,7 +90,7 @@
     using CallList = std::vector<const ast::CallExpression*>;
     std::unordered_map<const ast::Function*, CallList> calls_to_replace;
 
-    std::vector<ast::Function*> functions_to_process;
+    std::vector<const ast::Function*> functions_to_process;
 
     // Build a list of functions that transitively reference any private or
     // workgroup variables, or texture/sampler variables.
@@ -123,7 +123,8 @@
     // rules when this expression is passed to a function.
     // TODO(jrprice): We should add support for bidirectional SEM tree traversal
     // so that we can do this on the fly instead.
-    std::unordered_map<ast::IdentifierExpression*, ast::UnaryOpExpression*>
+    std::unordered_map<const ast::IdentifierExpression*,
+                       const ast::UnaryOpExpression*>
         ident_to_address_of;
     for (auto* node : ctx.src->ASTNodes().Objects()) {
       auto* address_of = node->As<ast::UnaryOpExpression>();
@@ -248,7 +249,7 @@
         // For non-entry points, dereference non-handle pointer parameters.
         for (auto* user : var->Users()) {
           if (user->Stmt()->Function() == func_ast) {
-            ast::Expression* expr = ctx.dst->Expr(new_var_symbol);
+            const ast::Expression* expr = ctx.dst->Expr(new_var_symbol);
             if (is_pointer) {
               // If this identifier is used by an address-of operator, just
               // remove the address-of instead of adding a deref, since we
@@ -301,7 +302,8 @@
               target_var->StorageClass() == ast::StorageClass::kWorkgroup ||
               target_var->StorageClass() ==
                   ast::StorageClass::kUniformConstant) {
-            ast::Expression* arg = ctx.dst->Expr(var_to_symbol[target_var]);
+            const ast::Expression* arg =
+                ctx.dst->Expr(var_to_symbol[target_var]);
             if (is_entry_point && !is_handle && !is_workgroup_matrix) {
               arg = ctx.dst->AddressOf(arg);
             }
diff --git a/src/transform/num_workgroups_from_uniform.cc b/src/transform/num_workgroups_from_uniform.cc
index 4b2815c..62faf9a 100644
--- a/src/transform/num_workgroups_from_uniform.cc
+++ b/src/transform/num_workgroups_from_uniform.cc
@@ -116,7 +116,7 @@
 
   // Get (or create, on first call) the uniform buffer that will receive the
   // number of workgroups.
-  ast::Variable* num_workgroups_ubo = nullptr;
+  const ast::Variable* num_workgroups_ubo = nullptr;
   auto get_ubo = [&]() {
     if (!num_workgroups_ubo) {
       auto* num_workgroups_struct = ctx.dst->Structure(
diff --git a/src/transform/pad_array_elements.cc b/src/transform/pad_array_elements.cc
index ff3f8df..58f6836 100644
--- a/src/transform/pad_array_elements.cc
+++ b/src/transform/pad_array_elements.cc
@@ -28,7 +28,7 @@
 namespace transform {
 namespace {
 
-using ArrayBuilder = std::function<ast::Array*()>;
+using ArrayBuilder = std::function<const ast::Array*()>;
 
 /// PadArray returns a function that constructs a new array in `ctx.dst` with
 /// the element type padded to account for the explicit stride. PadArray will
@@ -55,7 +55,7 @@
     auto name = ctx.dst->Symbols().New("tint_padded_array_element");
 
     // Examine the element type. Is it also an array?
-    ast::Type* el_ty = nullptr;
+    const ast::Type* el_ty = nullptr;
     if (auto* el_array = array->ElemType()->As<sem::Array>()) {
       // Array of array - call PadArray() on the element type
       if (auto p =
@@ -104,7 +104,7 @@
   };
 
   // Replace all array types with their corresponding padded array type
-  ctx.ReplaceAll([&](ast::Type* ast_type) -> ast::Type* {
+  ctx.ReplaceAll([&](const ast::Type* ast_type) -> const ast::Type* {
     auto* type = ctx.src->TypeOf(ast_type);
     if (auto* array = type->UnwrapRef()->As<sem::Array>()) {
       if (auto p = pad(array)) {
@@ -115,23 +115,24 @@
   });
 
   // Fix up array accessors so `a[1]` becomes `a[1].el`
-  ctx.ReplaceAll(
-      [&](ast::ArrayAccessorExpression* accessor) -> ast::Expression* {
-        if (auto* array = tint::As<sem::Array>(
-                sem.Get(accessor->array)->Type()->UnwrapRef())) {
-          if (pad(array)) {
-            // Array element is wrapped in a structure. Emit a member accessor
-            // to get to the actual array element.
-            auto* idx = ctx.CloneWithoutTransform(accessor);
-            return ctx.dst->MemberAccessor(idx, "el");
-          }
-        }
-        return nullptr;
-      });
+  ctx.ReplaceAll([&](const ast::ArrayAccessorExpression* accessor)
+                     -> const ast::Expression* {
+    if (auto* array = tint::As<sem::Array>(
+            sem.Get(accessor->array)->Type()->UnwrapRef())) {
+      if (pad(array)) {
+        // Array element is wrapped in a structure. Emit a member accessor
+        // to get to the actual array element.
+        auto* idx = ctx.CloneWithoutTransform(accessor);
+        return ctx.dst->MemberAccessor(idx, "el");
+      }
+    }
+    return nullptr;
+  });
 
   // Fix up array constructors so `A(1,2)` becomes
   // `A(padded(1), padded(2))`
-  ctx.ReplaceAll([&](ast::TypeConstructorExpression* ctor) -> ast::Expression* {
+  ctx.ReplaceAll([&](const ast::TypeConstructorExpression* ctor)
+                     -> const ast::Expression* {
     if (auto* array =
             tint::As<sem::Array>(sem.Get(ctor)->Type()->UnwrapRef())) {
       if (auto p = pad(array)) {
diff --git a/src/transform/renamer.cc b/src/transform/renamer.cc
index 38fbc35..647db82 100644
--- a/src/transform/renamer.cc
+++ b/src/transform/renamer.cc
@@ -1131,7 +1131,7 @@
 
   // Swizzles, intrinsic calls and builtin structure members need to keep their
   // symbols preserved.
-  std::unordered_set<ast::IdentifierExpression*> preserve;
+  std::unordered_set<const ast::IdentifierExpression*> preserve;
   for (auto* node : in->ASTNodes().Objects()) {
     if (auto* member = node->As<ast::MemberAccessorExpression>()) {
       auto* sem = in->Sem().Get(member);
@@ -1213,17 +1213,17 @@
     return sym_out;
   });
 
-  ctx.ReplaceAll(
-      [&](ast::IdentifierExpression* ident) -> ast::IdentifierExpression* {
-        if (preserve.count(ident)) {
-          auto sym_in = ident->symbol;
-          auto str = in->Symbols().NameFor(sym_in);
-          auto sym_out = out.Symbols().Register(str);
-          return ctx.dst->create<ast::IdentifierExpression>(
-              ctx.Clone(ident->source), sym_out);
-        }
-        return nullptr;  // Clone ident. Uses the symbol remapping above.
-      });
+  ctx.ReplaceAll([&](const ast::IdentifierExpression* ident)
+                     -> const ast::IdentifierExpression* {
+    if (preserve.count(ident)) {
+      auto sym_in = ident->symbol;
+      auto str = in->Symbols().NameFor(sym_in);
+      auto sym_out = out.Symbols().Register(str);
+      return ctx.dst->create<ast::IdentifierExpression>(
+          ctx.Clone(ident->source), sym_out);
+    }
+    return nullptr;  // Clone ident. Uses the symbol remapping above.
+  });
   ctx.Clone();
 
   return Output(Program(std::move(out)),
diff --git a/src/transform/renamer.h b/src/transform/renamer.h
index 2e0586e..2c2ea0a 100644
--- a/src/transform/renamer.h
+++ b/src/transform/renamer.h
@@ -43,7 +43,7 @@
     ~Data() override;
 
     /// A map of old symbol name to new symbol name
-    Remappings const remappings;
+    const Remappings remappings;
   };
 
   /// Target is an enumerator of rename targets that can be used
diff --git a/src/transform/robustness.cc b/src/transform/robustness.cc
index 6bd7678..28cd0f9 100644
--- a/src/transform/robustness.cc
+++ b/src/transform/robustness.cc
@@ -41,16 +41,19 @@
 
   /// Applies the transformation state to `ctx`.
   void Transform() {
+    ctx.ReplaceAll([&](const ast::ArrayAccessorExpression* expr) {
+      return Transform(expr);
+    });
     ctx.ReplaceAll(
-        [&](ast::ArrayAccessorExpression* expr) { return Transform(expr); });
-    ctx.ReplaceAll([&](ast::CallExpression* expr) { return Transform(expr); });
+        [&](const ast::CallExpression* expr) { return Transform(expr); });
   }
 
   /// Apply bounds clamping to array, vector and matrix indexing
   /// @param expr the array, vector or matrix index expression
   /// @return the clamped replacement expression, or nullptr if `expr` should be
   /// cloned without changes.
-  ast::ArrayAccessorExpression* Transform(ast::ArrayAccessorExpression* expr) {
+  const ast::ArrayAccessorExpression* Transform(
+      const ast::ArrayAccessorExpression* expr) {
     auto* ret_type = ctx.src->Sem().Get(expr->array)->Type();
 
     auto* ref = ret_type->As<sem::Reference>();
@@ -64,7 +67,7 @@
     using u32 = ProgramBuilder::u32;
 
     struct Value {
-      ast::Expression* expr = nullptr;  // If null, then is a constant
+      const ast::Expression* expr = nullptr;  // If null, then is a constant
       union {
         uint32_t u32 = 0;  // use if is_signed == false
         int32_t i32;       // use if is_signed == true
@@ -208,7 +211,7 @@
   /// @param expr the intrinsic call expression
   /// @return the clamped replacement call expression, or nullptr if `expr`
   /// should be cloned without changes.
-  ast::CallExpression* Transform(ast::CallExpression* expr) {
+  const ast::CallExpression* Transform(const ast::CallExpression* expr) {
     auto* call = ctx.src->Sem().Get(expr);
     auto* call_target = call->Target();
     auto* intrinsic = call_target->As<sem::Intrinsic>();
@@ -235,7 +238,7 @@
     // to clamp both usages.
     // TODO(bclayton): We probably want to place this into a let so that the
     // calculation can be reused. This is fiddly to get right.
-    std::function<ast::Expression*()> level_arg;
+    std::function<const ast::Expression*()> level_arg;
     if (level_idx >= 0) {
       level_arg = [&] {
         auto* arg = expr->args[level_idx];
diff --git a/src/transform/simplify.cc b/src/transform/simplify.cc
index 2d414d7..7d26ee2 100644
--- a/src/transform/simplify.cc
+++ b/src/transform/simplify.cc
@@ -35,7 +35,7 @@
 Simplify::~Simplify() = default;
 
 void Simplify::Run(CloneContext& ctx, const DataMap&, DataMap&) {
-  ctx.ReplaceAll([&](ast::Expression* expr) -> ast::Expression* {
+  ctx.ReplaceAll([&](const ast::Expression* expr) -> const ast::Expression* {
     if (auto* outer = expr->As<ast::UnaryOpExpression>()) {
       if (auto* inner = outer->expr->As<ast::UnaryOpExpression>()) {
         if (outer->op == ast::UnaryOp::kAddressOf &&
diff --git a/src/transform/single_entry_point.cc b/src/transform/single_entry_point.cc
index 9a3135a..531a1de 100644
--- a/src/transform/single_entry_point.cc
+++ b/src/transform/single_entry_point.cc
@@ -42,7 +42,7 @@
   }
 
   // Find the target entry point.
-  ast::Function* entry_point = nullptr;
+  const ast::Function* entry_point = nullptr;
   for (auto* f : ctx.src->AST().Functions()) {
     if (!f->IsEntryPoint()) {
       continue;
diff --git a/src/transform/transform.cc b/src/transform/transform.cc
index 32ca39d..34f5ec6 100644
--- a/src/transform/transform.cc
+++ b/src/transform/transform.cc
@@ -90,7 +90,8 @@
       << sem->TypeInfo().name;
 }
 
-ast::Type* Transform::CreateASTTypeFor(CloneContext& ctx, const sem::Type* ty) {
+const ast::Type* Transform::CreateASTTypeFor(CloneContext& ctx,
+                                             const sem::Type* ty) {
   if (ty->Is<sem::Void>()) {
     return ctx.dst->create<ast::Void>();
   }
diff --git a/src/transform/transform.h b/src/transform/transform.h
index f270e27..6d34997 100644
--- a/src/transform/transform.h
+++ b/src/transform/transform.h
@@ -198,7 +198,8 @@
   /// @param ty the semantic type to reconstruct
   /// @returns a ast::Type that when resolved, will produce the semantic type
   /// `ty`.
-  static ast::Type* CreateASTTypeFor(CloneContext& ctx, const sem::Type* ty);
+  static const ast::Type* CreateASTTypeFor(CloneContext& ctx,
+                                           const sem::Type* ty);
 };
 
 }  // namespace transform
diff --git a/src/transform/transform_test.cc b/src/transform/transform_test.cc
index 6b9d352..001e54a 100644
--- a/src/transform/transform_test.cc
+++ b/src/transform/transform_test.cc
@@ -26,7 +26,7 @@
 struct CreateASTTypeForTest : public testing::Test, public Transform {
   Output Run(const Program*, const DataMap&) override { return {}; }
 
-  ast::Type* create(
+  const ast::Type* create(
       std::function<sem::Type*(ProgramBuilder&)> create_sem_type) {
     ProgramBuilder sem_type_builder;
     auto* sem_type = create_sem_type(sem_type_builder);
diff --git a/src/transform/vertex_pulling.cc b/src/transform/vertex_pulling.cc
index ae89e3d..1fc01fb 100644
--- a/src/transform/vertex_pulling.cc
+++ b/src/transform/vertex_pulling.cc
@@ -137,7 +137,7 @@
   uint32_t width;  // 1 for scalar, 2+ for a vector
 };
 
-DataType DataTypeOf(sem::Type* ty) {
+DataType DataTypeOf(const sem::Type* ty) {
   if (ty->Is<sem::I32>()) {
     return {BaseType::kI32, 1};
   }
@@ -217,15 +217,15 @@
   };
 
   struct LocationInfo {
-    std::function<ast::Expression*()> expr;
-    sem::Type* type;
+    std::function<const ast::Expression*()> expr;
+    const sem::Type* type;
   };
 
   CloneContext& ctx;
   VertexPulling::Config const cfg;
   std::unordered_map<uint32_t, LocationInfo> location_info;
-  std::function<ast::Expression*()> vertex_index_expr = nullptr;
-  std::function<ast::Expression*()> instance_index_expr = nullptr;
+  std::function<const ast::Expression*()> vertex_index_expr = nullptr;
+  std::function<const ast::Expression*()> instance_index_expr = nullptr;
   Symbol pulling_position_name;
   Symbol struct_buffer_name;
   std::unordered_map<uint32_t, Symbol> vertex_buffer_names;
@@ -369,7 +369,7 @@
           }
         } else if (var_dt.width > fmt_dt.width) {
           // WGSL variable vector width is wider than the loaded vector width
-          ast::Type* ty = nullptr;
+          const ast::Type* ty = nullptr;
           ast::ExpressionList values{fetch};
           switch (var_dt.base_type) {
             case BaseType::kI32:
@@ -416,10 +416,10 @@
   /// @param offset the byte offset of the data from `buffer_base`
   /// @param buffer the index of the vertex buffer
   /// @param format the format to read
-  ast::Expression* Fetch(Symbol array_base,
-                         uint32_t offset,
-                         uint32_t buffer,
-                         VertexFormat format) {
+  const ast::Expression* Fetch(Symbol array_base,
+                               uint32_t offset,
+                               uint32_t buffer,
+                               VertexFormat format) {
     using u32 = ProgramBuilder::u32;
     using i32 = ProgramBuilder::i32;
     using f32 = ProgramBuilder::f32;
@@ -642,15 +642,15 @@
   /// @param buffer the index of the vertex buffer
   /// @param format VertexFormat::kUint32, VertexFormat::kSint32 or
   /// VertexFormat::kFloat32
-  ast::Expression* LoadPrimitive(Symbol array_base,
-                                 uint32_t offset,
-                                 uint32_t buffer,
-                                 VertexFormat format) {
-    ast::Expression* u32 = nullptr;
+  const ast::Expression* LoadPrimitive(Symbol array_base,
+                                       uint32_t offset,
+                                       uint32_t buffer,
+                                       VertexFormat format) {
+    const ast::Expression* u32 = nullptr;
     if ((offset & 3) == 0) {
       // Aligned load.
 
-      ast ::Expression* index = nullptr;
+      const ast ::Expression* index = nullptr;
       if (offset > 0) {
         index = ctx.dst->Add(array_base, offset / 4);
       } else {
@@ -700,13 +700,13 @@
   /// @param base_type underlying AST type
   /// @param base_format underlying vertex format
   /// @param count how many elements the vector has
-  ast::Expression* LoadVec(Symbol array_base,
-                           uint32_t offset,
-                           uint32_t buffer,
-                           uint32_t element_stride,
-                           ast::Type* base_type,
-                           VertexFormat base_format,
-                           uint32_t count) {
+  const ast::Expression* LoadVec(Symbol array_base,
+                                 uint32_t offset,
+                                 uint32_t buffer,
+                                 uint32_t element_stride,
+                                 const ast::Type* base_type,
+                                 VertexFormat base_format,
+                                 uint32_t count) {
     ast::ExpressionList expr_list;
     for (uint32_t i = 0; i < count; ++i) {
       // Offset read position by element_stride for each component
@@ -724,7 +724,8 @@
   /// vertex_index and instance_index builtins if present.
   /// @param func the entry point function
   /// @param param the parameter to process
-  void ProcessNonStructParameter(ast::Function* func, ast::Variable* param) {
+  void ProcessNonStructParameter(const ast::Function* func,
+                                 const ast::Variable* param) {
     if (auto* location =
             ast::GetDecoration<ast::LocationDecoration>(param->decorations)) {
       // Create a function-scope variable to replace the parameter.
@@ -764,8 +765,8 @@
   /// @param func the entry point function
   /// @param param the parameter to process
   /// @param struct_ty the structure type
-  void ProcessStructParameter(ast::Function* func,
-                              ast::Variable* param,
+  void ProcessStructParameter(const ast::Function* func,
+                              const ast::Variable* param,
                               const ast::Struct* struct_ty) {
     auto param_sym = ctx.Clone(param->symbol);
 
@@ -774,8 +775,8 @@
     ast::StructMemberList members_to_clone;
     for (auto* member : struct_ty->members) {
       auto member_sym = ctx.Clone(member->symbol);
-      std::function<ast::Expression*()> member_expr = [this, param_sym,
-                                                       member_sym]() {
+      std::function<const ast::Expression*()> member_expr = [this, param_sym,
+                                                             member_sym]() {
         return ctx.dst->MemberAccessor(param_sym, member_sym);
       };
 
@@ -842,7 +843,7 @@
 
   /// Process an entry point function.
   /// @param func the entry point function
-  void Process(ast::Function* func) {
+  void Process(const ast::Function* func) {
     if (func->body->Empty()) {
       return;
     }
diff --git a/src/transform/wrap_arrays_in_structs.cc b/src/transform/wrap_arrays_in_structs.cc
index 6d6f90c..782004f 100644
--- a/src/transform/wrap_arrays_in_structs.cc
+++ b/src/transform/wrap_arrays_in_structs.cc
@@ -48,7 +48,7 @@
   };
 
   // Replace all array types with their corresponding wrapper
-  ctx.ReplaceAll([&](ast::Type* ast_type) -> ast::Type* {
+  ctx.ReplaceAll([&](const ast::Type* ast_type) -> const ast::Type* {
     auto* type = ctx.src->TypeOf(ast_type);
     if (auto* array = type->UnwrapRef()->As<sem::Array>()) {
       return wrapper_typename(array);
@@ -57,8 +57,8 @@
   });
 
   // Fix up array accessors so `a[1]` becomes `a.arr[1]`
-  ctx.ReplaceAll([&](ast::ArrayAccessorExpression* accessor)
-                     -> ast::ArrayAccessorExpression* {
+  ctx.ReplaceAll([&](const ast::ArrayAccessorExpression* accessor)
+                     -> const ast::ArrayAccessorExpression* {
     if (auto* array = ::tint::As<sem::Array>(
             sem.Get(accessor->array)->Type()->UnwrapRef())) {
       if (wrapper(array)) {
@@ -74,7 +74,8 @@
   });
 
   // Fix up array constructors so `A(1,2)` becomes `tint_array_wrapper(A(1,2))`
-  ctx.ReplaceAll([&](ast::TypeConstructorExpression* ctor) -> ast::Expression* {
+  ctx.ReplaceAll([&](const ast::TypeConstructorExpression* ctor)
+                     -> const ast::Expression* {
     if (auto* array =
             ::tint::As<sem::Array>(sem.Get(ctor)->Type()->UnwrapRef())) {
       if (auto w = wrapper(array)) {
@@ -107,7 +108,7 @@
     info.wrapper_name = ctx.dst->Symbols().New("tint_array_wrapper");
 
     // Examine the element type. Is it also an array?
-    std::function<ast::Type*(CloneContext&)> el_type;
+    std::function<const ast::Type*(CloneContext&)> el_type;
     if (auto* el_array = array->ElemType()->As<sem::Array>()) {
       // Array of array - call WrapArray() on the element type
       if (auto el = WrapArray(ctx, wrapped_arrays, el_array)) {
diff --git a/src/transform/wrap_arrays_in_structs.h b/src/transform/wrap_arrays_in_structs.h
index 6eb4bc1..64dc047 100644
--- a/src/transform/wrap_arrays_in_structs.h
+++ b/src/transform/wrap_arrays_in_structs.h
@@ -60,7 +60,7 @@
     ~WrappedArrayInfo();
 
     Symbol wrapper_name;
-    std::function<ast::Type*(CloneContext&)> array_type;
+    std::function<const ast::Type*(CloneContext&)> array_type;
 
     operator bool() { return wrapper_name.IsValid(); }
   };
diff --git a/src/transform/zero_init_workgroup_memory.cc b/src/transform/zero_init_workgroup_memory.cc
index edd51c3..e5f3f8e 100644
--- a/src/transform/zero_init_workgroup_memory.cc
+++ b/src/transform/zero_init_workgroup_memory.cc
@@ -46,7 +46,7 @@
   uint32_t workgroup_size_const = 0;
   /// The size of the workgroup as an expression generator. Use if
   /// #workgroup_size_const is 0.
-  std::function<ast::Expression*()> workgroup_size_expr;
+  std::function<const ast::Expression*()> workgroup_size_expr;
 
   /// ArrayIndex represents a function on the local invocation index, of
   /// the form: `array_index = (local_invocation_index % modulo) / division`
@@ -80,7 +80,7 @@
   /// statement will zero workgroup values.
   struct Expression {
     /// The AST expression node
-    ast::Expression* expr = nullptr;
+    const ast::Expression* expr = nullptr;
     /// The number of iterations required to zero the value
     uint32_t num_iterations = 0;
     /// All array indices used by this expression
@@ -91,7 +91,7 @@
   /// values.
   struct Statement {
     /// The AST statement node
-    ast::Statement* stmt;
+    const ast::Statement* stmt;
     /// The number of iterations required to zero the value
     uint32_t num_iterations;
     /// All array indices used by this statement
@@ -112,7 +112,7 @@
   /// Run inserts the workgroup memory zero-initialization logic at the top of
   /// the given function
   /// @param fn a compute shader entry point function
-  void Run(ast::Function* fn) {
+  void Run(const ast::Function* fn) {
     auto& sem = ctx.src->Sem();
 
     CalculateWorkgroupSize(
@@ -137,7 +137,7 @@
 
     // Scan the entry point for an existing local_invocation_index builtin
     // parameter
-    std::function<ast::Expression*()> local_index;
+    std::function<const ast::Expression*()> local_index;
     for (auto* param : fn->params) {
       if (auto* builtin =
               ast::GetDecoration<ast::BuiltinDecoration>(param->decorations)) {
@@ -341,7 +341,7 @@
   ast::StatementList DeclareArrayIndices(
       uint32_t num_iterations,
       const ArrayIndices& array_indices,
-      const std::function<ast::Expression*()>& iteration) {
+      const std::function<const ast::Expression*()>& iteration) {
     ast::StatementList stmts;
     std::map<Symbol, ArrayIndex> indices_by_name;
     for (auto index : array_indices) {
diff --git a/src/writer/append_vector.cc b/src/writer/append_vector.cc
index 0da52c0..18ea614 100644
--- a/src/writer/append_vector.cc
+++ b/src/writer/append_vector.cc
@@ -23,8 +23,9 @@
 
 namespace {
 
-ast::TypeConstructorExpression* AsVectorConstructor(ProgramBuilder* b,
-                                                    ast::Expression* expr) {
+const ast::TypeConstructorExpression* AsVectorConstructor(
+    ProgramBuilder* b,
+    const ast::Expression* expr) {
   if (auto* constructor = expr->As<ast::TypeConstructorExpression>()) {
     if (b->TypeOf(constructor)->Is<sem::Vector>()) {
       return constructor;
@@ -35,9 +36,10 @@
 
 }  // namespace
 
-ast::TypeConstructorExpression* AppendVector(ProgramBuilder* b,
-                                             ast::Expression* vector,
-                                             ast::Expression* scalar) {
+const ast::TypeConstructorExpression* AppendVector(
+    ProgramBuilder* b,
+    const ast::Expression* vector,
+    const ast::Expression* scalar) {
   uint32_t packed_size;
   const sem::Type* packed_el_sem_ty;
   auto* vector_sem = b->Sem().Get(vector);
@@ -50,7 +52,7 @@
     packed_el_sem_ty = vector_ty;
   }
 
-  ast::Type* packed_el_ty = nullptr;
+  const ast::Type* packed_el_ty = nullptr;
   if (packed_el_sem_ty->Is<sem::I32>()) {
     packed_el_ty = b->create<ast::I32>();
   } else if (packed_el_sem_ty->Is<sem::U32>()) {
@@ -83,7 +85,7 @@
     const auto num_supplied = vc->values.size();
     if (num_supplied == 0) {
       // Zero-value vector constructor. Populate with zeros
-      auto buildZero = [&]() -> ast::ScalarConstructorExpression* {
+      auto buildZero = [&]() -> const ast::ScalarConstructorExpression* {
         if (packed_el_sem_ty->Is<sem::I32>()) {
           return b->Expr(0);
         } else if (packed_el_sem_ty->Is<sem::U32>()) {
diff --git a/src/writer/append_vector.h b/src/writer/append_vector.h
index 3955a31..e95d4b4 100644
--- a/src/writer/append_vector.h
+++ b/src/writer/append_vector.h
@@ -36,9 +36,10 @@
 /// @param scalar the scalar to append to the vector. Must be a scalar.
 /// @returns a vector expression containing the elements of `vector` followed by
 /// the single element of `scalar` cast to the `vector` element type.
-ast::TypeConstructorExpression* AppendVector(ProgramBuilder* builder,
-                                             ast::Expression* vector,
-                                             ast::Expression* scalar);
+const ast::TypeConstructorExpression* AppendVector(
+    ProgramBuilder* builder,
+    const ast::Expression* vector,
+    const ast::Expression* scalar);
 
 }  // namespace writer
 }  // namespace tint
diff --git a/src/writer/glsl/generator_impl.cc b/src/writer/glsl/generator_impl.cc
index b7a79c4..2bbef36 100644
--- a/src/writer/glsl/generator_impl.cc
+++ b/src/writer/glsl/generator_impl.cc
@@ -157,8 +157,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitArrayAccessor(std::ostream& out,
-                                      ast::ArrayAccessorExpression* expr) {
+bool GeneratorImpl::EmitArrayAccessor(
+    std::ostream& out,
+    const ast::ArrayAccessorExpression* expr) {
   if (!EmitExpression(out, expr->array)) {
     return false;
   }
@@ -173,7 +174,7 @@
 }
 
 bool GeneratorImpl::EmitBitcast(std::ostream& out,
-                                ast::BitcastExpression* expr) {
+                                const ast::BitcastExpression* expr) {
   auto* type = TypeOf(expr);
   if (auto* vec = type->UnwrapRef()->As<sem::Vector>()) {
     type = vec->type();
@@ -198,7 +199,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitAssign(ast::AssignmentStatement* stmt) {
+bool GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
   auto out = line();
   if (!EmitExpression(out, stmt->lhs)) {
     return false;
@@ -211,7 +212,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(std::ostream& out,
+                               const ast::BinaryExpression* expr) {
   if (expr->op == ast::BinaryOp::kLogicalAnd ||
       expr->op == ast::BinaryOp::kLogicalOr) {
     auto name = UniqueIdentifier(kTempNamePrefix);
@@ -351,12 +353,13 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBreak(ast::BreakStatement*) {
+bool GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
   line() << "break;";
   return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(std::ostream& out,
+                             const ast::CallExpression* expr) {
   const auto& args = expr->args;
   auto* ident = expr->func;
   auto* call = builder_.Sem().Get(expr);
@@ -456,7 +459,7 @@
 }
 
 bool GeneratorImpl::EmitWorkgroupAtomicCall(std::ostream& out,
-                                            ast::CallExpression* expr,
+                                            const ast::CallExpression* expr,
                                             const sem::Intrinsic* intrinsic) {
   std::string result = UniqueIdentifier("atomic_result");
 
@@ -625,7 +628,7 @@
 }
 
 bool GeneratorImpl::EmitSelectCall(std::ostream& out,
-                                   ast::CallExpression* expr) {
+                                   const ast::CallExpression* expr) {
   auto* expr_false = expr->args[0];
   auto* expr_true = expr->args[1];
   auto* expr_cond = expr->args[2];
@@ -650,7 +653,7 @@
 }
 
 bool GeneratorImpl::EmitModfCall(std::ostream& out,
-                                 ast::CallExpression* expr,
+                                 const ast::CallExpression* expr,
                                  const sem::Intrinsic* intrinsic) {
   if (expr->args.size() == 1) {
     return CallIntrinsicHelper(
@@ -700,7 +703,7 @@
 }
 
 bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
-                                  ast::CallExpression* expr,
+                                  const ast::CallExpression* expr,
                                   const sem::Intrinsic* intrinsic) {
   if (expr->args.size() == 1) {
     return CallIntrinsicHelper(
@@ -771,7 +774,7 @@
 }
 
 bool GeneratorImpl::EmitIsNormalCall(std::ostream& out,
-                                     ast::CallExpression* expr,
+                                     const ast::CallExpression* expr,
                                      const sem::Intrinsic* intrinsic) {
   // GLSL doesn't have a isNormal intrinsic, we need to emulate
   return CallIntrinsicHelper(
@@ -799,7 +802,7 @@
 }
 
 bool GeneratorImpl::EmitDataPackingCall(std::ostream& out,
-                                        ast::CallExpression* expr,
+                                        const ast::CallExpression* expr,
                                         const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -865,7 +868,7 @@
 }
 
 bool GeneratorImpl::EmitDataUnpackingCall(std::ostream& out,
-                                          ast::CallExpression* expr,
+                                          const ast::CallExpression* expr,
                                           const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -951,7 +954,7 @@
 }
 
 bool GeneratorImpl::EmitTextureCall(std::ostream& out,
-                                    ast::CallExpression* expr,
+                                    const ast::CallExpression* expr,
                                     const sem::Intrinsic* intrinsic) {
   using Usage = sem::ParameterUsage;
 
@@ -1072,7 +1075,7 @@
     return false;
   }
 
-  auto emit_vector_appended_with_i32_zero = [&](tint::ast::Expression* vector) {
+  auto emit_vector_appended_with_i32_zero = [&](const ast::Expression* vector) {
     auto* i32 = builder_.create<sem::I32>();
     auto* zero = builder_.Expr(0);
     auto* stmt = builder_.Sem().Get(vector)->Stmt();
@@ -1082,7 +1085,7 @@
     return EmitExpression(out, packed);
   };
 
-  auto emit_vector_appended_with_level = [&](tint::ast::Expression* vector) {
+  auto emit_vector_appended_with_level = [&](const ast::Expression* vector) {
     if (auto* level = arg(Usage::kLevel)) {
       auto* packed = AppendVector(&builder_, vector, level);
       return EmitExpression(out, packed);
@@ -1252,7 +1255,7 @@
   return "";
 }
 
-bool GeneratorImpl::EmitCase(ast::CaseStatement* stmt) {
+bool GeneratorImpl::EmitCase(const ast::CaseStatement* stmt) {
   if (stmt->IsDefault()) {
     line() << "default: {";
   } else {
@@ -1285,7 +1288,7 @@
 }
 
 bool GeneratorImpl::EmitConstructor(std::ostream& out,
-                                    ast::ConstructorExpression* expr) {
+                                    const ast::ConstructorExpression* expr) {
   if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
     return EmitScalarConstructor(out, scalar);
   }
@@ -1294,12 +1297,13 @@
 
 bool GeneratorImpl::EmitScalarConstructor(
     std::ostream& out,
-    ast::ScalarConstructorExpression* expr) {
+    const ast::ScalarConstructorExpression* expr) {
   return EmitLiteral(out, expr->literal);
 }
 
-bool GeneratorImpl::EmitTypeConstructor(std::ostream& out,
-                                        ast::TypeConstructorExpression* expr) {
+bool GeneratorImpl::EmitTypeConstructor(
+    std::ostream& out,
+    const ast::TypeConstructorExpression* expr) {
   auto* type = TypeOf(expr)->UnwrapRef();
 
   // If the type constructor is empty then we need to construct with the zero
@@ -1349,7 +1353,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitContinue(ast::ContinueStatement*) {
+bool GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
   if (!emit_continuing_()) {
     return false;
   }
@@ -1357,14 +1361,15 @@
   return true;
 }
 
-bool GeneratorImpl::EmitDiscard(ast::DiscardStatement*) {
+bool GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
   // TODO(dsinclair): Verify this is correct when the discard semantics are
   // defined for WGSL (https://github.com/gpuweb/gpuweb/issues/361)
   line() << "discard;";
   return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(std::ostream& out,
+                                   const ast::Expression* expr) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     return EmitArrayAccessor(out, a);
   }
@@ -1397,12 +1402,12 @@
 }
 
 bool GeneratorImpl::EmitIdentifier(std::ostream& out,
-                                   ast::IdentifierExpression* expr) {
+                                   const ast::IdentifierExpression* expr) {
   out << builder_.Symbols().NameFor(expr->symbol);
   return true;
 }
 
-bool GeneratorImpl::EmitIf(ast::IfStatement* stmt) {
+bool GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
   {
     auto out = line();
     out << "if (";
@@ -1449,7 +1454,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitFunction(ast::Function* func) {
+bool GeneratorImpl::EmitFunction(const ast::Function* func) {
   auto* sem = builder_.Sem().Get(func);
 
   if (ast::HasDecoration<ast::InternalDecoration>(func->decorations)) {
@@ -1524,7 +1529,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitGlobalVariable(ast::Variable* global) {
+bool GeneratorImpl::EmitGlobalVariable(const ast::Variable* global) {
   if (global->is_const) {
     return EmitProgramConstVariable(global);
   }
@@ -1729,7 +1734,7 @@
   return modifiers;
 }
 
-bool GeneratorImpl::EmitEntryPointFunction(ast::Function* func) {
+bool GeneratorImpl::EmitEntryPointFunction(const ast::Function* func) {
   auto* func_sem = builder_.Sem().Get(func);
 
   {
@@ -1917,7 +1922,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, ast::Literal* lit) {
+bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::Literal* lit) {
   if (auto* l = lit->As<ast::BoolLiteral>()) {
     out << (l->value ? "true" : "false");
   } else if (auto* fl = lit->As<ast::FloatLiteral>()) {
@@ -2014,7 +2019,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitLoop(ast::LoopStatement* stmt) {
+bool GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
   auto emit_continuing = [this, stmt]() {
     if (stmt->continuing && !stmt->continuing->Empty()) {
       if (!EmitBlock(stmt->continuing)) {
@@ -2040,7 +2045,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitForLoop(ast::ForLoopStatement* stmt) {
+bool GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
   // Nest a for loop with a new block. In HLSL the initializer scope is not
   // nested by the for-loop, so we may get variable redefinitions.
   line() << "{";
@@ -2149,8 +2154,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
-                                       ast::MemberAccessorExpression* expr) {
+bool GeneratorImpl::EmitMemberAccessor(
+    std::ostream& out,
+    const ast::MemberAccessorExpression* expr) {
   if (!EmitExpression(out, expr->structure)) {
     return false;
   }
@@ -2166,7 +2172,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitReturn(ast::ReturnStatement* stmt) {
+bool GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
   if (stmt->value) {
     auto out = line();
     out << "return ";
@@ -2180,7 +2186,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitStatement(ast::Statement* stmt) {
+bool GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return EmitAssign(a);
   }
@@ -2236,7 +2242,7 @@
   return false;
 }
 
-bool GeneratorImpl::EmitSwitch(ast::SwitchStatement* stmt) {
+bool GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
   {  // switch(expr) {
     auto out = line();
     out << "switch(";
@@ -2484,7 +2490,7 @@
 }
 
 bool GeneratorImpl::EmitUnaryOp(std::ostream& out,
-                                ast::UnaryOpExpression* expr) {
+                                const ast::UnaryOpExpression* expr) {
   switch (expr->op) {
     case ast::UnaryOp::kIndirection:
     case ast::UnaryOp::kAddressOf:
@@ -2510,7 +2516,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitVariable(ast::Variable* var) {
+bool GeneratorImpl::EmitVariable(const ast::Variable* var) {
   auto* sem = builder_.Sem().Get(var);
   auto* type = sem->Type()->UnwrapRef();
 
@@ -2604,7 +2610,7 @@
 
 template <typename F>
 bool GeneratorImpl::CallIntrinsicHelper(std::ostream& out,
-                                        ast::CallExpression* call,
+                                        const ast::CallExpression* call,
                                         const sem::Intrinsic* intrinsic,
                                         F&& build) {
   // Generate the helper function if it hasn't been created already
diff --git a/src/writer/glsl/generator_impl.h b/src/writer/glsl/generator_impl.h
index c689ba1..4f9e891 100644
--- a/src/writer/glsl/generator_impl.h
+++ b/src/writer/glsl/generator_impl.h
@@ -63,21 +63,22 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the array accessor was emitted
-  bool EmitArrayAccessor(std::ostream& out, ast::ArrayAccessorExpression* expr);
+  bool EmitArrayAccessor(std::ostream& out,
+                         const ast::ArrayAccessorExpression* expr);
   /// Handles an assignment statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitAssign(ast::AssignmentStatement* stmt);
+  bool EmitAssign(const ast::AssignmentStatement* stmt);
   /// Handles generating a binary expression
   /// @param out the output of the expression stream
   /// @param expr the binary expression
   /// @returns true if the expression was emitted, false otherwise
-  bool EmitBinary(std::ostream& out, ast::BinaryExpression* expr);
+  bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
   /// Handles generating a bitcast expression
   /// @param out the output of the expression stream
   /// @param expr the as expression
   /// @returns true if the bitcast was emitted
-  bool EmitBitcast(std::ostream& out, ast::BitcastExpression* expr);
+  bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
   /// Emits a list of statements
   /// @param stmts the statement list
   /// @returns true if the statements were emitted successfully
@@ -93,12 +94,12 @@
   /// Handles a break statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitBreak(ast::BreakStatement* stmt);
+  bool EmitBreak(const ast::BreakStatement* stmt);
   /// Handles generating a call expression
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles generating a barrier intrinsic call
   /// @param out the output of the expression stream
   /// @param intrinsic the semantic information for the barrier intrinsic
@@ -111,7 +112,7 @@
   /// @returns true if the call expression is emitted
   bool EmitStorageAtomicCall(
       std::ostream& out,
-      ast::CallExpression* expr,
+      const ast::CallExpression* expr,
       const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
   /// Handles generating an atomic intrinsic call for a workgroup variable
   /// @param out the output of the expression stream
@@ -119,7 +120,7 @@
   /// @param intrinsic the semantic information for the atomic intrinsic
   /// @returns true if the call expression is emitted
   bool EmitWorkgroupAtomicCall(std::ostream& out,
-                               ast::CallExpression* expr,
+                               const ast::CallExpression* expr,
                                const sem::Intrinsic* intrinsic);
   /// Handles generating a call to a texture function (`textureSample`,
   /// `textureSampleGrad`, etc)
@@ -128,20 +129,20 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitTextureCall(std::ostream& out,
-                       ast::CallExpression* expr,
+                       const ast::CallExpression* expr,
                        const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `select()` intrinsic
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitSelectCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitSelectCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles generating a call to the `modf()` intrinsic
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitModfCall(std::ostream& out,
-                    ast::CallExpression* expr,
+                    const ast::CallExpression* expr,
                     const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `frexp()` intrinsic
   /// @param out the output of the expression stream
@@ -149,7 +150,7 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitFrexpCall(std::ostream& out,
-                     ast::CallExpression* expr,
+                     const ast::CallExpression* expr,
                      const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `isNormal()` intrinsic
   /// @param out the output of the expression stream
@@ -157,7 +158,7 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitIsNormalCall(std::ostream& out,
-                        ast::CallExpression* expr,
+                        const ast::CallExpression* expr,
                         const sem::Intrinsic* intrinsic);
   /// Handles generating a call to data packing intrinsic
   /// @param out the output of the expression stream
@@ -165,7 +166,7 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitDataPackingCall(std::ostream& out,
-                           ast::CallExpression* expr,
+                           const ast::CallExpression* expr,
                            const sem::Intrinsic* intrinsic);
   /// Handles generating a call to data unpacking intrinsic
   /// @param out the output of the expression stream
@@ -173,51 +174,52 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitDataUnpackingCall(std::ostream& out,
-                             ast::CallExpression* expr,
+                             const ast::CallExpression* expr,
                              const sem::Intrinsic* intrinsic);
   /// Handles a case statement
   /// @param stmt the statement
   /// @returns true if the statement was emitted successfully
-  bool EmitCase(ast::CaseStatement* stmt);
+  bool EmitCase(const ast::CaseStatement* stmt);
   /// Handles generating constructor expressions
   /// @param out the output of the expression stream
   /// @param expr the constructor expression
   /// @returns true if the expression was emitted
-  bool EmitConstructor(std::ostream& out, ast::ConstructorExpression* expr);
+  bool EmitConstructor(std::ostream& out,
+                       const ast::ConstructorExpression* expr);
   /// Handles generating a discard statement
   /// @param stmt the discard statement
   /// @returns true if the statement was successfully emitted
-  bool EmitDiscard(ast::DiscardStatement* stmt);
+  bool EmitDiscard(const ast::DiscardStatement* stmt);
   /// Handles generating a scalar constructor
   /// @param out the output of the expression stream
   /// @param expr the scalar constructor expression
   /// @returns true if the scalar constructor is emitted
   bool EmitScalarConstructor(std::ostream& out,
-                             ast::ScalarConstructorExpression* expr);
+                             const ast::ScalarConstructorExpression* expr);
   /// Handles emitting a type constructor
   /// @param out the output of the expression stream
   /// @param expr the type constructor expression
   /// @returns true if the constructor is emitted
   bool EmitTypeConstructor(std::ostream& out,
-                           ast::TypeConstructorExpression* expr);
+                           const ast::TypeConstructorExpression* expr);
   /// Handles a continue statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitContinue(ast::ContinueStatement* stmt);
+  bool EmitContinue(const ast::ContinueStatement* stmt);
   /// Handles generate an Expression
   /// @param out the output of the expression stream
   /// @param expr the expression
   /// @returns true if the expression was emitted
-  bool EmitExpression(std::ostream& out, ast::Expression* expr);
+  bool EmitExpression(std::ostream& out, const ast::Expression* expr);
   /// Handles generating a function
   /// @param func the function to generate
   /// @returns true if the function was emitted
-  bool EmitFunction(ast::Function* func);
+  bool EmitFunction(const ast::Function* func);
 
   /// Handles emitting a global variable
   /// @param global the global variable
   /// @returns true on success
-  bool EmitGlobalVariable(ast::Variable* global);
+  bool EmitGlobalVariable(const ast::Variable* global);
 
   /// Handles emitting a global variable with the uniform storage class
   /// @param var the global variable
@@ -247,47 +249,47 @@
   /// Handles emitting the entry point function
   /// @param func the entry point
   /// @returns true if the entry point function was emitted
-  bool EmitEntryPointFunction(ast::Function* func);
+  bool EmitEntryPointFunction(const ast::Function* func);
   /// Handles an if statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitIf(ast::IfStatement* stmt);
+  bool EmitIf(const ast::IfStatement* stmt);
   /// Handles a literal
   /// @param out the output stream
   /// @param lit the literal to emit
   /// @returns true if the literal was successfully emitted
-  bool EmitLiteral(std::ostream& out, ast::Literal* lit);
+  bool EmitLiteral(std::ostream& out, const ast::Literal* lit);
   /// Handles a loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitLoop(ast::LoopStatement* stmt);
+  bool EmitLoop(const ast::LoopStatement* stmt);
   /// Handles a for loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitForLoop(ast::ForLoopStatement* stmt);
+  bool EmitForLoop(const ast::ForLoopStatement* stmt);
   /// Handles generating an identifier expression
   /// @param out the output of the expression stream
   /// @param expr the identifier expression
   /// @returns true if the identifeir was emitted
-  bool EmitIdentifier(std::ostream& out, ast::IdentifierExpression* expr);
+  bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
   /// Handles a member accessor expression
   /// @param out the output of the expression stream
   /// @param expr the member accessor expression
   /// @returns true if the member accessor was emitted
   bool EmitMemberAccessor(std::ostream& out,
-                          ast::MemberAccessorExpression* expr);
+                          const ast::MemberAccessorExpression* expr);
   /// Handles return statements
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitReturn(ast::ReturnStatement* stmt);
+  bool EmitReturn(const ast::ReturnStatement* stmt);
   /// Handles statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitStatement(ast::Statement* stmt);
+  bool EmitStatement(const ast::Statement* stmt);
   /// Handles generating a switch statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitSwitch(ast::SwitchStatement* stmt);
+  bool EmitSwitch(const ast::SwitchStatement* stmt);
   /// Handles generating type
   /// @param out the output stream
   /// @param type the type to generate
@@ -324,7 +326,7 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the expression was emitted
-  bool EmitUnaryOp(std::ostream& out, ast::UnaryOpExpression* expr);
+  bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
   /// Emits the zero value for the given type
   /// @param out the output stream
   /// @param type the type to emit the value for
@@ -333,7 +335,7 @@
   /// Handles generating a variable
   /// @param var the variable to generate
   /// @returns true if the variable was emitted
-  bool EmitVariable(ast::Variable* var);
+  bool EmitVariable(const ast::Variable* var);
   /// Handles generating a program scope constant variable
   /// @param var the variable to emit
   /// @returns true if the variable was emitted
@@ -398,7 +400,7 @@
   /// @returns true if the call expression is emitted
   template <typename F>
   bool CallIntrinsicHelper(std::ostream& out,
-                           ast::CallExpression* call,
+                           const ast::CallExpression* call,
                            const sem::Intrinsic* intrinsic,
                            F&& build);
 
diff --git a/src/writer/glsl/generator_impl_intrinsic_test.cc b/src/writer/glsl/generator_impl_intrinsic_test.cc
index 35333de..a499b4b 100644
--- a/src/writer/glsl/generator_impl_intrinsic_test.cc
+++ b/src/writer/glsl/generator_impl_intrinsic_test.cc
@@ -57,9 +57,9 @@
   return out;
 }
 
-ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
-                                  ParamType type,
-                                  ProgramBuilder* builder) {
+const ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
+                                        ParamType type,
+                                        ProgramBuilder* builder) {
   std::string name;
   std::ostringstream str(name);
   str << intrinsic;
diff --git a/src/writer/glsl/generator_impl_member_accessor_test.cc b/src/writer/glsl/generator_impl_member_accessor_test.cc
index a7c0925..b297bf3 100644
--- a/src/writer/glsl/generator_impl_member_accessor_test.cc
+++ b/src/writer/glsl/generator_impl_member_accessor_test.cc
@@ -25,63 +25,63 @@
 using ::testing::HasSubstr;
 
 using create_type_func_ptr =
-    ast::Type* (*)(const ProgramBuilder::TypesBuilder& ty);
+    const ast::Type* (*)(const ProgramBuilder::TypesBuilder& ty);
 
-inline ast::Type* ty_i32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_i32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.i32();
 }
-inline ast::Type* ty_u32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_u32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.u32();
 }
-inline ast::Type* ty_f32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_f32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.f32();
 }
 template <typename T>
-inline ast::Type* ty_vec2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec2<T>();
 }
 template <typename T>
-inline ast::Type* ty_vec3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec3<T>();
 }
 template <typename T>
-inline ast::Type* ty_vec4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x4<T>();
 }
 
diff --git a/src/writer/glsl/generator_impl_type_test.cc b/src/writer/glsl/generator_impl_type_test.cc
index 2481f6f..74a32c2 100644
--- a/src/writer/glsl/generator_impl_type_test.cc
+++ b/src/writer/glsl/generator_impl_type_test.cc
@@ -372,7 +372,7 @@
 TEST_P(GlslSampledTexturesTest, Emit) {
   auto params = GetParam();
 
-  ast::Type* datatype = nullptr;
+  const ast::Type* datatype = nullptr;
   switch (params.datatype) {
     case TextureDataType::F32:
       datatype = ty.f32();
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index b690e89..f0dee54 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -102,7 +102,7 @@
   RegisterAndSpace(char r, ast::VariableBindingPoint bp)
       : reg(r), binding_point(bp) {}
 
-  char const reg;
+  const char reg;
   ast::VariableBindingPoint const binding_point;
 };
 
@@ -306,8 +306,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitArrayAccessor(std::ostream& out,
-                                      ast::ArrayAccessorExpression* expr) {
+bool GeneratorImpl::EmitArrayAccessor(
+    std::ostream& out,
+    const ast::ArrayAccessorExpression* expr) {
   if (!EmitExpression(out, expr->array)) {
     return false;
   }
@@ -322,7 +323,7 @@
 }
 
 bool GeneratorImpl::EmitBitcast(std::ostream& out,
-                                ast::BitcastExpression* expr) {
+                                const ast::BitcastExpression* expr) {
   auto* type = TypeOf(expr);
   if (auto* vec = type->UnwrapRef()->As<sem::Vector>()) {
     type = vec->type();
@@ -347,7 +348,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitAssign(ast::AssignmentStatement* stmt) {
+bool GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
   if (auto* idx = stmt->lhs->As<ast::ArrayAccessorExpression>()) {
     if (auto* vec = TypeOf(idx->array)->UnwrapRef()->As<sem::Vector>()) {
       auto* rhs_sem = builder_.Sem().Get(idx->index);
@@ -369,7 +370,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(std::ostream& out,
+                               const ast::BinaryExpression* expr) {
   if (expr->op == ast::BinaryOp::kLogicalAnd ||
       expr->op == ast::BinaryOp::kLogicalOr) {
     auto name = UniqueIdentifier(kTempNamePrefix);
@@ -545,12 +547,13 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBreak(ast::BreakStatement*) {
+bool GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
   line() << "break;";
   return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(std::ostream& out,
+                             const ast::CallExpression* expr) {
   const auto& args = expr->args;
   auto* ident = expr->func;
   auto* call = builder_.Sem().Get(expr);
@@ -667,7 +670,7 @@
 
 bool GeneratorImpl::EmitUniformBufferAccess(
     std::ostream& out,
-    ast::CallExpression* expr,
+    const ast::CallExpression* expr,
     const transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
   const auto& args = expr->args;
   auto* offset_arg = builder_.Sem().Get(args[1]);
@@ -806,7 +809,7 @@
 
 bool GeneratorImpl::EmitStorageBufferAccess(
     std::ostream& out,
-    ast::CallExpression* expr,
+    const ast::CallExpression* expr,
     const transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
   const auto& args = expr->args;
 
@@ -940,7 +943,7 @@
 
 bool GeneratorImpl::EmitStorageAtomicCall(
     std::ostream& out,
-    ast::CallExpression* expr,
+    const ast::CallExpression* expr,
     const transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
   using Op = transform::DecomposeMemoryAccess::Intrinsic::Op;
 
@@ -1166,7 +1169,7 @@
 }
 
 bool GeneratorImpl::EmitWorkgroupAtomicCall(std::ostream& out,
-                                            ast::CallExpression* expr,
+                                            const ast::CallExpression* expr,
                                             const sem::Intrinsic* intrinsic) {
   std::string result = UniqueIdentifier("atomic_result");
 
@@ -1339,7 +1342,7 @@
 }
 
 bool GeneratorImpl::EmitSelectCall(std::ostream& out,
-                                   ast::CallExpression* expr) {
+                                   const ast::CallExpression* expr) {
   auto* expr_false = expr->args[0];
   auto* expr_true = expr->args[1];
   auto* expr_cond = expr->args[2];
@@ -1364,7 +1367,7 @@
 }
 
 bool GeneratorImpl::EmitModfCall(std::ostream& out,
-                                 ast::CallExpression* expr,
+                                 const ast::CallExpression* expr,
                                  const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1400,7 +1403,7 @@
 }
 
 bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
-                                  ast::CallExpression* expr,
+                                  const ast::CallExpression* expr,
                                   const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1436,7 +1439,7 @@
 }
 
 bool GeneratorImpl::EmitIsNormalCall(std::ostream& out,
-                                     ast::CallExpression* expr,
+                                     const ast::CallExpression* expr,
                                      const sem::Intrinsic* intrinsic) {
   // HLSL doesn't have a isNormal intrinsic, we need to emulate
   return CallIntrinsicHelper(
@@ -1464,7 +1467,7 @@
 }
 
 bool GeneratorImpl::EmitDataPackingCall(std::ostream& out,
-                                        ast::CallExpression* expr,
+                                        const ast::CallExpression* expr,
                                         const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1530,7 +1533,7 @@
 }
 
 bool GeneratorImpl::EmitDataUnpackingCall(std::ostream& out,
-                                          ast::CallExpression* expr,
+                                          const ast::CallExpression* expr,
                                           const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1616,7 +1619,7 @@
 }
 
 bool GeneratorImpl::EmitTextureCall(std::ostream& out,
-                                    ast::CallExpression* expr,
+                                    const ast::CallExpression* expr,
                                     const sem::Intrinsic* intrinsic) {
   using Usage = sem::ParameterUsage;
 
@@ -1877,7 +1880,7 @@
     return false;
   }
 
-  auto emit_vector_appended_with_i32_zero = [&](tint::ast::Expression* vector) {
+  auto emit_vector_appended_with_i32_zero = [&](const ast::Expression* vector) {
     auto* i32 = builder_.create<sem::I32>();
     auto* zero = builder_.Expr(0);
     auto* stmt = builder_.Sem().Get(vector)->Stmt();
@@ -1887,7 +1890,7 @@
     return EmitExpression(out, packed);
   };
 
-  auto emit_vector_appended_with_level = [&](tint::ast::Expression* vector) {
+  auto emit_vector_appended_with_level = [&](const ast::Expression* vector) {
     if (auto* level = arg(Usage::kLevel)) {
       auto* packed = AppendVector(&builder_, vector, level);
       return EmitExpression(out, packed);
@@ -2056,7 +2059,7 @@
   return "";
 }
 
-bool GeneratorImpl::EmitCase(ast::SwitchStatement* s, size_t case_idx) {
+bool GeneratorImpl::EmitCase(const ast::SwitchStatement* s, size_t case_idx) {
   auto* stmt = s->body[case_idx];
   if (stmt->IsDefault()) {
     line() << "default: {";
@@ -2105,7 +2108,7 @@
 }
 
 bool GeneratorImpl::EmitConstructor(std::ostream& out,
-                                    ast::ConstructorExpression* expr) {
+                                    const ast::ConstructorExpression* expr) {
   if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
     return EmitScalarConstructor(out, scalar);
   }
@@ -2114,12 +2117,13 @@
 
 bool GeneratorImpl::EmitScalarConstructor(
     std::ostream& out,
-    ast::ScalarConstructorExpression* expr) {
+    const ast::ScalarConstructorExpression* expr) {
   return EmitLiteral(out, expr->literal);
 }
 
-bool GeneratorImpl::EmitTypeConstructor(std::ostream& out,
-                                        ast::TypeConstructorExpression* expr) {
+bool GeneratorImpl::EmitTypeConstructor(
+    std::ostream& out,
+    const ast::TypeConstructorExpression* expr) {
   auto* type = TypeOf(expr)->UnwrapRef();
 
   // If the type constructor is empty then we need to construct with the zero
@@ -2174,7 +2178,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitContinue(ast::ContinueStatement*) {
+bool GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
   if (!emit_continuing_()) {
     return false;
   }
@@ -2182,14 +2186,15 @@
   return true;
 }
 
-bool GeneratorImpl::EmitDiscard(ast::DiscardStatement*) {
+bool GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
   // TODO(dsinclair): Verify this is correct when the discard semantics are
   // defined for WGSL (https://github.com/gpuweb/gpuweb/issues/361)
   line() << "discard;";
   return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(std::ostream& out,
+                                   const ast::Expression* expr) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     return EmitArrayAccessor(out, a);
   }
@@ -2222,12 +2227,12 @@
 }
 
 bool GeneratorImpl::EmitIdentifier(std::ostream& out,
-                                   ast::IdentifierExpression* expr) {
+                                   const ast::IdentifierExpression* expr) {
   out << builder_.Symbols().NameFor(expr->symbol);
   return true;
 }
 
-bool GeneratorImpl::EmitIf(ast::IfStatement* stmt) {
+bool GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
   {
     auto out = line();
     out << "if (";
@@ -2274,7 +2279,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitFunction(ast::Function* func) {
+bool GeneratorImpl::EmitFunction(const ast::Function* func) {
   auto* sem = builder_.Sem().Get(func);
 
   if (ast::HasDecoration<ast::InternalDecoration>(func->decorations)) {
@@ -2349,7 +2354,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitGlobalVariable(ast::Variable* global) {
+bool GeneratorImpl::EmitGlobalVariable(const ast::Variable* global) {
   if (global->is_const) {
     return EmitProgramConstVariable(global);
   }
@@ -2563,7 +2568,7 @@
   return modifiers;
 }
 
-bool GeneratorImpl::EmitEntryPointFunction(ast::Function* func) {
+bool GeneratorImpl::EmitEntryPointFunction(const ast::Function* func) {
   auto* func_sem = builder_.Sem().Get(func);
 
   {
@@ -2643,7 +2648,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, ast::Literal* lit) {
+bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::Literal* lit) {
   if (auto* l = lit->As<ast::BoolLiteral>()) {
     out << (l->value ? "true" : "false");
   } else if (auto* fl = lit->As<ast::FloatLiteral>()) {
@@ -2718,7 +2723,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitLoop(ast::LoopStatement* stmt) {
+bool GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
   auto emit_continuing = [this, stmt]() {
     if (stmt->continuing && !stmt->continuing->Empty()) {
       if (!EmitBlock(stmt->continuing)) {
@@ -2744,7 +2749,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitForLoop(ast::ForLoopStatement* stmt) {
+bool GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
   // Nest a for loop with a new block. In HLSL the initializer scope is not
   // nested by the for-loop, so we may get variable redefinitions.
   line() << "{";
@@ -2853,8 +2858,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
-                                       ast::MemberAccessorExpression* expr) {
+bool GeneratorImpl::EmitMemberAccessor(
+    std::ostream& out,
+    const ast::MemberAccessorExpression* expr) {
   if (!EmitExpression(out, expr->structure)) {
     return false;
   }
@@ -2870,7 +2876,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitReturn(ast::ReturnStatement* stmt) {
+bool GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
   if (stmt->value) {
     auto out = line();
     out << "return ";
@@ -2884,7 +2890,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitStatement(ast::Statement* stmt) {
+bool GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return EmitAssign(a);
   }
@@ -2940,7 +2946,7 @@
   return false;
 }
 
-bool GeneratorImpl::EmitSwitch(ast::SwitchStatement* stmt) {
+bool GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
   {  // switch(expr) {
     auto out = line();
     out << "switch(";
@@ -3252,7 +3258,7 @@
 }
 
 bool GeneratorImpl::EmitUnaryOp(std::ostream& out,
-                                ast::UnaryOpExpression* expr) {
+                                const ast::UnaryOpExpression* expr) {
   switch (expr->op) {
     case ast::UnaryOp::kIndirection:
     case ast::UnaryOp::kAddressOf:
@@ -3278,7 +3284,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitVariable(ast::Variable* var) {
+bool GeneratorImpl::EmitVariable(const ast::Variable* var) {
   auto* sem = builder_.Sem().Get(var);
   auto* type = sem->Type()->UnwrapRef();
 
@@ -3374,7 +3380,7 @@
 
 template <typename F>
 bool GeneratorImpl::CallIntrinsicHelper(std::ostream& out,
-                                        ast::CallExpression* call,
+                                        const ast::CallExpression* call,
                                         const sem::Intrinsic* intrinsic,
                                         F&& build) {
   // Generate the helper function if it hasn't been created already
diff --git a/src/writer/hlsl/generator_impl.h b/src/writer/hlsl/generator_impl.h
index 5717d71..57d9cb8 100644
--- a/src/writer/hlsl/generator_impl.h
+++ b/src/writer/hlsl/generator_impl.h
@@ -79,21 +79,22 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the array accessor was emitted
-  bool EmitArrayAccessor(std::ostream& out, ast::ArrayAccessorExpression* expr);
+  bool EmitArrayAccessor(std::ostream& out,
+                         const ast::ArrayAccessorExpression* expr);
   /// Handles an assignment statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitAssign(ast::AssignmentStatement* stmt);
+  bool EmitAssign(const ast::AssignmentStatement* stmt);
   /// Handles generating a binary expression
   /// @param out the output of the expression stream
   /// @param expr the binary expression
   /// @returns true if the expression was emitted, false otherwise
-  bool EmitBinary(std::ostream& out, ast::BinaryExpression* expr);
+  bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
   /// Handles generating a bitcast expression
   /// @param out the output of the expression stream
   /// @param expr the as expression
   /// @returns true if the bitcast was emitted
-  bool EmitBitcast(std::ostream& out, ast::BitcastExpression* expr);
+  bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
   /// Emits a list of statements
   /// @param stmts the statement list
   /// @returns true if the statements were emitted successfully
@@ -109,12 +110,12 @@
   /// Handles a break statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitBreak(ast::BreakStatement* stmt);
+  bool EmitBreak(const ast::BreakStatement* stmt);
   /// Handles generating a call expression
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles generating a call expression to a
   /// transform::DecomposeMemoryAccess::Intrinsic for a uniform buffer
   /// @param out the output of the expression stream
@@ -123,7 +124,7 @@
   /// @returns true if the call expression is emitted
   bool EmitUniformBufferAccess(
       std::ostream& out,
-      ast::CallExpression* expr,
+      const ast::CallExpression* expr,
       const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
   /// Handles generating a call expression to a
   /// transform::DecomposeMemoryAccess::Intrinsic for a storage buffer
@@ -133,7 +134,7 @@
   /// @returns true if the call expression is emitted
   bool EmitStorageBufferAccess(
       std::ostream& out,
-      ast::CallExpression* expr,
+      const ast::CallExpression* expr,
       const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
   /// Handles generating a barrier intrinsic call
   /// @param out the output of the expression stream
@@ -147,7 +148,7 @@
   /// @returns true if the call expression is emitted
   bool EmitStorageAtomicCall(
       std::ostream& out,
-      ast::CallExpression* expr,
+      const ast::CallExpression* expr,
       const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
   /// Handles generating an atomic intrinsic call for a workgroup variable
   /// @param out the output of the expression stream
@@ -155,7 +156,7 @@
   /// @param intrinsic the semantic information for the atomic intrinsic
   /// @returns true if the call expression is emitted
   bool EmitWorkgroupAtomicCall(std::ostream& out,
-                               ast::CallExpression* expr,
+                               const ast::CallExpression* expr,
                                const sem::Intrinsic* intrinsic);
   /// Handles generating a call to a texture function (`textureSample`,
   /// `textureSampleGrad`, etc)
@@ -164,20 +165,20 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitTextureCall(std::ostream& out,
-                       ast::CallExpression* expr,
+                       const ast::CallExpression* expr,
                        const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `select()` intrinsic
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitSelectCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitSelectCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles generating a call to the `modf()` intrinsic
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitModfCall(std::ostream& out,
-                    ast::CallExpression* expr,
+                    const ast::CallExpression* expr,
                     const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `frexp()` intrinsic
   /// @param out the output of the expression stream
@@ -185,7 +186,7 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitFrexpCall(std::ostream& out,
-                     ast::CallExpression* expr,
+                     const ast::CallExpression* expr,
                      const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `isNormal()` intrinsic
   /// @param out the output of the expression stream
@@ -193,7 +194,7 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitIsNormalCall(std::ostream& out,
-                        ast::CallExpression* expr,
+                        const ast::CallExpression* expr,
                         const sem::Intrinsic* intrinsic);
   /// Handles generating a call to data packing intrinsic
   /// @param out the output of the expression stream
@@ -201,7 +202,7 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitDataPackingCall(std::ostream& out,
-                           ast::CallExpression* expr,
+                           const ast::CallExpression* expr,
                            const sem::Intrinsic* intrinsic);
   /// Handles generating a call to data unpacking intrinsic
   /// @param out the output of the expression stream
@@ -209,52 +210,53 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitDataUnpackingCall(std::ostream& out,
-                             ast::CallExpression* expr,
+                             const ast::CallExpression* expr,
                              const sem::Intrinsic* intrinsic);
   /// Handles a case statement
   /// @param s the switch statement
   /// @param case_idx the index of the switch case in the switch statement
   /// @returns true if the statement was emitted successfully
-  bool EmitCase(ast::SwitchStatement* s, size_t case_idx);
+  bool EmitCase(const ast::SwitchStatement* s, size_t case_idx);
   /// Handles generating constructor expressions
   /// @param out the output of the expression stream
   /// @param expr the constructor expression
   /// @returns true if the expression was emitted
-  bool EmitConstructor(std::ostream& out, ast::ConstructorExpression* expr);
+  bool EmitConstructor(std::ostream& out,
+                       const ast::ConstructorExpression* expr);
   /// Handles generating a discard statement
   /// @param stmt the discard statement
   /// @returns true if the statement was successfully emitted
-  bool EmitDiscard(ast::DiscardStatement* stmt);
+  bool EmitDiscard(const ast::DiscardStatement* stmt);
   /// Handles generating a scalar constructor
   /// @param out the output of the expression stream
   /// @param expr the scalar constructor expression
   /// @returns true if the scalar constructor is emitted
   bool EmitScalarConstructor(std::ostream& out,
-                             ast::ScalarConstructorExpression* expr);
+                             const ast::ScalarConstructorExpression* expr);
   /// Handles emitting a type constructor
   /// @param out the output of the expression stream
   /// @param expr the type constructor expression
   /// @returns true if the constructor is emitted
   bool EmitTypeConstructor(std::ostream& out,
-                           ast::TypeConstructorExpression* expr);
+                           const ast::TypeConstructorExpression* expr);
   /// Handles a continue statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitContinue(ast::ContinueStatement* stmt);
+  bool EmitContinue(const ast::ContinueStatement* stmt);
   /// Handles generate an Expression
   /// @param out the output of the expression stream
   /// @param expr the expression
   /// @returns true if the expression was emitted
-  bool EmitExpression(std::ostream& out, ast::Expression* expr);
+  bool EmitExpression(std::ostream& out, const ast::Expression* expr);
   /// Handles generating a function
   /// @param func the function to generate
   /// @returns true if the function was emitted
-  bool EmitFunction(ast::Function* func);
+  bool EmitFunction(const ast::Function* func);
 
   /// Handles emitting a global variable
   /// @param global the global variable
   /// @returns true on success
-  bool EmitGlobalVariable(ast::Variable* global);
+  bool EmitGlobalVariable(const ast::Variable* global);
 
   /// Handles emitting a global variable with the uniform storage class
   /// @param var the global variable
@@ -284,47 +286,47 @@
   /// Handles emitting the entry point function
   /// @param func the entry point
   /// @returns true if the entry point function was emitted
-  bool EmitEntryPointFunction(ast::Function* func);
+  bool EmitEntryPointFunction(const ast::Function* func);
   /// Handles an if statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitIf(ast::IfStatement* stmt);
+  bool EmitIf(const ast::IfStatement* stmt);
   /// Handles a literal
   /// @param out the output stream
   /// @param lit the literal to emit
   /// @returns true if the literal was successfully emitted
-  bool EmitLiteral(std::ostream& out, ast::Literal* lit);
+  bool EmitLiteral(std::ostream& out, const ast::Literal* lit);
   /// Handles a loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitLoop(ast::LoopStatement* stmt);
+  bool EmitLoop(const ast::LoopStatement* stmt);
   /// Handles a for loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitForLoop(ast::ForLoopStatement* stmt);
+  bool EmitForLoop(const ast::ForLoopStatement* stmt);
   /// Handles generating an identifier expression
   /// @param out the output of the expression stream
   /// @param expr the identifier expression
   /// @returns true if the identifeir was emitted
-  bool EmitIdentifier(std::ostream& out, ast::IdentifierExpression* expr);
+  bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
   /// Handles a member accessor expression
   /// @param out the output of the expression stream
   /// @param expr the member accessor expression
   /// @returns true if the member accessor was emitted
   bool EmitMemberAccessor(std::ostream& out,
-                          ast::MemberAccessorExpression* expr);
+                          const ast::MemberAccessorExpression* expr);
   /// Handles return statements
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitReturn(ast::ReturnStatement* stmt);
+  bool EmitReturn(const ast::ReturnStatement* stmt);
   /// Handles statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitStatement(ast::Statement* stmt);
+  bool EmitStatement(const ast::Statement* stmt);
   /// Handles generating a switch statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitSwitch(ast::SwitchStatement* stmt);
+  bool EmitSwitch(const ast::SwitchStatement* stmt);
   /// Handles generating type
   /// @param out the output stream
   /// @param type the type to generate
@@ -361,7 +363,7 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the expression was emitted
-  bool EmitUnaryOp(std::ostream& out, ast::UnaryOpExpression* expr);
+  bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
   /// Emits the zero value for the given type
   /// @param out the output stream
   /// @param type the type to emit the value for
@@ -370,7 +372,7 @@
   /// Handles generating a variable
   /// @param var the variable to generate
   /// @returns true if the variable was emitted
-  bool EmitVariable(ast::Variable* var);
+  bool EmitVariable(const ast::Variable* var);
   /// Handles generating a program scope constant variable
   /// @param var the variable to emit
   /// @returns true if the variable was emitted
@@ -441,7 +443,7 @@
   /// @returns true if the call expression is emitted
   template <typename F>
   bool CallIntrinsicHelper(std::ostream& out,
-                           ast::CallExpression* call,
+                           const ast::CallExpression* call,
                            const sem::Intrinsic* intrinsic,
                            F&& build);
 
diff --git a/src/writer/hlsl/generator_impl_intrinsic_test.cc b/src/writer/hlsl/generator_impl_intrinsic_test.cc
index 1603561..2771a92 100644
--- a/src/writer/hlsl/generator_impl_intrinsic_test.cc
+++ b/src/writer/hlsl/generator_impl_intrinsic_test.cc
@@ -57,9 +57,9 @@
   return out;
 }
 
-ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
-                                  ParamType type,
-                                  ProgramBuilder* builder) {
+const ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
+                                        ParamType type,
+                                        ProgramBuilder* builder) {
   std::string name;
   std::ostringstream str(name);
   str << intrinsic;
diff --git a/src/writer/hlsl/generator_impl_member_accessor_test.cc b/src/writer/hlsl/generator_impl_member_accessor_test.cc
index 40b73d4..ccefe54 100644
--- a/src/writer/hlsl/generator_impl_member_accessor_test.cc
+++ b/src/writer/hlsl/generator_impl_member_accessor_test.cc
@@ -25,63 +25,63 @@
 using ::testing::HasSubstr;
 
 using create_type_func_ptr =
-    ast::Type* (*)(const ProgramBuilder::TypesBuilder& ty);
+    const ast::Type* (*)(const ProgramBuilder::TypesBuilder& ty);
 
-inline ast::Type* ty_i32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_i32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.i32();
 }
-inline ast::Type* ty_u32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_u32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.u32();
 }
-inline ast::Type* ty_f32(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_f32(const ProgramBuilder::TypesBuilder& ty) {
   return ty.f32();
 }
 template <typename T>
-inline ast::Type* ty_vec2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec2<T>();
 }
 template <typename T>
-inline ast::Type* ty_vec3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec3<T>();
 }
 template <typename T>
-inline ast::Type* ty_vec4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_vec4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.vec4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat2x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat2x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat2x4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat3x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat3x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat3x4<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x2(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x2(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x2<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x3(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x3(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x3<T>();
 }
 template <typename T>
-inline ast::Type* ty_mat4x4(const ProgramBuilder::TypesBuilder& ty) {
+inline const ast::Type* ty_mat4x4(const ProgramBuilder::TypesBuilder& ty) {
   return ty.mat4x4<T>();
 }
 
diff --git a/src/writer/hlsl/generator_impl_type_test.cc b/src/writer/hlsl/generator_impl_type_test.cc
index fe4965b..6eb6a19 100644
--- a/src/writer/hlsl/generator_impl_type_test.cc
+++ b/src/writer/hlsl/generator_impl_type_test.cc
@@ -394,7 +394,7 @@
 TEST_P(HlslSampledTexturesTest, Emit) {
   auto params = GetParam();
 
-  ast::Type* datatype = nullptr;
+  const ast::Type* datatype = nullptr;
   switch (params.datatype) {
     case TextureDataType::F32:
       datatype = ty.f32();
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index 510b649..2c90761 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -242,8 +242,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitArrayAccessor(std::ostream& out,
-                                      ast::ArrayAccessorExpression* expr) {
+bool GeneratorImpl::EmitArrayAccessor(
+    std::ostream& out,
+    const ast::ArrayAccessorExpression* expr) {
   bool paren_lhs =
       !expr->array
            ->IsAnyOf<ast::ArrayAccessorExpression, ast::CallExpression,
@@ -271,7 +272,7 @@
 }
 
 bool GeneratorImpl::EmitBitcast(std::ostream& out,
-                                ast::BitcastExpression* expr) {
+                                const ast::BitcastExpression* expr) {
   out << "as_type<";
   if (!EmitType(out, TypeOf(expr)->UnwrapRef(), "")) {
     return false;
@@ -286,7 +287,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitAssign(ast::AssignmentStatement* stmt) {
+bool GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
   auto out = line();
 
   if (!EmitExpression(out, stmt->lhs)) {
@@ -304,7 +305,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(std::ostream& out,
+                               const ast::BinaryExpression* expr) {
   auto emit_op = [&] {
     out << " ";
 
@@ -491,12 +493,13 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBreak(ast::BreakStatement*) {
+bool GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
   line() << "break;";
   return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(std::ostream& out,
+                             const ast::CallExpression* expr) {
   auto* ident = expr->func;
   auto* call = program_->Sem().Get(expr);
   if (auto* intrinsic = call->Target()->As<sem::Intrinsic>()) {
@@ -551,7 +554,7 @@
 }
 
 bool GeneratorImpl::EmitIntrinsicCall(std::ostream& out,
-                                      ast::CallExpression* expr,
+                                      const ast::CallExpression* expr,
                                       const sem::Intrinsic* intrinsic) {
   if (intrinsic->IsAtomic()) {
     return EmitAtomicCall(out, expr, intrinsic);
@@ -653,7 +656,7 @@
 }
 
 bool GeneratorImpl::EmitAtomicCall(std::ostream& out,
-                                   ast::CallExpression* expr,
+                                   const ast::CallExpression* expr,
                                    const sem::Intrinsic* intrinsic) {
   auto call = [&](const std::string& name, bool append_memory_order_relaxed) {
     out << name;
@@ -754,7 +757,7 @@
 }
 
 bool GeneratorImpl::EmitTextureCall(std::ostream& out,
-                                    ast::CallExpression* expr,
+                                    const ast::CallExpression* expr,
                                     const sem::Intrinsic* intrinsic) {
   using Usage = sem::ParameterUsage;
 
@@ -1026,7 +1029,7 @@
 }
 
 bool GeneratorImpl::EmitModfCall(std::ostream& out,
-                                 ast::CallExpression* expr,
+                                 const ast::CallExpression* expr,
                                  const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1054,7 +1057,7 @@
 }
 
 bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
-                                  ast::CallExpression* expr,
+                                  const ast::CallExpression* expr,
                                   const sem::Intrinsic* intrinsic) {
   return CallIntrinsicHelper(
       out, expr, intrinsic,
@@ -1226,7 +1229,7 @@
   return out;
 }
 
-bool GeneratorImpl::EmitCase(ast::CaseStatement* stmt) {
+bool GeneratorImpl::EmitCase(const ast::CaseStatement* stmt) {
   if (stmt->IsDefault()) {
     line() << "default: {";
   } else {
@@ -1263,14 +1266,14 @@
 }
 
 bool GeneratorImpl::EmitConstructor(std::ostream& out,
-                                    ast::ConstructorExpression* expr) {
+                                    const ast::ConstructorExpression* expr) {
   if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
     return EmitScalarConstructor(out, scalar);
   }
   return EmitTypeConstructor(out, expr->As<ast::TypeConstructorExpression>());
 }
 
-bool GeneratorImpl::EmitContinue(ast::ContinueStatement*) {
+bool GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
   if (!emit_continuing_()) {
     return false;
   }
@@ -1279,8 +1282,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitTypeConstructor(std::ostream& out,
-                                        ast::TypeConstructorExpression* expr) {
+bool GeneratorImpl::EmitTypeConstructor(
+    std::ostream& out,
+    const ast::TypeConstructorExpression* expr) {
   auto* type = TypeOf(expr)->UnwrapRef();
 
   if (type->IsAnyOf<sem::Array, sem::Struct>()) {
@@ -1359,11 +1363,11 @@
 
 bool GeneratorImpl::EmitScalarConstructor(
     std::ostream& out,
-    ast::ScalarConstructorExpression* expr) {
+    const ast::ScalarConstructorExpression* expr) {
   return EmitLiteral(out, expr->literal);
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, ast::Literal* lit) {
+bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::Literal* lit) {
   if (auto* l = lit->As<ast::BoolLiteral>()) {
     out << (l->value ? "true" : "false");
   } else if (auto* fl = lit->As<ast::FloatLiteral>()) {
@@ -1395,7 +1399,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(std::ostream& out,
+                                   const ast::Expression* expr) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     return EmitArrayAccessor(out, a);
   }
@@ -1444,7 +1449,7 @@
   return;
 }
 
-bool GeneratorImpl::EmitFunction(ast::Function* func) {
+bool GeneratorImpl::EmitFunction(const ast::Function* func) {
   auto* func_sem = program_->Sem().Get(func);
 
   {
@@ -1584,7 +1589,7 @@
   return attr;
 }
 
-bool GeneratorImpl::EmitEntryPointFunction(ast::Function* func) {
+bool GeneratorImpl::EmitEntryPointFunction(const ast::Function* func) {
   auto* func_sem = program_->Sem().Get(func);
   auto func_name = program_->Symbols().NameFor(func->symbol);
 
@@ -1750,12 +1755,12 @@
 }
 
 bool GeneratorImpl::EmitIdentifier(std::ostream& out,
-                                   ast::IdentifierExpression* expr) {
+                                   const ast::IdentifierExpression* expr) {
   out << program_->Symbols().NameFor(expr->symbol);
   return true;
 }
 
-bool GeneratorImpl::EmitLoop(ast::LoopStatement* stmt) {
+bool GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
   auto emit_continuing = [this, stmt]() {
     if (stmt->continuing && !stmt->continuing->Empty()) {
       if (!EmitBlock(stmt->continuing)) {
@@ -1781,7 +1786,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitForLoop(ast::ForLoopStatement* stmt) {
+bool GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
   TextBuffer init_buf;
   if (auto* init = stmt->initializer) {
     TINT_SCOPED_ASSIGNMENT(current_buffer_, &init_buf);
@@ -1891,14 +1896,14 @@
   return true;
 }
 
-bool GeneratorImpl::EmitDiscard(ast::DiscardStatement*) {
+bool GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
   // TODO(dsinclair): Verify this is correct when the discard semantics are
   // defined for WGSL (https://github.com/gpuweb/gpuweb/issues/361)
   line() << "discard_fragment();";
   return true;
 }
 
-bool GeneratorImpl::EmitIf(ast::IfStatement* stmt) {
+bool GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
   {
     auto out = line();
     out << "if (";
@@ -1945,8 +1950,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
-                                       ast::MemberAccessorExpression* expr) {
+bool GeneratorImpl::EmitMemberAccessor(
+    std::ostream& out,
+    const ast::MemberAccessorExpression* expr) {
   bool paren_lhs =
       !expr->structure
            ->IsAnyOf<ast::ArrayAccessorExpression, ast::CallExpression,
@@ -1974,7 +1980,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitReturn(ast::ReturnStatement* stmt) {
+bool GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
   auto out = line();
   out << "return";
   if (stmt->value) {
@@ -1999,7 +2005,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitStatement(ast::Statement* stmt) {
+bool GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return EmitAssign(a);
   }
@@ -2067,7 +2073,7 @@
   return EmitStatements(stmts);
 }
 
-bool GeneratorImpl::EmitSwitch(ast::SwitchStatement* stmt) {
+bool GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
   {
     auto out = line();
     out << "switch(";
@@ -2510,7 +2516,7 @@
 }
 
 bool GeneratorImpl::EmitUnaryOp(std::ostream& out,
-                                ast::UnaryOpExpression* expr) {
+                                const ast::UnaryOpExpression* expr) {
   // Handle `-e` when `e` is signed, so that we ensure that if `e` is the
   // largest negative value, it returns `e`.
   auto* expr_type = TypeOf(expr->expr)->UnwrapRef();
@@ -2760,7 +2766,7 @@
 
 template <typename F>
 bool GeneratorImpl::CallIntrinsicHelper(std::ostream& out,
-                                        ast::CallExpression* call,
+                                        const ast::CallExpression* call,
                                         const sem::Intrinsic* intrinsic,
                                         F&& build) {
   // Generate the helper function if it hasn't been created already
diff --git a/src/writer/msl/generator_impl.h b/src/writer/msl/generator_impl.h
index ee5b2bf..6009c92 100644
--- a/src/writer/msl/generator_impl.h
+++ b/src/writer/msl/generator_impl.h
@@ -99,21 +99,22 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the array accessor was emitted
-  bool EmitArrayAccessor(std::ostream& out, ast::ArrayAccessorExpression* expr);
+  bool EmitArrayAccessor(std::ostream& out,
+                         const ast::ArrayAccessorExpression* expr);
   /// Handles an assignment statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitAssign(ast::AssignmentStatement* stmt);
+  bool EmitAssign(const ast::AssignmentStatement* stmt);
   /// Handles generating a binary expression
   /// @param out the output of the expression stream
   /// @param expr the binary expression
   /// @returns true if the expression was emitted, false otherwise
-  bool EmitBinary(std::ostream& out, ast::BinaryExpression* expr);
+  bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
   /// Handles generating a bitcast expression
   /// @param out the output of the expression stream
   /// @param expr the bitcast expression
   /// @returns true if the bitcast was emitted
-  bool EmitBitcast(std::ostream& out, ast::BitcastExpression* expr);
+  bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
   /// Handles a block statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
@@ -121,19 +122,19 @@
   /// Handles a break statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitBreak(ast::BreakStatement* stmt);
+  bool EmitBreak(const ast::BreakStatement* stmt);
   /// Handles generating a call expression
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles generating an intrinsic call expression
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @param intrinsic the intrinsic being called
   /// @returns true if the call expression is emitted
   bool EmitIntrinsicCall(std::ostream& out,
-                         ast::CallExpression* expr,
+                         const ast::CallExpression* expr,
                          const sem::Intrinsic* intrinsic);
   /// Handles generating a call to an atomic function (`atomicAdd`,
   /// `atomicMax`, etc)
@@ -142,7 +143,7 @@
   /// @param intrinsic the semantic information for the atomic intrinsic
   /// @returns true if the call expression is emitted
   bool EmitAtomicCall(std::ostream& out,
-                      ast::CallExpression* expr,
+                      const ast::CallExpression* expr,
                       const sem::Intrinsic* intrinsic);
   /// Handles generating a call to a texture function (`textureSample`,
   /// `textureSampleGrad`, etc)
@@ -151,7 +152,7 @@
   /// @param intrinsic the semantic information for the texture intrinsic
   /// @returns true if the call expression is emitted
   bool EmitTextureCall(std::ostream& out,
-                       ast::CallExpression* expr,
+                       const ast::CallExpression* expr,
                        const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `modf()` intrinsic
   /// @param out the output of the expression stream
@@ -159,7 +160,7 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitModfCall(std::ostream& out,
-                    ast::CallExpression* expr,
+                    const ast::CallExpression* expr,
                     const sem::Intrinsic* intrinsic);
   /// Handles generating a call to the `frexp()` intrinsic
   /// @param out the output of the expression stream
@@ -167,76 +168,77 @@
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns true if the call expression is emitted
   bool EmitFrexpCall(std::ostream& out,
-                     ast::CallExpression* expr,
+                     const ast::CallExpression* expr,
                      const sem::Intrinsic* intrinsic);
   /// Handles a case statement
   /// @param stmt the statement
   /// @returns true if the statement was emitted successfully
-  bool EmitCase(ast::CaseStatement* stmt);
+  bool EmitCase(const ast::CaseStatement* stmt);
   /// Handles generating constructor expressions
   /// @param out the output of the expression stream
   /// @param expr the constructor expression
   /// @returns true if the expression was emitted
-  bool EmitConstructor(std::ostream& out, ast::ConstructorExpression* expr);
+  bool EmitConstructor(std::ostream& out,
+                       const ast::ConstructorExpression* expr);
   /// Handles a continue statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitContinue(ast::ContinueStatement* stmt);
+  bool EmitContinue(const ast::ContinueStatement* stmt);
   /// Handles generating a discard statement
   /// @param stmt the discard statement
   /// @returns true if the statement was successfully emitted
-  bool EmitDiscard(ast::DiscardStatement* stmt);
+  bool EmitDiscard(const ast::DiscardStatement* stmt);
   /// Handles emitting the entry point function
   /// @param func the entry point function
   /// @returns true if the entry point function was emitted
-  bool EmitEntryPointFunction(ast::Function* func);
+  bool EmitEntryPointFunction(const ast::Function* func);
   /// Handles generate an Expression
   /// @param out the output of the expression stream
   /// @param expr the expression
   /// @returns true if the expression was emitted
-  bool EmitExpression(std::ostream& out, ast::Expression* expr);
+  bool EmitExpression(std::ostream& out, const ast::Expression* expr);
   /// Handles generating a function
   /// @param func the function to generate
   /// @returns true if the function was emitted
-  bool EmitFunction(ast::Function* func);
+  bool EmitFunction(const ast::Function* func);
   /// Handles generating an identifier expression
   /// @param out the output of the expression stream
   /// @param expr the identifier expression
   /// @returns true if the identifier was emitted
-  bool EmitIdentifier(std::ostream& out, ast::IdentifierExpression* expr);
+  bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
   /// Handles an if statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitIf(ast::IfStatement* stmt);
+  bool EmitIf(const ast::IfStatement* stmt);
   /// Handles a literal
   /// @param out the output of the expression stream
   /// @param lit the literal to emit
   /// @returns true if the literal was successfully emitted
-  bool EmitLiteral(std::ostream& out, ast::Literal* lit);
+  bool EmitLiteral(std::ostream& out, const ast::Literal* lit);
   /// Handles a loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitLoop(ast::LoopStatement* stmt);
+  bool EmitLoop(const ast::LoopStatement* stmt);
   /// Handles a for loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitForLoop(ast::ForLoopStatement* stmt);
+  bool EmitForLoop(const ast::ForLoopStatement* stmt);
   /// Handles a member accessor expression
   /// @param out the output of the expression stream
   /// @param expr the member accessor expression
   /// @returns true if the member accessor was emitted
   bool EmitMemberAccessor(std::ostream& out,
-                          ast::MemberAccessorExpression* expr);
+                          const ast::MemberAccessorExpression* expr);
   /// Handles return statements
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitReturn(ast::ReturnStatement* stmt);
+  bool EmitReturn(const ast::ReturnStatement* stmt);
   /// Handles generating a scalar constructor
   /// @param out the output of the expression stream
   /// @param expr the scalar constructor expression
   /// @returns true if the scalar constructor is emitted
   bool EmitScalarConstructor(std::ostream& out,
-                             ast::ScalarConstructorExpression* expr);
+                             const ast::ScalarConstructorExpression* expr);
   /// Handles emitting a pipeline stage name
   /// @param out the output of the expression stream
   /// @param stage the stage to emit
@@ -244,7 +246,7 @@
   /// Handles statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitStatement(ast::Statement* stmt);
+  bool EmitStatement(const ast::Statement* stmt);
   /// Emits a list of statements
   /// @param stmts the statement list
   /// @returns true if the statements were emitted successfully
@@ -256,7 +258,7 @@
   /// Handles generating a switch statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitSwitch(ast::SwitchStatement* stmt);
+  bool EmitSwitch(const ast::SwitchStatement* stmt);
   /// Handles generating a type
   /// @param out the output of the type stream
   /// @param type the type to generate
@@ -300,12 +302,12 @@
   /// @param expr the type constructor expression
   /// @returns true if the constructor is emitted
   bool EmitTypeConstructor(std::ostream& out,
-                           ast::TypeConstructorExpression* expr);
+                           const ast::TypeConstructorExpression* expr);
   /// Handles a unary op expression
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the expression was emitted
-  bool EmitUnaryOp(std::ostream& out, ast::UnaryOpExpression* expr);
+  bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
   /// Handles generating a variable
   /// @param var the variable to generate
   /// @returns true if the variable was emitted
@@ -360,7 +362,7 @@
   /// @returns true if the call expression is emitted
   template <typename F>
   bool CallIntrinsicHelper(std::ostream& out,
-                           ast::CallExpression* call,
+                           const ast::CallExpression* call,
                            const sem::Intrinsic* intrinsic,
                            F&& build);
 
diff --git a/src/writer/msl/generator_impl_binary_test.cc b/src/writer/msl/generator_impl_binary_test.cc
index 4b460dc..330d641 100644
--- a/src/writer/msl/generator_impl_binary_test.cc
+++ b/src/writer/msl/generator_impl_binary_test.cc
@@ -34,8 +34,8 @@
   auto type = [&] {
     return ((params.op == ast::BinaryOp::kLogicalAnd) ||
             (params.op == ast::BinaryOp::kLogicalOr))
-               ? static_cast<ast::Type*>(ty.bool_())
-               : static_cast<ast::Type*>(ty.u32());
+               ? static_cast<const ast::Type*>(ty.bool_())
+               : static_cast<const ast::Type*>(ty.u32());
   };
 
   auto* left = Var("left", type());
@@ -82,7 +82,7 @@
   auto* a_type = ty.i32();
   auto* b_type = (params.op == ast::BinaryOp::kShiftLeft ||
                   params.op == ast::BinaryOp::kShiftRight)
-                     ? static_cast<ast::Type*>(ty.u32())
+                     ? static_cast<const ast::Type*>(ty.u32())
                      : ty.i32();
 
   auto* a = Var("a", a_type);
@@ -117,7 +117,7 @@
   auto* a_type = ty.i32();
   auto* b_type = (params.op == ast::BinaryOp::kShiftLeft ||
                   params.op == ast::BinaryOp::kShiftRight)
-                     ? static_cast<ast::Type*>(ty.u32())
+                     ? static_cast<const ast::Type*>(ty.u32())
                      : ty.i32();
 
   auto* a = Var("a", a_type);
diff --git a/src/writer/msl/generator_impl_intrinsic_test.cc b/src/writer/msl/generator_impl_intrinsic_test.cc
index eba80cc..5b681b3 100644
--- a/src/writer/msl/generator_impl_intrinsic_test.cc
+++ b/src/writer/msl/generator_impl_intrinsic_test.cc
@@ -53,9 +53,9 @@
   return out;
 }
 
-ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
-                                  ParamType type,
-                                  ProgramBuilder* builder) {
+const ast::CallExpression* GenerateCall(IntrinsicType intrinsic,
+                                        ParamType type,
+                                        ProgramBuilder* builder) {
   std::string name;
   std::ostringstream str(name);
   str << intrinsic;
diff --git a/src/writer/spirv/builder.cc b/src/writer/spirv/builder.cc
index f37ffd1..da3f887 100644
--- a/src/writer/spirv/builder.cc
+++ b/src/writer/spirv/builder.cc
@@ -387,7 +387,7 @@
   return true;
 }
 
-bool Builder::GenerateAssignStatement(ast::AssignmentStatement* assign) {
+bool Builder::GenerateAssignStatement(const ast::AssignmentStatement* assign) {
   auto lhs_id = GenerateExpression(assign->lhs);
   if (lhs_id == 0) {
     return false;
@@ -404,7 +404,7 @@
   return GenerateStore(lhs_id, rhs_id);
 }
 
-bool Builder::GenerateBreakStatement(ast::BreakStatement*) {
+bool Builder::GenerateBreakStatement(const ast::BreakStatement*) {
   if (merge_stack_.empty()) {
     error_ = "Attempted to break without a merge block";
     return false;
@@ -416,7 +416,7 @@
   return true;
 }
 
-bool Builder::GenerateContinueStatement(ast::ContinueStatement*) {
+bool Builder::GenerateContinueStatement(const ast::ContinueStatement*) {
   if (continue_stack_.empty()) {
     error_ = "Attempted to continue without a continue block";
     return false;
@@ -431,14 +431,14 @@
 // TODO(dsinclair): This is generating an OpKill but the semantics of kill
 // haven't been defined for WGSL yet. So, this may need to change.
 // https://github.com/gpuweb/gpuweb/issues/676
-bool Builder::GenerateDiscardStatement(ast::DiscardStatement*) {
+bool Builder::GenerateDiscardStatement(const ast::DiscardStatement*) {
   if (!push_function_inst(spv::Op::OpKill, {})) {
     return false;
   }
   return true;
 }
 
-bool Builder::GenerateEntryPoint(ast::Function* func, uint32_t id) {
+bool Builder::GenerateEntryPoint(const ast::Function* func, uint32_t id) {
   auto stage = pipeline_stage_to_execution_model(func->PipelineStage());
   if (stage == SpvExecutionModelMax) {
     error_ = "Unknown pipeline stage provided";
@@ -472,7 +472,7 @@
   return true;
 }
 
-bool Builder::GenerateExecutionModes(ast::Function* func, uint32_t id) {
+bool Builder::GenerateExecutionModes(const ast::Function* func, uint32_t id) {
   auto* func_sem = builder_.Sem().Get(func);
 
   // WGSL fragment shader origin is upper left
@@ -553,7 +553,7 @@
   return true;
 }
 
-uint32_t Builder::GenerateExpression(ast::Expression* expr) {
+uint32_t Builder::GenerateExpression(const ast::Expression* expr) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     return GenerateAccessorExpression(a);
   }
@@ -583,7 +583,7 @@
   return 0;
 }
 
-bool Builder::GenerateFunction(ast::Function* func_ast) {
+bool Builder::GenerateFunction(const ast::Function* func_ast) {
   auto* func = builder_.Sem().Get(func_ast);
 
   uint32_t func_type_id = GenerateFunctionTypeIfNeeded(func);
@@ -678,7 +678,7 @@
       });
 }
 
-bool Builder::GenerateFunctionVariable(ast::Variable* var) {
+bool Builder::GenerateFunctionVariable(const ast::Variable* var) {
   uint32_t init_id = 0;
   if (var->constructor) {
     init_id = GenerateExpression(var->constructor);
@@ -741,7 +741,7 @@
                             {Operand::Int(to), Operand::Int(from)});
 }
 
-bool Builder::GenerateGlobalVariable(ast::Variable* var) {
+bool Builder::GenerateGlobalVariable(const ast::Variable* var) {
   auto* sem = builder_.Sem().Get(var);
   auto* type = sem->Type()->UnwrapRef();
 
@@ -893,7 +893,7 @@
   return true;
 }
 
-bool Builder::GenerateArrayAccessor(ast::ArrayAccessorExpression* expr,
+bool Builder::GenerateArrayAccessor(const ast::ArrayAccessorExpression* expr,
                                     AccessorInfo* info) {
   auto idx_id = GenerateExpression(expr->index);
   if (idx_id == 0) {
@@ -962,7 +962,7 @@
   return false;
 }
 
-bool Builder::GenerateMemberAccessor(ast::MemberAccessorExpression* expr,
+bool Builder::GenerateMemberAccessor(const ast::MemberAccessorExpression* expr,
                                      AccessorInfo* info) {
   auto* expr_sem = builder_.Sem().Get(expr);
   auto* expr_type = expr_sem->Type();
@@ -1088,7 +1088,7 @@
   return false;
 }
 
-uint32_t Builder::GenerateAccessorExpression(ast::Expression* expr) {
+uint32_t Builder::GenerateAccessorExpression(const ast::Expression* expr) {
   if (!expr->IsAnyOf<ast::ArrayAccessorExpression,
                      ast::MemberAccessorExpression>()) {
     TINT_ICE(Writer, builder_.Diagnostics()) << "expression is not an accessor";
@@ -1098,8 +1098,8 @@
   // Gather a list of all the member and array accessors that are in this chain.
   // The list is built in reverse order as that's the order we need to access
   // the chain.
-  std::vector<ast::Expression*> accessors;
-  ast::Expression* source = expr;
+  std::vector<const ast::Expression*> accessors;
+  const ast::Expression* source = expr;
   while (true) {
     if (auto* array = source->As<ast::ArrayAccessorExpression>()) {
       accessors.insert(accessors.begin(), source);
@@ -1162,7 +1162,7 @@
 }
 
 uint32_t Builder::GenerateIdentifierExpression(
-    ast::IdentifierExpression* expr) {
+    const ast::IdentifierExpression* expr) {
   uint32_t val = 0;
   if (scope_stack_.get(expr->symbol, &val)) {
     return val;
@@ -1190,7 +1190,8 @@
   return result_id;
 }
 
-uint32_t Builder::GenerateUnaryOpExpression(ast::UnaryOpExpression* expr) {
+uint32_t Builder::GenerateUnaryOpExpression(
+    const ast::UnaryOpExpression* expr) {
   auto result = result_op();
   auto result_id = result.to_i();
 
@@ -1256,8 +1257,8 @@
 }
 
 uint32_t Builder::GenerateConstructorExpression(
-    ast::Variable* var,
-    ast::ConstructorExpression* expr,
+    const ast::Variable* var,
+    const ast::ConstructorExpression* expr,
     bool is_global_init) {
   if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
     return GenerateLiteralIfNeeded(var, scalar->literal);
@@ -1270,7 +1271,8 @@
   return 0;
 }
 
-bool Builder::is_constructor_const(ast::Expression* expr, bool is_global_init) {
+bool Builder::is_constructor_const(const ast::Expression* expr,
+                                   bool is_global_init) {
   auto* constructor = expr->As<ast::ConstructorExpression>();
   if (constructor == nullptr) {
     return false;
@@ -1326,7 +1328,7 @@
 }
 
 uint32_t Builder::GenerateTypeConstructorExpression(
-    ast::TypeConstructorExpression* init,
+    const ast::TypeConstructorExpression* init,
     bool is_global_init) {
   auto& values = init->values;
 
@@ -1502,9 +1504,10 @@
   return result.to_i();
 }
 
-uint32_t Builder::GenerateCastOrCopyOrPassthrough(const sem::Type* to_type,
-                                                  ast::Expression* from_expr,
-                                                  bool is_global_init) {
+uint32_t Builder::GenerateCastOrCopyOrPassthrough(
+    const sem::Type* to_type,
+    const ast::Expression* from_expr,
+    bool is_global_init) {
   // This should not happen as we rely on constant folding to obviate
   // casts/conversions for module-scope variables
   if (is_global_init) {
@@ -1648,8 +1651,8 @@
   return result_id;
 }
 
-uint32_t Builder::GenerateLiteralIfNeeded(ast::Variable* var,
-                                          ast::Literal* lit) {
+uint32_t Builder::GenerateLiteralIfNeeded(const ast::Variable* var,
+                                          const ast::Literal* lit) {
   ScalarConstant constant;
 
   auto* global = builder_.Sem().Get<sem::GlobalVariable>(var);
@@ -1803,7 +1806,7 @@
 }
 
 uint32_t Builder::GenerateShortCircuitBinaryExpression(
-    ast::BinaryExpression* expr) {
+    const ast::BinaryExpression* expr) {
   auto lhs_id = GenerateExpression(expr->lhs);
   if (lhs_id == 0) {
     return false;
@@ -1965,7 +1968,7 @@
   return result_mat_id.to_i();
 }
 
-uint32_t Builder::GenerateBinaryExpression(ast::BinaryExpression* expr) {
+uint32_t Builder::GenerateBinaryExpression(const ast::BinaryExpression* expr) {
   // There is special logic for short circuiting operators.
   if (expr->IsLogicalAnd() || expr->IsLogicalOr()) {
     return GenerateShortCircuitBinaryExpression(expr);
@@ -2216,7 +2219,7 @@
   return true;
 }
 
-uint32_t Builder::GenerateCallExpression(ast::CallExpression* expr) {
+uint32_t Builder::GenerateCallExpression(const ast::CallExpression* expr) {
   auto* ident = expr->func;
   auto* call = builder_.Sem().Get(expr);
   auto* target = call->Target();
@@ -2263,7 +2266,7 @@
   return result_id;
 }
 
-uint32_t Builder::GenerateIntrinsic(ast::CallExpression* call,
+uint32_t Builder::GenerateIntrinsic(const ast::CallExpression* call,
                                     const sem::Intrinsic* intrinsic) {
   auto result = result_op();
   auto result_id = result.to_i();
@@ -2648,7 +2651,7 @@
   return result_id;
 }
 
-bool Builder::GenerateTextureIntrinsic(ast::CallExpression* call,
+bool Builder::GenerateTextureIntrinsic(const ast::CallExpression* call,
                                        const sem::Intrinsic* intrinsic,
                                        Operand result_type,
                                        Operand result_id) {
@@ -2658,7 +2661,7 @@
   auto arguments = call->args;
 
   // Generates the given expression, returning the operand ID
-  auto gen = [&](ast::Expression* expr) {
+  auto gen = [&](const ast::Expression* expr) {
     auto val_id = GenerateExpression(expr);
     if (val_id == 0) {
       return Operand::Int(0);
@@ -3094,7 +3097,7 @@
                                 });
 }
 
-bool Builder::GenerateAtomicIntrinsic(ast::CallExpression* call,
+bool Builder::GenerateAtomicIntrinsic(const ast::CallExpression* call,
                                       const sem::Intrinsic* intrinsic,
                                       Operand result_type,
                                       Operand result_id) {
@@ -3368,7 +3371,8 @@
   return sampled_image.to_i();
 }
 
-uint32_t Builder::GenerateBitcastExpression(ast::BitcastExpression* expr) {
+uint32_t Builder::GenerateBitcastExpression(
+    const ast::BitcastExpression* expr) {
   auto result = result_op();
   auto result_id = result.to_i();
 
@@ -3404,7 +3408,7 @@
 }
 
 bool Builder::GenerateConditionalBlock(
-    ast::Expression* cond,
+    const ast::Expression* cond,
     const ast::BlockStatement* true_body,
     size_t cur_else_idx,
     const ast::ElseStatementList& else_stmts) {
@@ -3482,7 +3486,7 @@
   return GenerateLabel(merge_block_id);
 }
 
-bool Builder::GenerateIfStatement(ast::IfStatement* stmt) {
+bool Builder::GenerateIfStatement(const ast::IfStatement* stmt) {
   if (!continuing_stack_.empty() &&
       stmt == continuing_stack_.back().last_statement->As<ast::IfStatement>()) {
     const ContinuingInfo& ci = continuing_stack_.back();
@@ -3497,7 +3501,7 @@
     //  continuing { ...
     //    if (cond) {} else {break;}
     //  }
-    auto is_just_a_break = [](ast::BlockStatement* block) {
+    auto is_just_a_break = [](const ast::BlockStatement* block) {
       return block && (block->statements.size() == 1) &&
              block->Last()->Is<ast::BreakStatement>();
     };
@@ -3533,7 +3537,7 @@
   return true;
 }
 
-bool Builder::GenerateSwitchStatement(ast::SwitchStatement* stmt) {
+bool Builder::GenerateSwitchStatement(const ast::SwitchStatement* stmt) {
   auto merge_block = result_op();
   auto merge_block_id = merge_block.to_i();
 
@@ -3635,7 +3639,7 @@
   return GenerateLabel(merge_block_id);
 }
 
-bool Builder::GenerateReturnStatement(ast::ReturnStatement* stmt) {
+bool Builder::GenerateReturnStatement(const ast::ReturnStatement* stmt) {
   if (stmt->value) {
     auto val_id = GenerateExpression(stmt->value);
     if (val_id == 0) {
@@ -3654,7 +3658,7 @@
   return true;
 }
 
-bool Builder::GenerateLoopStatement(ast::LoopStatement* stmt) {
+bool Builder::GenerateLoopStatement(const ast::LoopStatement* stmt) {
   auto loop_header = result_op();
   auto loop_header_id = loop_header.to_i();
   if (!push_function_inst(spv::Op::OpBranch, {Operand::Int(loop_header_id)})) {
@@ -3738,7 +3742,7 @@
   return GenerateLabel(merge_block_id);
 }
 
-bool Builder::GenerateStatement(ast::Statement* stmt) {
+bool Builder::GenerateStatement(const ast::Statement* stmt) {
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return GenerateAssignStatement(a);
   }
@@ -3781,7 +3785,8 @@
   return false;
 }
 
-bool Builder::GenerateVariableDeclStatement(ast::VariableDeclStatement* stmt) {
+bool Builder::GenerateVariableDeclStatement(
+    const ast::VariableDeclStatement* stmt) {
   return GenerateFunctionVariable(stmt->variable);
 }
 
diff --git a/src/writer/spirv/builder.h b/src/writer/spirv/builder.h
index baa4d7f..0aad2dd 100644
--- a/src/writer/spirv/builder.h
+++ b/src/writer/spirv/builder.h
@@ -241,7 +241,7 @@
   /// Generates an assignment statement
   /// @param assign the statement to generate
   /// @returns true if the statement was successfully generated
-  bool GenerateAssignStatement(ast::AssignmentStatement* assign);
+  bool GenerateAssignStatement(const ast::AssignmentStatement* assign);
   /// Generates a block statement, wrapped in a push/pop scope
   /// @param stmt the statement to generate
   /// @returns true if the statement was successfully generated
@@ -253,33 +253,33 @@
   /// Generates a break statement
   /// @param stmt the statement to generate
   /// @returns true if the statement was successfully generated
-  bool GenerateBreakStatement(ast::BreakStatement* stmt);
+  bool GenerateBreakStatement(const ast::BreakStatement* stmt);
   /// Generates a continue statement
   /// @param stmt the statement to generate
   /// @returns true if the statement was successfully generated
-  bool GenerateContinueStatement(ast::ContinueStatement* stmt);
+  bool GenerateContinueStatement(const ast::ContinueStatement* stmt);
   /// Generates a discard statement
   /// @param stmt the statement to generate
   /// @returns true if the statement was successfully generated
-  bool GenerateDiscardStatement(ast::DiscardStatement* stmt);
+  bool GenerateDiscardStatement(const ast::DiscardStatement* stmt);
   /// Generates an entry point instruction
   /// @param func the function
   /// @param id the id of the function
   /// @returns true if the instruction was generated, false otherwise
-  bool GenerateEntryPoint(ast::Function* func, uint32_t id);
+  bool GenerateEntryPoint(const ast::Function* func, uint32_t id);
   /// Generates execution modes for an entry point
   /// @param func the function
   /// @param id the id of the function
   /// @returns false on failure
-  bool GenerateExecutionModes(ast::Function* func, uint32_t id);
+  bool GenerateExecutionModes(const ast::Function* func, uint32_t id);
   /// Generates an expression
   /// @param expr the expression to generate
   /// @returns the resulting ID of the expression or 0 on error
-  uint32_t GenerateExpression(ast::Expression* expr);
+  uint32_t GenerateExpression(const ast::Expression* expr);
   /// Generates the instructions for a function
   /// @param func the function to generate
   /// @returns true if the instructions were generated
-  bool GenerateFunction(ast::Function* func);
+  bool GenerateFunction(const ast::Function* func);
   /// Generates a function type if not already created
   /// @param func the function to generate for
   /// @returns the ID to use for the function type. Returns 0 on failure.
@@ -294,11 +294,11 @@
   /// Generates a function variable
   /// @param var the variable
   /// @returns true if the variable was generated
-  bool GenerateFunctionVariable(ast::Variable* var);
+  bool GenerateFunctionVariable(const ast::Variable* var);
   /// Generates a global variable
   /// @param var the variable to generate
   /// @returns true if the variable is emited.
-  bool GenerateGlobalVariable(ast::Variable* var);
+  bool GenerateGlobalVariable(const ast::Variable* var);
   /// Generates an array accessor expression.
   ///
   /// For more information on accessors see the "Pointer evaluation" section of
@@ -306,31 +306,31 @@
   ///
   /// @param expr the expresssion to generate
   /// @returns the id of the expression or 0 on failure
-  uint32_t GenerateAccessorExpression(ast::Expression* expr);
+  uint32_t GenerateAccessorExpression(const ast::Expression* expr);
   /// Generates an array accessor
   /// @param expr the accessor to generate
   /// @param info the current accessor information
   /// @returns true if the accessor was generated successfully
-  bool GenerateArrayAccessor(ast::ArrayAccessorExpression* expr,
+  bool GenerateArrayAccessor(const ast::ArrayAccessorExpression* expr,
                              AccessorInfo* info);
   /// Generates a member accessor
   /// @param expr the accessor to generate
   /// @param info the current accessor information
   /// @returns true if the accessor was generated successfully
-  bool GenerateMemberAccessor(ast::MemberAccessorExpression* expr,
+  bool GenerateMemberAccessor(const ast::MemberAccessorExpression* expr,
                               AccessorInfo* info);
   /// Generates an identifier expression
   /// @param expr the expresssion to generate
   /// @returns the id of the expression or 0 on failure
-  uint32_t GenerateIdentifierExpression(ast::IdentifierExpression* expr);
+  uint32_t GenerateIdentifierExpression(const ast::IdentifierExpression* expr);
   /// Generates a unary op expression
   /// @param expr the expression to generate
   /// @returns the id of the expression or 0 on failure
-  uint32_t GenerateUnaryOpExpression(ast::UnaryOpExpression* expr);
+  uint32_t GenerateUnaryOpExpression(const ast::UnaryOpExpression* expr);
   /// Generates an if statement
   /// @param stmt the statement to generate
   /// @returns true on success
-  bool GenerateIfStatement(ast::IfStatement* stmt);
+  bool GenerateIfStatement(const ast::IfStatement* stmt);
   /// Generates an import instruction for the "GLSL.std.450" extended
   /// instruction set, if one doesn't exist yet, and returns the import ID.
   /// @returns the import ID, or 0 on error.
@@ -340,42 +340,44 @@
   /// @param expr the expression to generate
   /// @param is_global_init set true if this is a global variable constructor
   /// @returns the ID of the expression or 0 on failure.
-  uint32_t GenerateConstructorExpression(ast::Variable* var,
-                                         ast::ConstructorExpression* expr,
+  uint32_t GenerateConstructorExpression(const ast::Variable* var,
+                                         const ast::ConstructorExpression* expr,
                                          bool is_global_init);
   /// Generates a type constructor expression
   /// @param init the expression to generate
   /// @param is_global_init set true if this is a global variable constructor
   /// @returns the ID of the expression or 0 on failure.
   uint32_t GenerateTypeConstructorExpression(
-      ast::TypeConstructorExpression* init,
+      const ast::TypeConstructorExpression* init,
       bool is_global_init);
   /// Generates a literal constant if needed
   /// @param var the variable generated for, nullptr if no variable associated.
   /// @param lit the literal to generate
   /// @returns the ID on success or 0 on failure
-  uint32_t GenerateLiteralIfNeeded(ast::Variable* var, ast::Literal* lit);
+  uint32_t GenerateLiteralIfNeeded(const ast::Variable* var,
+                                   const ast::Literal* lit);
   /// Generates a binary expression
   /// @param expr the expression to generate
   /// @returns the expression ID on success or 0 otherwise
-  uint32_t GenerateBinaryExpression(ast::BinaryExpression* expr);
+  uint32_t GenerateBinaryExpression(const ast::BinaryExpression* expr);
   /// Generates a bitcast expression
   /// @param expr the expression to generate
   /// @returns the expression ID on success or 0 otherwise
-  uint32_t GenerateBitcastExpression(ast::BitcastExpression* expr);
+  uint32_t GenerateBitcastExpression(const ast::BitcastExpression* expr);
   /// Generates a short circuting binary expression
   /// @param expr the expression to generate
   /// @returns teh expression ID on success or 0 otherwise
-  uint32_t GenerateShortCircuitBinaryExpression(ast::BinaryExpression* expr);
+  uint32_t GenerateShortCircuitBinaryExpression(
+      const ast::BinaryExpression* expr);
   /// Generates a call expression
   /// @param expr the expression to generate
   /// @returns the expression ID on success or 0 otherwise
-  uint32_t GenerateCallExpression(ast::CallExpression* expr);
+  uint32_t GenerateCallExpression(const ast::CallExpression* expr);
   /// Generates an intrinsic call
   /// @param call the call expression
   /// @param intrinsic the semantic information for the intrinsic
   /// @returns the expression ID on success or 0 otherwise
-  uint32_t GenerateIntrinsic(ast::CallExpression* call,
+  uint32_t GenerateIntrinsic(const ast::CallExpression* call,
                              const sem::Intrinsic* intrinsic);
   /// Generates a texture intrinsic call. Emits an error and returns false if
   /// we're currently outside a function.
@@ -385,7 +387,7 @@
   /// @param result_id result identifier operand of the texture instruction
   /// parameters
   /// @returns true on success
-  bool GenerateTextureIntrinsic(ast::CallExpression* call,
+  bool GenerateTextureIntrinsic(const ast::CallExpression* call,
                                 const sem::Intrinsic* intrinsic,
                                 spirv::Operand result_type,
                                 spirv::Operand result_id);
@@ -399,7 +401,7 @@
   /// @param result_type result type operand of the texture instruction
   /// @param result_id result identifier operand of the texture instruction
   /// @returns true on success
-  bool GenerateAtomicIntrinsic(ast::CallExpression* call,
+  bool GenerateAtomicIntrinsic(const ast::CallExpression* call,
                                const sem::Intrinsic* intrinsic,
                                Operand result_type,
                                Operand result_id);
@@ -419,34 +421,34 @@
   /// @param is_global_init if this is a global initializer
   /// @returns the expression ID on success or 0 otherwise
   uint32_t GenerateCastOrCopyOrPassthrough(const sem::Type* to_type,
-                                           ast::Expression* from_expr,
+                                           const ast::Expression* from_expr,
                                            bool is_global_init);
   /// Generates a loop statement
   /// @param stmt the statement to generate
   /// @returns true on successful generation
-  bool GenerateLoopStatement(ast::LoopStatement* stmt);
+  bool GenerateLoopStatement(const ast::LoopStatement* stmt);
   /// Generates a return statement
   /// @param stmt the statement to generate
   /// @returns true on success, false otherwise
-  bool GenerateReturnStatement(ast::ReturnStatement* stmt);
+  bool GenerateReturnStatement(const ast::ReturnStatement* stmt);
   /// Generates a switch statement
   /// @param stmt the statement to generate
   /// @returns ture on success, false otherwise
-  bool GenerateSwitchStatement(ast::SwitchStatement* stmt);
+  bool GenerateSwitchStatement(const ast::SwitchStatement* stmt);
   /// Generates a conditional section merge block
   /// @param cond the condition
   /// @param true_body the statements making up the true block
   /// @param cur_else_idx the index of the current else statement to process
   /// @param else_stmts the list of all else statements
   /// @returns true on success, false on failure
-  bool GenerateConditionalBlock(ast::Expression* cond,
+  bool GenerateConditionalBlock(const ast::Expression* cond,
                                 const ast::BlockStatement* true_body,
                                 size_t cur_else_idx,
                                 const ast::ElseStatementList& else_stmts);
   /// Generates a statement
   /// @param stmt the statement to generate
   /// @returns true if the statement was generated
-  bool GenerateStatement(ast::Statement* stmt);
+  bool GenerateStatement(const ast::Statement* stmt);
   /// Geneates an OpLoad
   /// @param type the type to load
   /// @param id the variable id to load
@@ -504,7 +506,7 @@
   /// Generates a variable declaration statement
   /// @param stmt the statement to generate
   /// @returns true on successfull generation
-  bool GenerateVariableDeclStatement(ast::VariableDeclStatement* stmt);
+  bool GenerateVariableDeclStatement(const ast::VariableDeclStatement* stmt);
   /// Generates a vector type declaration
   /// @param vec the vector to generate
   /// @param result the result operand
@@ -538,7 +540,7 @@
   /// @param expr the expression to check
   /// @param is_global_init if this is a global initializer
   /// @returns true if the constructor is constant
-  bool is_constructor_const(ast::Expression* expr, bool is_global_init);
+  bool is_constructor_const(const ast::Expression* expr, bool is_global_init);
 
  private:
   /// @returns an Operand with a new result ID in it. Increments the next_id_
@@ -547,7 +549,7 @@
 
   /// @returns the resolved type of the ast::Expression `expr`
   /// @param expr the expression
-  const sem::Type* TypeOf(ast::Expression* expr) const {
+  const sem::Type* TypeOf(const ast::Expression* expr) const {
     return builder_.TypeOf(expr);
   }
 
@@ -594,7 +596,7 @@
   std::unordered_map<std::string, uint32_t>
       texture_type_name_to_sampled_image_type_id_;
   ScopeStack<uint32_t> scope_stack_;
-  std::unordered_map<uint32_t, ast::Variable*> spirv_id_to_variable_;
+  std::unordered_map<uint32_t, const ast::Variable*> spirv_id_to_variable_;
   std::vector<uint32_t> merge_stack_;
   std::vector<uint32_t> continue_stack_;
   std::unordered_set<uint32_t> capability_set_;
diff --git a/src/writer/spirv/builder_binary_expression_test.cc b/src/writer/spirv/builder_binary_expression_test.cc
index e384387..a238845 100644
--- a/src/writer/spirv/builder_binary_expression_test.cc
+++ b/src/writer/spirv/builder_binary_expression_test.cc
@@ -923,7 +923,8 @@
 namespace BinaryArithVectorScalar {
 
 enum class Type { f32, i32, u32 };
-static ast::Expression* MakeVectorExpr(ProgramBuilder* builder, Type type) {
+static const ast::Expression* MakeVectorExpr(ProgramBuilder* builder,
+                                             Type type) {
   switch (type) {
     case Type::f32:
       return builder->vec3<ProgramBuilder::f32>(1.f, 1.f, 1.f);
@@ -934,7 +935,8 @@
   }
   return nullptr;
 }
-static ast::Expression* MakeScalarExpr(ProgramBuilder* builder, Type type) {
+static const ast::Expression* MakeScalarExpr(ProgramBuilder* builder,
+                                             Type type) {
   switch (type) {
     case Type::f32:
       return builder->Expr(1.f);
@@ -967,8 +969,8 @@
 TEST_P(BinaryArithVectorScalarTest, VectorScalar) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = MakeVectorExpr(this, param.type);
-  ast::Expression* rhs = MakeScalarExpr(this, param.type);
+  const ast::Expression* lhs = MakeVectorExpr(this, param.type);
+  const ast::Expression* rhs = MakeScalarExpr(this, param.type);
   std::string op_type_decl = OpTypeDecl(param.type);
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
@@ -1005,8 +1007,8 @@
 TEST_P(BinaryArithVectorScalarTest, ScalarVector) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = MakeScalarExpr(this, param.type);
-  ast::Expression* rhs = MakeVectorExpr(this, param.type);
+  const ast::Expression* lhs = MakeScalarExpr(this, param.type);
+  const ast::Expression* rhs = MakeVectorExpr(this, param.type);
   std::string op_type_decl = OpTypeDecl(param.type);
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
@@ -1068,8 +1070,8 @@
 TEST_P(BinaryArithVectorScalarMultiplyTest, VectorScalar) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = MakeVectorExpr(this, param.type);
-  ast::Expression* rhs = MakeScalarExpr(this, param.type);
+  const ast::Expression* lhs = MakeVectorExpr(this, param.type);
+  const ast::Expression* rhs = MakeScalarExpr(this, param.type);
   std::string op_type_decl = OpTypeDecl(param.type);
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
@@ -1102,8 +1104,8 @@
 TEST_P(BinaryArithVectorScalarMultiplyTest, ScalarVector) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = MakeScalarExpr(this, param.type);
-  ast::Expression* rhs = MakeVectorExpr(this, param.type);
+  const ast::Expression* lhs = MakeScalarExpr(this, param.type);
+  const ast::Expression* rhs = MakeVectorExpr(this, param.type);
   std::string op_type_decl = OpTypeDecl(param.type);
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
@@ -1151,8 +1153,8 @@
 TEST_P(BinaryArithMatrixMatrix, AddOrSubtract) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = mat3x4<f32>();
-  ast::Expression* rhs = mat3x4<f32>();
+  const ast::Expression* lhs = mat3x4<f32>();
+  const ast::Expression* rhs = mat3x4<f32>();
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
@@ -1200,8 +1202,8 @@
 TEST_P(BinaryArithMatrixMatrixMultiply, Multiply) {
   auto& param = GetParam();
 
-  ast::Expression* lhs = mat3x4<f32>();
-  ast::Expression* rhs = mat4x3<f32>();
+  const ast::Expression* lhs = mat3x4<f32>();
+  const ast::Expression* rhs = mat4x3<f32>();
 
   auto* expr = create<ast::BinaryExpression>(param.op, lhs, rhs);
 
diff --git a/src/writer/spirv/builder_function_decoration_test.cc b/src/writer/spirv/builder_function_decoration_test.cc
index 0aa57ea..309a6fb 100644
--- a/src/writer/spirv/builder_function_decoration_test.cc
+++ b/src/writer/spirv/builder_function_decoration_test.cc
@@ -50,8 +50,8 @@
 TEST_P(Decoration_StageTest, Emit) {
   auto params = GetParam();
 
-  ast::Variable* var = nullptr;
-  ast::Type* ret_type = nullptr;
+  const ast::Variable* var = nullptr;
+  const ast::Type* ret_type = nullptr;
   ast::DecorationList ret_type_decos;
   ast::StatementList body;
   if (params.stage == ast::PipelineStage::kVertex) {
diff --git a/src/writer/text_generator.h b/src/writer/text_generator.h
index 8b8aeaf..3bd2049 100644
--- a/src/writer/text_generator.h
+++ b/src/writer/text_generator.h
@@ -195,7 +195,7 @@
 
   /// @returns the resolved type of the ast::Expression `expr`
   /// @param expr the expression
-  sem::Type* TypeOf(ast::Expression* expr) const {
+  const sem::Type* TypeOf(const ast::Expression* expr) const {
     return builder_.TypeOf(expr);
   }
 
diff --git a/src/writer/wgsl/generator_impl.cc b/src/writer/wgsl/generator_impl.cc
index 68133bb..3c3a06d 100644
--- a/src/writer/wgsl/generator_impl.cc
+++ b/src/writer/wgsl/generator_impl.cc
@@ -114,7 +114,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(std::ostream& out,
+                                   const ast::Expression* expr) {
   if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
     return EmitArrayAccessor(out, a);
   }
@@ -144,8 +145,9 @@
   return false;
 }
 
-bool GeneratorImpl::EmitArrayAccessor(std::ostream& out,
-                                      ast::ArrayAccessorExpression* expr) {
+bool GeneratorImpl::EmitArrayAccessor(
+    std::ostream& out,
+    const ast::ArrayAccessorExpression* expr) {
   bool paren_lhs =
       !expr->array
            ->IsAnyOf<ast::ArrayAccessorExpression, ast::CallExpression,
@@ -170,8 +172,9 @@
   return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
-                                       ast::MemberAccessorExpression* expr) {
+bool GeneratorImpl::EmitMemberAccessor(
+    std::ostream& out,
+    const ast::MemberAccessorExpression* expr) {
   bool paren_lhs =
       !expr->structure
            ->IsAnyOf<ast::ArrayAccessorExpression, ast::CallExpression,
@@ -193,7 +196,7 @@
 }
 
 bool GeneratorImpl::EmitBitcast(std::ostream& out,
-                                ast::BitcastExpression* expr) {
+                                const ast::BitcastExpression* expr) {
   out << "bitcast<";
   if (!EmitType(out, expr->type)) {
     return false;
@@ -208,7 +211,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(std::ostream& out,
+                             const ast::CallExpression* expr) {
   if (!EmitExpression(out, expr->func)) {
     return false;
   }
@@ -233,15 +237,16 @@
 }
 
 bool GeneratorImpl::EmitConstructor(std::ostream& out,
-                                    ast::ConstructorExpression* expr) {
+                                    const ast::ConstructorExpression* expr) {
   if (auto* scalar = expr->As<ast::ScalarConstructorExpression>()) {
     return EmitScalarConstructor(out, scalar);
   }
   return EmitTypeConstructor(out, expr->As<ast::TypeConstructorExpression>());
 }
 
-bool GeneratorImpl::EmitTypeConstructor(std::ostream& out,
-                                        ast::TypeConstructorExpression* expr) {
+bool GeneratorImpl::EmitTypeConstructor(
+    std::ostream& out,
+    const ast::TypeConstructorExpression* expr) {
   if (!EmitType(out, expr->type)) {
     return false;
   }
@@ -266,11 +271,11 @@
 
 bool GeneratorImpl::EmitScalarConstructor(
     std::ostream& out,
-    ast::ScalarConstructorExpression* expr) {
+    const ast::ScalarConstructorExpression* expr) {
   return EmitLiteral(out, expr->literal);
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, ast::Literal* lit) {
+bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::Literal* lit) {
   if (auto* bl = lit->As<ast::BoolLiteral>()) {
     out << (bl->value ? "true" : "false");
   } else if (auto* fl = lit->As<ast::FloatLiteral>()) {
@@ -287,12 +292,12 @@
 }
 
 bool GeneratorImpl::EmitIdentifier(std::ostream& out,
-                                   ast::IdentifierExpression* expr) {
+                                   const ast::IdentifierExpression* expr) {
   out << program_->Symbols().NameFor(expr->symbol);
   return true;
 }
 
-bool GeneratorImpl::EmitFunction(ast::Function* func) {
+bool GeneratorImpl::EmitFunction(const ast::Function* func) {
   if (func->decorations.size()) {
     if (!EmitDecorations(line(), func->decorations)) {
       return false;
@@ -592,7 +597,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitVariable(std::ostream& out, ast::Variable* var) {
+bool GeneratorImpl::EmitVariable(std::ostream& out, const ast::Variable* var) {
   if (!var->decorations.empty()) {
     if (!EmitDecorations(out, var->decorations)) {
       return false;
@@ -706,7 +711,8 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(std::ostream& out,
+                               const ast::BinaryExpression* expr) {
   out << "(";
 
   if (!EmitExpression(out, expr->lhs)) {
@@ -785,7 +791,7 @@
 }
 
 bool GeneratorImpl::EmitUnaryOp(std::ostream& out,
-                                ast::UnaryOpExpression* expr) {
+                                const ast::UnaryOpExpression* expr) {
   switch (expr->op) {
     case ast::UnaryOp::kAddressOf:
       out << "&";
@@ -824,7 +830,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitStatement(ast::Statement* stmt) {
+bool GeneratorImpl::EmitStatement(const ast::Statement* stmt) {
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return EmitAssign(a);
   }
@@ -890,7 +896,7 @@
   return EmitStatements(stmts);
 }
 
-bool GeneratorImpl::EmitAssign(ast::AssignmentStatement* stmt) {
+bool GeneratorImpl::EmitAssign(const ast::AssignmentStatement* stmt) {
   auto out = line();
 
   if (!EmitExpression(out, stmt->lhs)) {
@@ -908,12 +914,12 @@
   return true;
 }
 
-bool GeneratorImpl::EmitBreak(ast::BreakStatement*) {
+bool GeneratorImpl::EmitBreak(const ast::BreakStatement*) {
   line() << "break;";
   return true;
 }
 
-bool GeneratorImpl::EmitCase(ast::CaseStatement* stmt) {
+bool GeneratorImpl::EmitCase(const ast::CaseStatement* stmt) {
   if (stmt->IsDefault()) {
     line() << "default: {";
   } else {
@@ -942,17 +948,17 @@
   return true;
 }
 
-bool GeneratorImpl::EmitContinue(ast::ContinueStatement*) {
+bool GeneratorImpl::EmitContinue(const ast::ContinueStatement*) {
   line() << "continue;";
   return true;
 }
 
-bool GeneratorImpl::EmitFallthrough(ast::FallthroughStatement*) {
+bool GeneratorImpl::EmitFallthrough(const ast::FallthroughStatement*) {
   line() << "fallthrough;";
   return true;
 }
 
-bool GeneratorImpl::EmitIf(ast::IfStatement* stmt) {
+bool GeneratorImpl::EmitIf(const ast::IfStatement* stmt) {
   {
     auto out = line();
     out << "if (";
@@ -988,12 +994,12 @@
   return true;
 }
 
-bool GeneratorImpl::EmitDiscard(ast::DiscardStatement*) {
+bool GeneratorImpl::EmitDiscard(const ast::DiscardStatement*) {
   line() << "discard;";
   return true;
 }
 
-bool GeneratorImpl::EmitLoop(ast::LoopStatement* stmt) {
+bool GeneratorImpl::EmitLoop(const ast::LoopStatement* stmt) {
   line() << "loop {";
   increment_indent();
 
@@ -1016,7 +1022,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitForLoop(ast::ForLoopStatement* stmt) {
+bool GeneratorImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
   TextBuffer init_buf;
   if (auto* init = stmt->initializer) {
     TINT_SCOPED_ASSIGNMENT(current_buffer_, &init_buf);
@@ -1090,7 +1096,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitReturn(ast::ReturnStatement* stmt) {
+bool GeneratorImpl::EmitReturn(const ast::ReturnStatement* stmt) {
   auto out = line();
   out << "return";
   if (stmt->value) {
@@ -1103,7 +1109,7 @@
   return true;
 }
 
-bool GeneratorImpl::EmitSwitch(ast::SwitchStatement* stmt) {
+bool GeneratorImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
   {
     auto out = line();
     out << "switch(";
diff --git a/src/writer/wgsl/generator_impl.h b/src/writer/wgsl/generator_impl.h
index c413ee2..c626d76 100644
--- a/src/writer/wgsl/generator_impl.h
+++ b/src/writer/wgsl/generator_impl.h
@@ -63,21 +63,22 @@
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the array accessor was emitted
-  bool EmitArrayAccessor(std::ostream& out, ast::ArrayAccessorExpression* expr);
+  bool EmitArrayAccessor(std::ostream& out,
+                         const ast::ArrayAccessorExpression* expr);
   /// Handles an assignment statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitAssign(ast::AssignmentStatement* stmt);
+  bool EmitAssign(const ast::AssignmentStatement* stmt);
   /// Handles generating a binary expression
   /// @param out the output of the expression stream
   /// @param expr the binary expression
   /// @returns true if the expression was emitted, false otherwise
-  bool EmitBinary(std::ostream& out, ast::BinaryExpression* expr);
+  bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
   /// Handles generating a bitcast expression
   /// @param out the output of the expression stream
   /// @param expr the bitcast expression
   /// @returns true if the bitcast was emitted
-  bool EmitBitcast(std::ostream& out, ast::BitcastExpression* expr);
+  bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
   /// Handles a block statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
@@ -85,84 +86,85 @@
   /// Handles a break statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitBreak(ast::BreakStatement* stmt);
+  bool EmitBreak(const ast::BreakStatement* stmt);
   /// Handles generating a call expression
   /// @param out the output of the expression stream
   /// @param expr the call expression
   /// @returns true if the call expression is emitted
-  bool EmitCall(std::ostream& out, ast::CallExpression* expr);
+  bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
   /// Handles a case statement
   /// @param stmt the statement
   /// @returns true if the statment was emitted successfully
-  bool EmitCase(ast::CaseStatement* stmt);
+  bool EmitCase(const ast::CaseStatement* stmt);
   /// Handles generating a scalar constructor
   /// @param out the output of the expression stream
   /// @param expr the scalar constructor expression
   /// @returns true if the scalar constructor is emitted
   bool EmitScalarConstructor(std::ostream& out,
-                             ast::ScalarConstructorExpression* expr);
+                             const ast::ScalarConstructorExpression* expr);
   /// Handles a continue statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted successfully
-  bool EmitContinue(ast::ContinueStatement* stmt);
+  bool EmitContinue(const ast::ContinueStatement* stmt);
   /// Handles generate an Expression
   /// @param out the output of the expression stream
   /// @param expr the expression
   /// @returns true if the expression was emitted
-  bool EmitExpression(std::ostream& out, ast::Expression* expr);
+  bool EmitExpression(std::ostream& out, const ast::Expression* expr);
   /// Handles generating a fallthrough statement
   /// @param stmt the fallthrough statement
   /// @returns true if the statement was successfully emitted
-  bool EmitFallthrough(ast::FallthroughStatement* stmt);
+  bool EmitFallthrough(const ast::FallthroughStatement* stmt);
   /// Handles generating a function
   /// @param func the function to generate
   /// @returns true if the function was emitted
-  bool EmitFunction(ast::Function* func);
+  bool EmitFunction(const ast::Function* func);
   /// Handles generating an identifier expression
   /// @param out the output of the expression stream
   /// @param expr the identifier expression
   /// @returns true if the identifeir was emitted
-  bool EmitIdentifier(std::ostream& out, ast::IdentifierExpression* expr);
+  bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
   /// Handles an if statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitIf(ast::IfStatement* stmt);
+  bool EmitIf(const ast::IfStatement* stmt);
   /// Handles generating constructor expressions
   /// @param out the output of the expression stream
   /// @param expr the constructor expression
   /// @returns true if the expression was emitted
-  bool EmitConstructor(std::ostream& out, ast::ConstructorExpression* expr);
+  bool EmitConstructor(std::ostream& out,
+                       const ast::ConstructorExpression* expr);
   /// Handles generating a discard statement
   /// @param stmt the discard statement
   /// @returns true if the statement was successfully emitted
-  bool EmitDiscard(ast::DiscardStatement* stmt);
+  bool EmitDiscard(const ast::DiscardStatement* stmt);
   /// Handles a literal
   /// @param out the output of the expression stream
   /// @param lit the literal to emit
   /// @returns true if the literal was successfully emitted
-  bool EmitLiteral(std::ostream& out, ast::Literal* lit);
+  bool EmitLiteral(std::ostream& out, const ast::Literal* lit);
   /// Handles a loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emtited
-  bool EmitLoop(ast::LoopStatement* stmt);
+  bool EmitLoop(const ast::LoopStatement* stmt);
   /// Handles a for-loop statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emtited
-  bool EmitForLoop(ast::ForLoopStatement* stmt);
+  bool EmitForLoop(const ast::ForLoopStatement* stmt);
   /// Handles a member accessor expression
   /// @param out the output of the expression stream
   /// @param expr the member accessor expression
   /// @returns true if the member accessor was emitted
   bool EmitMemberAccessor(std::ostream& out,
-                          ast::MemberAccessorExpression* expr);
+                          const ast::MemberAccessorExpression* expr);
   /// Handles return statements
   /// @param stmt the statement to emit
   /// @returns true if the statement was successfully emitted
-  bool EmitReturn(ast::ReturnStatement* stmt);
+  bool EmitReturn(const ast::ReturnStatement* stmt);
   /// Handles statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitStatement(ast::Statement* stmt);
+  bool EmitStatement(const ast::Statement* stmt);
   /// Handles a statement list
   /// @param stmts the statements to emit
   /// @returns true if the statements were emitted
@@ -174,7 +176,7 @@
   /// Handles generating a switch statement
   /// @param stmt the statement to emit
   /// @returns true if the statement was emitted
-  bool EmitSwitch(ast::SwitchStatement* stmt);
+  bool EmitSwitch(const ast::SwitchStatement* stmt);
   /// Handles generating type
   /// @param out the output of the expression stream
   /// @param type the type to generate
@@ -199,17 +201,17 @@
   /// @param expr the type constructor expression
   /// @returns true if the constructor is emitted
   bool EmitTypeConstructor(std::ostream& out,
-                           ast::TypeConstructorExpression* expr);
+                           const ast::TypeConstructorExpression* expr);
   /// Handles a unary op expression
   /// @param out the output of the expression stream
   /// @param expr the expression to emit
   /// @returns true if the expression was emitted
-  bool EmitUnaryOp(std::ostream& out, ast::UnaryOpExpression* expr);
+  bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
   /// Handles generating a variable
   /// @param out the output of the expression stream
   /// @param var the variable to generate
   /// @returns true if the variable was emitted
-  bool EmitVariable(std::ostream& out, ast::Variable* var);
+  bool EmitVariable(std::ostream& out, const ast::Variable* var);
   /// Handles generating a decoration list
   /// @param out the output of the expression stream
   /// @param decos the decoration list
diff --git a/src/writer/wgsl/generator_impl_binary_test.cc b/src/writer/wgsl/generator_impl_binary_test.cc
index 3432f83..aeaf894 100644
--- a/src/writer/wgsl/generator_impl_binary_test.cc
+++ b/src/writer/wgsl/generator_impl_binary_test.cc
@@ -31,7 +31,7 @@
 TEST_P(WgslBinaryTest, Emit) {
   auto params = GetParam();
 
-  auto op_ty = [&]() -> ast::Type* {
+  auto op_ty = [&]() -> const ast::Type* {
     if (params.op == ast::BinaryOp::kLogicalAnd ||
         params.op == ast::BinaryOp::kLogicalOr) {
       return ty.bool_();
diff --git a/tools/src/cmd/remote-compile/main.cc b/tools/src/cmd/remote-compile/main.cc
index df541c6..d6f62e7 100644
--- a/tools/src/cmd/remote-compile/main.cc
+++ b/tools/src/cmd/remote-compile/main.cc
@@ -168,7 +168,7 @@
 
   explicit Message(Type ty) : type(ty) {}
 
-  Type const type;
+  const Type type;
 };
 
 struct ConnectionResponse : Message {  // Server -> Client